[
  {
    "path": ".devcontainer/Dockerfile",
    "content": "FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04\n\n# use this Dockerfile to install additional tools you might need, e.g.\n# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \\\n#     && apt-get -y install --no-install-recommends <your-package-list-here>\n"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "// The Dev Container format allows you to configure your environment. At the heart of it\n// is a Docker image or Dockerfile which controls the tools available in your environment.\n//\n// See https://aka.ms/devcontainer.json for more information.\n{\n  \"name\": \"Gorse\",\n  // Use \"image\": \"mcr.microsoft.com/devcontainers/base:ubuntu-24.04\",\n  // instead of the build to use a pre-built image.\n  \"build\": {\n    \"context\": \".\",\n    \"dockerfile\": \"Dockerfile\"\n  },\n  // Features add additional features to your environment. See https://containers.dev/features\n  // Beware: features are not supported on all platforms and may have unintended side-effects.\n  \"features\": {\n    \"ghcr.io/devcontainers/features/docker-in-docker\": {\n      \"moby\": false\n    },\n    \"ghcr.io/devcontainers/features/go\": {},\n    \"ghcr.io/devcontainers/features/python\": {},\n    \"ghcr.io/devcontainers-extra/features/protoc\": {}\n  },\n  \"postCreateCommand\": [\n    \"go install google.golang.org/protobuf/cmd/protoc-gen-go@latest\",\n    \"go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest\"\n  ]\n}\n"
  },
  {
    "path": ".dockerignore",
    "content": ".github\nassets\n\nLICENSE\n\n*.yml\n*.md\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\nPlease answer these questions before submitting your issue. Thanks!\n\n**Gorse version**\nPrint build info by the `--version` option.\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior.\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/custom.md",
    "content": "---\nname: Custom issue template\nabout: Describe this issue template's purpose here.\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/workflows/assign_issue.yml",
    "content": "name: 'assign issues'\n\non:\n  issue_comment:\n    types: [created, edited]\n\njobs:\n  assign_issues:\n    name: assign issues\n    if: ${{ !github.event.issue.pull_request && github.event.comment.body == '/assign' }}\n    runs-on: ubuntu-latest\n    steps:\n      - name: 'Assign issue'\n        uses: pozil/auto-assign-issue@v1.4.0\n        with:\n          assignees: ${{ github.event.comment.user.login }}\n"
  },
  {
    "path": ".github/workflows/backport.yml",
    "content": "name: backport merged pull request\n\non:\n  pull_request_target:\n    types: [closed]\n  issue_comment:\n    types: [created]\n\npermissions:\n  contents: write       # so it can comment\n  pull-requests: write  # so it can create pull requests\n\njobs:\n  backport:\n    name: backport pull request\n    runs-on: ubuntu-latest\n\n    # Only run when pull request is merged\n    # or when a comment containing `/backport` is created by someone other than the \n    # https://github.com/backport-action bot user (user id: 97796249). Note that if you use your\n    # own PAT as `github_token`, that you should replace this id with yours.\n    if: >\n      (\n        github.event_name == 'pull_request' &&\n        github.event.pull_request.merged\n      ) || (\n        github.event_name == 'issue_comment' &&\n        github.event.issue.pull_request &&\n        github.event.comment.user.id != 97796249 &&\n        contains(github.event.comment.body, '/backport')\n      )\n    steps:\n      - uses: actions/checkout@v3\n      - name: Create backport pull requests\n        uses: korthout/backport-action@v1\n"
  },
  {
    "path": ".github/workflows/build_docker.yml",
    "content": "name: build\n\non:\n  push:\n    branches:    \n      - master\n\njobs:\n  windows_images:\n    name: docker images (windows)\n    runs-on: windows-latest\n    steps:\n      - name: Pull source\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n\n      - name: Login to DockerHub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Build docker image\n        run: |\n          foreach ($image in \"gorse-master\", \"gorse-server\", \"gorse-worker\", \"gorse-in-one\") {\n            docker build -f cmd/$image/Dockerfile.windows `\n              -t zhenghaoz/${image}:nightly-windowsservercore .\n            docker image push --all-tags zhenghaoz/$image\n          }\n\n  docker_images:\n    name: docker images\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        targets: [default]\n    steps:\n      - name: Pull source\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n        with:\n          buildkitd-config-inline: |\n            [worker.oci]\n              max-parallelism = 1\n\n      - name: Login to DockerHub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Build docker image\n        uses: docker/bake-action@v6\n        with:\n          source: .\n          targets: ${{ matrix.targets }}\n          push: true\n        env:\n          AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}\n"
  },
  {
    "path": ".github/workflows/build_release.yml",
    "content": "name: release\n\non:\n  release:\n    types: [published]\n\njobs:\n  binaries:\n    name: binaries\n    runs-on: macos-latest\n    steps:\n      - name: Pull source\n        uses: actions/checkout@v5\n\n      - name: Set up Go\n        uses: actions/setup-go@v5\n        with:\n          go-version-file: ./go.mod\n\n      - name: Install gox\n        run: go install github.com/mitchellh/gox@master\n\n      - name: Build release (windows and linux)\n        run: >\n          gox -output=\"{{.OS}}/{{.Arch}}/{{.Dir}}\" \\\n            -osarch='windows/arm64 windows/amd64 linux/arm64 linux/amd64 linux/riscv64' -ldflags=\"\n            -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))'\n            -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)'\n            -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)'\" ./...\n        env:\n          CGO_ENABLED: 0\n\n      - name: Build release (darwin)\n        run: >\n          gox -output=\"{{.OS}}/{{.Arch}}/{{.Dir}}\" \\\n            -osarch='darwin/arm64' -ldflags=\"\n            -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))'\n            -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)'\n            -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)'\" ./...\n\n      - name: Install zip\n        run: brew install zip\n\n      - name: Zip binaries\n        run: |\n          zip -j gorse_linux_amd64.zip linux/amd64/gorse-*\n          zip -j gorse_linux_arm64.zip linux/arm64/gorse-*\n          zip -j gorse_linux_riscv64.zip linux/riscv64/gorse-*\n          zip -j gorse_windows_amd64.zip windows/amd64/gorse-*\n          zip -j gorse_windows_arm64.zip windows/arm64/gorse-*\n          zip -j gorse_darwin_arm64.zip darwin/arm64/gorse-*\n\n      - name: Upload release\n        uses: svenstaro/upload-release-action@v2\n        with:\n          repo_token: ${{ secrets.GITHUB_TOKEN }}\n          file: gorse_*_*.zip\n          tag: ${{ github.ref }}\n          overwrite: true\n          file_glob: true\n\n  docker_images:\n    name: docker images\n    runs-on: ubuntu-latest\n    steps:\n      - name: Pull source\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n        with:\n          buildkitd-config-inline: |\n            [worker.oci]\n              max-parallelism = 1\n\n      - name: Login to DockerHub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - id: get_version\n        uses: battila7/get-version-action@v2\n\n      - if: github.event.release.prerelease == false\n        name: Build Docker image\n        uses: docker/bake-action@v6\n        with:\n          source: .\n          targets: default\n          push: true\n        env:\n          VERSIONS: latest,${{ steps.get_version.outputs.major }}.${{ steps.get_version.outputs.minor }},${{ steps.get_version.outputs.version-without-v }}\n\n      - if: github.event.release.prerelease == true\n        name: Build prerelease Docker image\n        uses: docker/bake-action@v6\n        with:\n          source: .\n          targets: default\n          push: true\n        env:\n          VERSIONS: ${{ steps.get_version.outputs.version-without-v }}\n\n  windows_images:\n    name: docker images (windows)\n    runs-on: windows-latest\n    steps:\n      - name: Pull source\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n\n      - id: get_version\n        uses: battila7/get-version-action@v2\n\n      - name: Login to DockerHub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Build docker image\n        if: github.event.release.prerelease == false\n        run: |\n          foreach ($image in \"gorse-master\", \"gorse-server\", \"gorse-worker\", \"gorse-in-one\") {\n            docker build -f cmd/${image}/Dockerfile.windows `\n              -t zhenghaoz/${image}:windowsservercore `\n              -t zhenghaoz/${image}:${{ steps.get_version.outputs.major }}.${{ steps.get_version.outputs.minor }}-windowsservercore `\n              -t zhenghaoz/${image}:${{ steps.get_version.outputs.version-without-v }}-windowsservercore .\n            docker image push --all-tags zhenghaoz/$image\n          }\n\n      - name: Build prerelease docker image\n        if: github.event.release.prerelease == true\n        run: |\n          foreach ($image in \"gorse-master\", \"gorse-server\", \"gorse-worker\", \"gorse-in-one\") {\n            docker build -f cmd/${image}/Dockerfile.windows `\n              -t zhenghaoz/${image}:${{ steps.get_version.outputs.version-without-v }}-windowsservercore .\n            docker image push --all-tags zhenghaoz/$image\n          }\n"
  },
  {
    "path": ".github/workflows/build_test.yml",
    "content": "name: test\n\non:\n  push:\n    branches:    \n      - master\n      - 'release-**'\n  pull_request:\n    branches:    \n      - master\n      - 'release-**'\n\njobs:\n  unit_test:\n    strategy:\n      matrix:\n        os: [ubuntu-latest, ubuntu-24.04-arm]\n    name: unit tests\n    runs-on: ${{ matrix.os }}\n\n    services:\n      mysql:\n        image: mysql:8.0\n        ports:\n          - 3306\n        env:\n          MYSQL_ROOT_PASSWORD: password\n        options: --health-cmd=\"mysqladmin ping\" --health-interval=10s --health-timeout=5s --health-retries=3\n\n      postgres:\n        image: postgres:10.0\n        ports:\n          - 5432\n        env:\n          POSTGRES_USER: gorse\n          POSTGRES_PASSWORD: gorse_pass\n        options: >-\n          --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n\n      mongo:\n        image: mongo:4.0\n        ports:\n          - 27017\n        env:\n          MONGO_INITDB_ROOT_USERNAME: root\n          MONGO_INITDB_ROOT_PASSWORD: password\n        options: >-\n          --health-cmd mongo\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n\n      clickhouse:\n        image: clickhouse/clickhouse-server:23\n        ports:\n          - 8123\n        options: >-\n          --health-cmd=\"clickhouse-client --query 'SELECT 1'\" \n          --health-interval=10s \n          --health-timeout=5s \n          --health-retries=5\n\n      redis:\n        image: redis/redis-stack:6.2.6-v9\n        ports:\n          - 6379\n\n      rustfs:\n        image: rustfs/rustfs:alpha\n        ports:\n          - 9000\n        env:\n          RUSTFS_ACCESS_KEY: rustfsadmin\n          RUSTFS_SECRET_KEY: rustfsadmin\n\n      qdrant:\n        image: qdrant/qdrant:latest\n        ports:\n          - 6334\n      \n      weaviate:\n        image: cr.weaviate.io/semitechnologies/weaviate:1.35.7\n        ports:\n          - 8080\n\n    steps:\n    - name: Set up dataset\n      run: |\n        mkdir -p ~/.gorse/dataset\n        mkdir -p ~/.gorse/download\n        wget https://cdn.gorse.io/datasets/ml-100k.zip -P ~/.gorse/download\n        wget https://cdn.gorse.io/datasets/ml-1m.zip -P ~/.gorse/download\n        wget https://cdn.gorse.io/datasets/pinterest-20.zip -P ~/.gorse/download\n        wget https://cdn.gorse.io/datasets/frappe.zip -P ~/.gorse/download\n        wget https://cdn.gorse.io/datasets/ml-tag.zip -P ~/.gorse/download\n        wget https://cdn.gorse.io/datasets/criteo.zip -P ~/.gorse/download\n        wget https://cdn.gorse.io/datasets/mnist.zip -P ~/.gorse/download\n        unzip ~/.gorse/download/ml-100k.zip -d ~/.gorse/dataset\n        unzip ~/.gorse/download/ml-1m.zip -d ~/.gorse/dataset\n        unzip ~/.gorse/download/pinterest-20.zip -d ~/.gorse/dataset\n        unzip ~/.gorse/download/frappe.zip -d ~/.gorse/dataset\n        unzip ~/.gorse/download/ml-tag.zip -d ~/.gorse/dataset\n        unzip ~/.gorse/download/criteo.zip -d ~/.gorse/dataset\n        unzip ~/.gorse/download/mnist.zip -d ~/.gorse/dataset\n\n    - uses: actions/checkout@v2\n    - uses: actions/setup-go@v5\n      with:\n        go-version-file: ./go.mod\n\n    - name: Install and start Azurite\n      uses: potatoqualitee/azuright@v1\n\n    - name: Setup Milvus\n      run: |\n        curl -sfL https://raw.githubusercontent.com/milvus-io/milvus/master/scripts/standalone_embed.sh -o standalone_embed.sh\n        bash standalone_embed.sh start\n      working-directory: ${{ runner.temp }}\n\n    - name: Test\n      run: go test -timeout 30m -v ./... -coverprofile=coverage.txt -covermode=atomic -coverpkg=./...\n      env:\n        # MySQL\n        MYSQL_URI: mysql://root:password@tcp(localhost:${{ job.services.mysql.ports[3306] }})/\n        # Postgres\n        POSTGRES_URI: postgres://gorse:gorse_pass@localhost:${{ job.services.postgres.ports[5432] }}/\n        # MongoDB\n        MONGO_URI: mongodb://root:password@localhost:${{ job.services.mongo.ports[27017] }}/\n        # ClickHouse\n        CLICKHOUSE_URI: clickhouse://localhost:${{ job.services.clickhouse.ports[8123] }}/\n        # Redis\n        REDIS_URI: redis://localhost:${{ job.services.redis.ports[6379] }}/\n        # S3\n        S3_ENDPOINT: localhost:${{ job.services.rustfs.ports[9000] }}\n        S3_ACCESS_KEY_ID: rustfsadmin\n        S3_SECRET_ACCESS_KEY: rustfsadmin\n        # Azure Blob (Azurite)\n        AZURE_STORAGE_CONNECTION_STRING: DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;\n        # Qdrant\n        QDRANT_URI: qdrant://localhost:${{ job.services.qdrant.ports[6334] }}/\n        # Weaviate\n        WEAVIATE_URI: weaviate://localhost:${{ job.services.weaviate.ports[8080] }}/\n\n    - name: Upload coverage to Codecov\n      if: matrix.os == 'ubuntu-latest'\n      uses: codecov/codecov-action@v5\n      with:\n        files: ./coverage.txt\n        fail_ci_if_error: false\n        token: ${{ secrets.CODECOV_TOKEN }}\n\n  unit_test_macos:\n    name: unit tests (macos-latest)\n    runs-on: macos-latest\n    steps:\n    - name: Set up dataset\n      run: |\n        mkdir -p ~/.gorse/dataset\n        mkdir -p ~/.gorse/download\n        wget https://cdn.gorse.io/datasets/ml-100k.zip -P ~/.gorse/download\n        wget https://cdn.gorse.io/datasets/ml-1m.zip -P ~/.gorse/download\n        wget https://cdn.gorse.io/datasets/pinterest-20.zip -P ~/.gorse/download\n        wget https://cdn.gorse.io/datasets/frappe.zip -P ~/.gorse/download\n        wget https://cdn.gorse.io/datasets/ml-tag.zip -P ~/.gorse/download\n        wget https://cdn.gorse.io/datasets/criteo.zip -P ~/.gorse/download\n        wget https://cdn.gorse.io/datasets/mnist.zip -P ~/.gorse/download\n        unzip ~/.gorse/download/ml-100k.zip -d ~/.gorse/dataset\n        unzip ~/.gorse/download/ml-1m.zip -d ~/.gorse/dataset\n        unzip ~/.gorse/download/pinterest-20.zip -d ~/.gorse/dataset\n        unzip ~/.gorse/download/frappe.zip -d ~/.gorse/dataset\n        unzip ~/.gorse/download/ml-tag.zip -d ~/.gorse/dataset\n        unzip ~/.gorse/download/criteo.zip -d ~/.gorse/dataset\n        unzip ~/.gorse/download/mnist.zip -d ~/.gorse/dataset\n\n    - uses: actions/checkout@v2\n    - uses: actions/setup-go@v5\n      with:\n        go-version-file: ./go.mod\n\n    - name: Test\n      run: go test -timeout 20m -v ./... -skip \"TestPostgres|TestMySQL|TestMongo|TestRedis|TestClickHouse|TestMilvus|TestQdrant|TestWeaviate\"\n\n  unit_test_windows:\n    strategy:\n      matrix:\n        os: [windows-latest, windows-11-arm]\n    name: unit tests\n    runs-on: ${{ matrix.os }}\n    steps:\n    - name: Set up dataset\n      run: |\n        New-Item -Type Directory -Path ~/.gorse/dataset\n        New-Item -Type Directory -Path ~/.gorse/download\n        Invoke-WebRequest https://cdn.gorse.io/datasets/ml-100k.zip -OutFile ~/.gorse/download/ml-100k.zip\n        Invoke-WebRequest https://cdn.gorse.io/datasets/ml-1m.zip -OutFile ~/.gorse/download/ml-1m.zip\n        Invoke-WebRequest https://cdn.gorse.io/datasets/pinterest-20.zip -OutFile ~/.gorse/download/pinterest-20.zip\n        Invoke-WebRequest https://cdn.gorse.io/datasets/frappe.zip -OutFile ~/.gorse/download/frappe.zip\n        Invoke-WebRequest https://cdn.gorse.io/datasets/ml-tag.zip -OutFile ~/.gorse/download/ml-tag.zip\n        Invoke-WebRequest https://cdn.gorse.io/datasets/criteo.zip -OutFile ~/.gorse/download/criteo.zip\n        Invoke-WebRequest https://cdn.gorse.io/datasets/mnist.zip -OutFile ~/.gorse/download/mnist.zip\n        Expand-Archive ~/.gorse/download/ml-100k.zip -DestinationPath ~/.gorse/dataset\n        Expand-Archive ~/.gorse/download/ml-1m.zip -DestinationPath ~/.gorse/dataset\n        Expand-Archive ~/.gorse/download/pinterest-20.zip -DestinationPath ~/.gorse/dataset\n        Expand-Archive ~/.gorse/download/frappe.zip -DestinationPath ~/.gorse/dataset\n        Expand-Archive ~/.gorse/download/ml-tag.zip -DestinationPath ~/.gorse/dataset\n        Expand-Archive ~/.gorse/download/criteo.zip -DestinationPath ~/.gorse/dataset\n        Expand-Archive ~/.gorse/download/mnist.zip -DestinationPath ~/.gorse/dataset\n\n    - uses: actions/checkout@v2\n    - uses: actions/setup-go@v5\n      with:\n        go-version-file: ./go.mod\n\n    - name: Test\n      run: go test -timeout 20m -v ./... -skip \"TestPostgres|TestMySQL|TestMongo|TestRedis|TestClickHouse|TestMilvus|TestQdrant|TestWeaviate\"\n\n  integrate_test:\n    name: integrate tests\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        database: [mysql, postgres, mongo, sqlite]\n    steps:\n    - uses: actions/checkout@v5\n      with:\n        fetch-depth: 0\n\n    - uses: actions/setup-go@v5\n      with:\n        go-version-file: ./go.mod\n\n    - uses: cuchi/jinja2-action@v1.2.0\n      with:\n        template: client/docker-compose.yml.j2\n        output_file: docker-compose.yml\n        strict: true\n        variables: |\n          database=${{ matrix.database }}\n\n    - name: Setup test\n      run: ./client/setup-test.sh\n      env:\n        DOCKER_BUILDKIT: 1\n\n    - name: Test\n      run: go test ./client/\n      env:\n        GORSE_SERVER_ENDPOINT: http://localhost:8087\n        GORSE_DASHBOARD_ENDPOINT: http://localhost:8088\n\n  compat_test:\n    name: compatibility tests\n    runs-on: ubuntu-latest\n\n    services:\n      mariadb:\n        image: mariadb:10.2\n        ports:\n          - 3306\n        env:\n          MYSQL_ROOT_PASSWORD: password\n\n      kvrocks:\n        image: apache/kvrocks:nightly\n        ports:\n          - 6666\n\n    steps:\n      - name: Install pre-requisites\n        uses: awalsh128/cache-apt-pkgs-action@latest\n        with:\n          packages: redis-tools\n\n      - uses: actions/checkout@v5\n        with:\n          repository: gorse-cloud/redis-stack\n          path: redis-stack\n\n      - name: Setup Redis cluster\n        run: |\n          docker compose -f redis-stack/docker-compose.yml --project-directory redis-stack up -d\n          for i in {1..5}; do\n            redis-cli -p 7005 ping | grep PONG && break\n            sleep 10\n          done\n\n      - uses: actions/checkout@v2\n      - uses: actions/setup-go@v5\n        with:\n          go-version-file: ./go.mod\n\n      - name: Test MariaDB\n        run: go test ./storage/data -run TestMySQL\n        env:\n          MYSQL_URI: mysql://root:password@tcp(localhost:${{ job.services.mariadb.ports[3306] }})/\n\n      - name: Test Kvrocks\n        run: go test ./storage/cache -run ^TestRedis\n        env:\n          REDIS_URI: redis://localhost:${{ job.services.kvrocks.ports[6666] }}/\n\n      - name: Test Redis cluster (1 address)\n        run: go test ./storage/cache -run ^TestRedis\n        env:\n          REDIS_URI: redis+cluster://localhost:7000\n      \n      - name: Test Redis cluster (6 addresses)\n        run: go test ./storage/cache -run ^TestRedis\n        env:\n          REDIS_URI: redis+cluster://localhost:7000?addr=localhost:7001&addr=localhost:7002&addr=localhost:7003&addr=localhost:7004&addr=localhost:7005\n\n  playground:\n    name: playground\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - name: Build Docker image\n        run: docker build -t zhenghaoz/gorse-in-one -f ./cmd/gorse-in-one/Dockerfile .\n      - name: Run playground\n        run: docker run -d -p 8088:8088 --name playground zhenghaoz/gorse-in-one --playground\n      - name: Check dashboard URL\n        run: |\n          for i in {1..10}; do\n            curl -sSf http://localhost:8088 && break\n            docker logs playground\n            sleep 10\n          done\n\n  golangci:\n    name: lint\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions/setup-go@v5\n        with:\n          go-version-file: ./go.mod\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@v9\n        with:\n          args: --timeout 20m\n"
  },
  {
    "path": ".github/workflows/dockerhub-description.yml",
    "content": "name: update\n\non:\n  push:\n    branches:    \n      - master\n    paths:\n      - 'README.md'\n\njobs:\n  dockerhub_description:\n    name: dockerHub description\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        image: [gorse-master, gorse-server, gorse-worker, gorse-in-one]\n    steps:\n      - uses: actions/checkout@v5\n\n      - name: Resolve Images on Description\n        run: |\n          sed -i -E \"s/src=\\\"assets\\//src=\\\"https:\\/\\/github.com\\/gorse-io\\/gorse\\/raw\\/master\\/assets\\//\" README.md\n          \n      - name: Update DockerHub Description\n        uses: peter-evans/dockerhub-description@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n          repository: zhenghaoz/${{ matrix.image }}\n"
  },
  {
    "path": ".github/workflows/translate_issues.yml",
    "content": "name: 'translate issues'\non: \n  issue_comment: \n    types: [created]\n  issues: \n    types: [opened]\n\njobs:\n  translate-isssues:\n    name: translate issues\n    runs-on: ubuntu-latest\n    steps:\n      - name: Issues Translator\n        uses: tomsun28/issues-translate-action@v2.5\n"
  },
  {
    "path": ".gitignore",
    "content": "\n# Created by https://www.gitignore.io/api/go,windows,jetbrains\n\n### Go ###\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, build with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n### Go Patch ###\n/vendor/\n/Godeps/\n\n### JetBrains ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn.  Uncomment if using\n# auto-import.\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n### JetBrains Patch ###\n# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721\n\n# *.iml\n# modules.xml\n# .idea/misc.xml\n# *.ipr\n\n# Sonarlint plugin\n.idea/sonarlint\n\n### Windows ###\n# Windows thumbnail cache files\nThumbs.db\nehthumbs.db\nehthumbs_vista.db\n\n# Dump file\n*.stackdump\n\n# Folder config file\n[Dd]esktop.ini\n\n# Recycle Bin used on file shares\n$RECYCLE.BIN/\n\n# Windows Installer files\n*.cab\n*.msi\n*.msix\n*.msm\n*.msp\n\n# Windows shortcuts\n*.lnk\n\n.vscode\n\n# End of https://www.gitignore.io/api/go,windows,jetbrains"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\nlinters:\n  settings:\n    govet:\n      disable:\n        - composites\n    staticcheck:\n      checks:\n        - all\n        - -QF1004\n        - -QF1008\n        - -SA1019\n        - -ST1003\n  exclusions:\n    presets:\n      - comments\n      - common-false-positives\n      - legacy\n      - std-error-handling\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\ncoc@gorse.io.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contribution Guide\n\nWelcome and thank you for considering contributing to Gorse!\n\nReading and following these guidelines will help us make the contribution process easy and effective for everyone involved. It also communicates that you agree to respect the time of the developers managing and developing these open source projects. In return, we will reciprocate that respect by addressing your issue, assessing changes, and helping you finalize your pull requests.\n\n* [Getting Started](#getting-started)\n  * [Setup Develop Environment](#setup-develop-environment)\n  * [Option 1: Run an All-in-one Node](#option-1-run-an-all-in-one-node)\n  * [Option 2: Run Nodes](#option-2-run-nodes)\n  * [Run Unit Tests](#run-unit-tests)\n* [Your First Contribution](#your-first-contribution)\n  * [Contribution Workflow](#contribution-workflow)\n* [Getting Help](#getting-help)\n\n## Getting Started\n\n### Setup Develop Environment\n\nThese following installations are required:\n\n- **Go** (>= 1.18): Since Go features from 1.18 are used in Gorse, the version of the compiler must be greater than 1.18. GoLand or Visual Studio Code is highly recommended as the IDE to develop Gorse.\n\n- **Docker Compose**: Multiple databases are required for unit tests. It's convenient to manage databases on Docker Compose.\n\n```bash\ncd storage\n\ndocker compose up -d\n```\n\nIf you need import sample data, download the SQL file github.sql and import to the MySQL instance.\n\n```bash\n# Download sample data.\nwget https://cdn.gorse.io/example/github.sql\n\n# Import sample data.\nmysql -h 127.0.0.1 -u gorse -pgorse_pass gorse < github.sql\n```\n\n### Option 1: Run an All-in-one Node\n\n```bash\ngo run cmd/gorse-in-one/main.go --config config/config.toml\n```\n\n### Option 2: Run Nodes\n\n- Start the master node with the configuration file.\n\n```bash\ngo run cmd/gorse-master/main.go --config config/config.toml\n```\n\n- Start the worker node.\n\n```bash\ngo run cmd/gorse-worker/main.go\n```\n\n- Start the server node.\n\n```bash\ngo run cmd/gorse-server/main.go\n```\n\n### Run Unit Tests\n\nMost logics in Gorse are covered by unit tests. Run unit tests by the following command:\n\n```bash\ngo test -v ./...\n```\n\nThe default database URLs are directed to these databases in `storage/docker-compose.yml`. Test databases could be overrode by setting following environment variables:\n\n| Environment Value | Default Value                                |\n|-------------------|----------------------------------------------|\n| `MYSQL_URI`       | `mysql://root:password@tcp(127.0.0.1:3306)/` |\n| `POSTGRES_URI`    | `postgres://gorse:gorse_pass@127.0.0.1/`     |\n| `MONGO_URI`       | `mongodb://root:password@127.0.0.1:27017/`   |\n| `CLICKHOUSE_URI`  | `clickhouse://127.0.0.1:8123/`               |\n| `REDIS_URI`       | `redis://127.0.0.1:6379/`                    |\n\nFor example, use TiDB as a test database by:\n\n```bash\nMYSQL_URI=mysql://root:password@tcp(127.0.0.1:4000)/ go test -v ./...\n```\n\n## Your First Contribution\n\nYou can start by finding an existing issue with the [help wanted](https://github.com/gorse-io/gorse/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) label in the Gorse repository. These issues are well suited for new contributors. Issues can be claimed by publishing an `/assign` comment.\n\n### Contribution Workflow\n\nTo contribute to the Gorse code base, please follow the workflow as defined in this section.\n\n- Fork the repository to your own Github account\n- Make commits and add test case if the change fixes a bug or adds new functionality.\n- Run tests and make sure all the tests are passed.\n- Push your changes to a topic branch in your fork of the repository.\n- Submit a pull request.\n\nThis is a rough outline of what a contributor's workflow looks like. Thanks for your contributions!\n\n## Getting Help\n\nJoin us in the [Discord](https://discord.gg/x6gAtNNkAE) and post your question in the `#developers` channel.\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "README.md",
    "content": "# Gorse Open-source Recommender System Engine\n\n<img width=160 src=\"assets/gorse.png\"/>\n\n![](https://img.shields.io/github/go-mod/go-version/zhenghaoz/gorse)\n[![test](https://github.com/gorse-io/gorse/actions/workflows/build_test.yml/badge.svg)](https://github.com/gorse-io/gorse/actions/workflows/build_test.yml)\n[![codecov](https://codecov.io/gh/gorse-io/gorse/branch/master/graph/badge.svg)](https://codecov.io/gh/gorse-io/gorse)\n[![Discord](https://img.shields.io/discord/830635934210588743)](https://discord.gg/x6gAtNNkAE)\n[![Twitter Follow](https://img.shields.io/twitter/follow/gorse_io?label=Follow&style=social)](https://twitter.com/gorse_io)\n[![Gurubase](https://img.shields.io/badge/Gurubase-Ask%20Gorse%20Guru-006BFF)](https://gurubase.io/g/gorse)\n\nGorse is an AI powered open-source recommender system written in Go. Gorse aims to be a universal open-source recommender system that can be quickly integrated into a wide variety of online services. By importing items, users, and interaction data into Gorse, the system will automatically train models to generate recommendations for each user. Project features are as follows.\n\n![](https://github.com/gorse-io/docs/blob/main/src/img/dashboard/recflow.png?raw=true)\n\n- **Multi-source:** Recommend items from latest, user-to-user, item-to-item, collaborative filtering and etc.\n- **Multimodal:** Support multimodal content (text, image, videos, etc.) via embedding.\n- **AI-powered:** Support both classical recommenders and LLM-based recommenders.\n- **GUI Dashboard:** Provide GUI dashboard for recommendation pipeline editing, system monitoring, and data management.\n- **RESTful APIs:** Expose RESTful APIs for data CRUD and recommendation requests.\n\n## Quick Start\n\nThe playground mode has been prepared for beginners. Just set up a recommender system for GitHub repositories by the following commands.\n\n```bash\ndocker run -p 8088:8088 zhenghaoz/gorse-in-one --playground\n```\n\nThe playground mode will download data from [GitRec](https://gitrec.gorse.io/) and import it into Gorse. The dashboard is available at `http://localhost:8088`.\n\n![](https://github.com/gorse-io/docs/blob/main/src/img/dashboard/overview.png?raw=true)\n\nAfter the \"Generate item-to-item recommendation\" task is completed on the \"Tasks\" page, try to insert several feedbacks into Gorse. Suppose Bob is a developer who interested in LLM related repositories. We insert his star feedback to Gorse.\n\n```bash\nread -d '' JSON << EOF\n[\n    { \\\"FeedbackType\\\": \\\"star\\\", \\\"UserId\\\": \\\"bob\\\", \\\"ItemId\\\": \\\"ollama:ollama\\\", \\\"Value\\\": 1.0, \\\"Timestamp\\\": \\\"2022-02-24\\\" },\n    { \\\"FeedbackType\\\": \\\"star\\\", \\\"UserId\\\": \\\"bob\\\", \\\"ItemId\\\": \\\"huggingface:transformers\\\", \\\"Value\\\": 1.0, \\\"Timestamp\\\": \\\"2022-02-25\\\" },\n    { \\\"FeedbackType\\\": \\\"star\\\", \\\"UserId\\\": \\\"bob\\\", \\\"ItemId\\\": \\\"rasbt:llms-from-scratch\\\", \\\"Value\\\": 1.0, \\\"Timestamp\\\": \\\"2022-02-26\\\" },\n    { \\\"FeedbackType\\\": \\\"star\\\", \\\"UserId\\\": \\\"bob\\\", \\\"ItemId\\\": \\\"vllm-project:vllm\\\", \\\"Value\\\": 1.0, \\\"Timestamp\\\": \\\"2022-02-27\\\" },\n    { \\\"FeedbackType\\\": \\\"star\\\", \\\"UserId\\\": \\\"bob\\\", \\\"ItemId\\\": \\\"hiyouga:llama-factory\\\", \\\"Value\\\": 1.0, \\\"Timestamp\\\": \\\"2022-02-28\\\" }\n]\nEOF\n\ncurl -X POST http://127.0.0.1:8088/api/feedback \\\n   -H 'Content-Type: application/json' \\\n   -d \"$JSON\"\n```\n\nThen, fetch 10 recommended items from Gorse. We can find that LLM-related repositories are recommended for Bob.\n\n```bash\ncurl http://127.0.0.1:8088/api/recommend/bob?n=10\n```\n\nFor more information：\n\n- Read [official documents](https://gorse.io/docs/)\n- Visit [playground](https://play.gorse.io/) of Gorse dashboard\n- Explore [live demo](https://gitrec.gorse.io/), a recommender system for GitHub repositories\n- Discuss on [Discord](https://discord.gg/x6gAtNNkAE) or [GitHub Discussion](https://github.com/gorse-io/gorse/discussions)\n\n## Architecture\n\nGorse is a single-node training and distributed prediction recommender system. Gorse stores data in MySQL, MongoDB, Postgres, or ClickHouse, with intermediate results cached in Redis, MySQL, MongoDB and Postgres.\n\n1. The cluster consists of a master node, multiple worker nodes, and server nodes.\n1. The master node is responsible for model training, non-personalized recommendation, configuration management, and membership management.\n1. The server node is responsible for exposing the RESTful APIs and online real-time recommendations.\n1. Worker nodes are responsible for offline recommendations for each user.\n\nIn addition, the administrator can perform system monitoring, data import and export, and system status checking via the dashboard on the master node.\n\n<img width=520 src=\"https://github.com/gorse-io/docs/blob/main/src/img/cluster.drawio.svg?raw=true\"/>\n\n## Contributors\n\n<a href=\"https://github.com/gorse-io/gorse/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=zhenghaoz/gorse\" />\n</a>\n\nAny contribution is appreciated: report a bug, give advice or create a pull request. Read [CONTRIBUTING.md](CONTRIBUTING.md) for more information.\n\n## Acknowledgments\n\n`gorse` is inspired by the following projects:\n\n- [Guibing Guo's librec](https://github.com/guoguibing/librec)\n- [Nicolas Hug's Surprise](https://github.com/NicolasHug/Surprise)\n- [Golang Samples's gopher-vector](https://github.com/golang-samples/gopher-vector)\n"
  },
  {
    "path": "client/README.md",
    "content": "Go SDK has been moved to https://github.com/gorse-io/gorse-go\n"
  },
  {
    "path": "client/client_test.go",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage client\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\tclient \"github.com/gorse-io/gorse-go\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\nvar (\n\tserverEndpoint    string\n\tdashboardEndpoint string\n)\n\nfunc init() {\n\tserverEndpoint = os.Getenv(\"GORSE_SERVER_ENDPOINT\")\n\tdashboardEndpoint = os.Getenv(\"GORSE_DASHBOARD_ENDPOINT\")\n}\n\ntype GorseClientTestSuite struct {\n\tsuite.Suite\n\tclient *client.GorseClient\n}\n\nfunc (suite *GorseClientTestSuite) SetupSuite() {\n\tif serverEndpoint == \"\" || dashboardEndpoint == \"\" {\n\t\tsuite.T().Skip(\"GORSE_SERVER_ENDPOINT or GORSE_DASHBOARD_ENDPOINT is not set\")\n\t}\n\tsuite.client = client.NewGorseClient(serverEndpoint, \"\")\n}\n\nfunc (suite *GorseClientTestSuite) TestUsers() {\n\tctx := suite.T().Context()\n\n\tcursor, err := suite.client.GetUsers(ctx, 3, \"\")\n\tsuite.NoError(err)\n\tsuite.NotEmpty(cursor.Cursor)\n\tif suite.Len(cursor.Users, 3) {\n\t\tsuite.Equal(client.User{\n\t\t\tUserId: \"1\",\n\t\t\tLabels: map[string]any{\n\t\t\t\t\"age\":        float64(24),\n\t\t\t\t\"gender\":     \"M\",\n\t\t\t\t\"occupation\": \"technician\",\n\t\t\t\t\"zip_code\":   \"85711\",\n\t\t\t},\n\t\t}, cursor.Users[0])\n\t\tsuite.Equal(client.User{\n\t\t\tUserId: \"10\",\n\t\t\tLabels: map[string]any{\n\t\t\t\t\"age\":        float64(53),\n\t\t\t\t\"gender\":     \"M\",\n\t\t\t\t\"occupation\": \"lawyer\",\n\t\t\t\t\"zip_code\":   \"90703\",\n\t\t\t},\n\t\t}, cursor.Users[1])\n\t\tsuite.Equal(client.User{\n\t\t\tUserId: \"100\",\n\t\t\tLabels: map[string]any{\n\t\t\t\t\"age\":        float64(36),\n\t\t\t\t\"gender\":     \"M\",\n\t\t\t\t\"occupation\": \"executive\",\n\t\t\t\t\"zip_code\":   \"90254\",\n\t\t\t},\n\t\t}, cursor.Users[2])\n\t}\n\n\tuser := client.User{\n\t\tUserId:  \"1000\",\n\t\tLabels:  map[string]any{\"gender\": \"M\", \"occupation\": \"engineer\"},\n\t\tComment: \"zhenghaoz\",\n\t}\n\trowAffected, err := suite.client.InsertUser(ctx, user)\n\tsuite.NoError(err)\n\tsuite.Equal(1, rowAffected.RowAffected)\n\tresp, err := suite.client.GetUser(ctx, \"1000\")\n\tsuite.NoError(err)\n\tsuite.Equal(user, resp)\n\n\tpatch := client.UserPatch{\n\t\tComment: new(\"hongmi\"),\n\t}\n\trowAffected, err = suite.client.UpdateUser(ctx, user.UserId, patch)\n\tsuite.NoError(err)\n\tsuite.Equal(1, rowAffected.RowAffected)\n\tresp, err = suite.client.GetUser(ctx, \"1000\")\n\tsuite.NoError(err)\n\tsuite.Equal(\"hongmi\", resp.Comment)\n\n\tdeleteAffect, err := suite.client.DeleteUser(ctx, \"1000\")\n\tsuite.NoError(err)\n\tsuite.Equal(1, deleteAffect.RowAffected)\n\t_, err = suite.client.GetUser(ctx, \"1000\")\n\tsuite.Equal(\"1000: user not found\", err.Error())\n}\n\nfunc (suite *GorseClientTestSuite) TestItems() {\n\tctx := suite.T().Context()\n\titems, err := suite.client.GetItems(ctx, 3, \"\")\n\tsuite.NoError(err)\n\tsuite.NotEmpty(items.Cursor)\n\tif suite.Len(items.Items, 3) {\n\t\tsuite.Equal(\"1\", items.Items[0].ItemId)\n\t\tsuite.Equal([]string{\"Animation\", \"Children's\", \"Comedy\"}, items.Items[0].Categories)\n\t\tsuite.Equal(time.Date(1995, 1, 1, 0, 0, 0, 0, time.UTC), items.Items[0].Timestamp)\n\t\tsuite.Equal(\"Toy Story (1995)\", items.Items[0].Comment)\n\t\tsuite.Equal(\"10\", items.Items[1].ItemId)\n\t\tsuite.Equal([]string{\"Drama\", \"War\"}, items.Items[1].Categories)\n\t\tsuite.Equal(time.Date(1996, 1, 22, 0, 0, 0, 0, time.UTC), items.Items[1].Timestamp)\n\t\tsuite.Equal(\"Richard III (1995)\", items.Items[1].Comment)\n\t\tsuite.Equal(\"100\", items.Items[2].ItemId)\n\t\tsuite.Equal([]string{\"Crime\", \"Drama\", \"Thriller\"}, items.Items[2].Categories)\n\t\tsuite.Equal(time.Date(1997, 2, 14, 0, 0, 0, 0, time.UTC), items.Items[2].Timestamp)\n\t\tsuite.Equal(\"Fargo (1996)\", items.Items[2].Comment)\n\t}\n\n\titem := client.Item{\n\t\tItemId:   \"2000\",\n\t\tIsHidden: true,\n\t\tLabels: map[string]any{\n\t\t\t\"embedding\": []any{0.1, 0.2, 0.3},\n\t\t},\n\t\tCategories: []string{\"Comedy\", \"Animation\"},\n\t\tTimestamp:  time.Now().UTC().Truncate(time.Second),\n\t\tComment:    \"Minions (2015)\",\n\t}\n\trowAffected, err := suite.client.InsertItem(ctx, item)\n\tsuite.NoError(err)\n\tsuite.Equal(1, rowAffected.RowAffected)\n\tresp, err := suite.client.GetItem(ctx, \"2000\")\n\tsuite.NoError(err)\n\tsuite.Equal(item, resp)\n\n\tpatch := client.ItemPatch{\n\t\tComment: new(\"小黄人 (2015)\"),\n\t}\n\trowAffected, err = suite.client.UpdateItem(ctx, item.ItemId, patch)\n\tsuite.NoError(err)\n\tsuite.Equal(1, rowAffected.RowAffected)\n\tresp, err = suite.client.GetItem(ctx, \"2000\")\n\tsuite.NoError(err)\n\tsuite.Equal(\"小黄人 (2015)\", resp.Comment)\n\n\tdeleteAffect, err := suite.client.DeleteItem(ctx, \"2000\")\n\tsuite.NoError(err)\n\tsuite.Equal(1, deleteAffect.RowAffected)\n\t_, err = suite.client.GetItem(ctx, \"2000\")\n\tsuite.Equal(\"2000: item not found\", err.Error())\n}\n\nfunc (suite *GorseClientTestSuite) TestFeedback() {\n\tctx := suite.T().Context()\n\t_, err := suite.client.InsertUser(ctx, client.User{UserId: \"2000\"})\n\tsuite.NoError(err)\n\n\tfeedback := []client.Feedback{\n\t\t{\n\t\t\tFeedbackType: \"watch\",\n\t\t\tUserId:       \"2000\",\n\t\t\tItemId:       \"1\",\n\t\t\tValue:        1.0,\n\t\t\tTimestamp:    time.Now().UTC().Truncate(time.Second),\n\t\t},\n\t\t{\n\t\t\tFeedbackType: \"watch\",\n\t\t\tUserId:       \"2000\",\n\t\t\tItemId:       \"1060\",\n\t\t\tValue:        2.0,\n\t\t\tTimestamp:    time.Now().UTC().Truncate(time.Second),\n\t\t},\n\t\t{\n\t\t\tFeedbackType: \"watch\",\n\t\t\tUserId:       \"2000\",\n\t\t\tItemId:       \"11\",\n\t\t\tValue:        3.0,\n\t\t\tTimestamp:    time.Now().UTC().Truncate(time.Second),\n\t\t},\n\t}\n\tfor _, fb := range feedback {\n\t\t_, err := suite.client.DeleteFeedbacks(ctx, fb.UserId, fb.ItemId)\n\t\tsuite.NoError(err)\n\t}\n\t_, err = suite.client.InsertFeedback(ctx, feedback)\n\tsuite.NoError(err)\n\n\tuserFeedback, err := suite.client.ListFeedbacks(ctx, \"watch\", \"2000\")\n\tsuite.NoError(err)\n\tsuite.Equal(feedback, userFeedback)\n\n\t_, err = suite.client.DeleteFeedback(ctx, \"watch\", \"2000\", \"1\")\n\tsuite.NoError(err)\n\tuserFeedback, err = suite.client.ListFeedbacks(ctx, \"watch\", \"2000\")\n\tsuite.NoError(err)\n\tsuite.Equal([]client.Feedback{feedback[1], feedback[2]}, userFeedback)\n}\n\nfunc (suite *GorseClientTestSuite) TestLatest() {\n\tctx := suite.T().Context()\n\titems, err := suite.client.GetLatestItems(ctx, \"\", \"\", 3, 0)\n\tsuite.NoError(err)\n\tif suite.Len(items, 3) {\n\t\tsuite.Equal(\"315\", items[0].Id)\n\t\tsuite.Equal(\"1432\", items[1].Id)\n\t\tsuite.Equal(\"918\", items[2].Id)\n\t}\n}\n\nfunc (suite *GorseClientTestSuite) TestItemToItem() {\n\tctx := suite.T().Context()\n\tneighbors, err := suite.client.GetNeighbors(ctx, \"1\", 3)\n\tsuite.NoError(err)\n\tif suite.Len(neighbors, 3) {\n\t\tsuite.Equal(\"1060\", neighbors[0].Id)\n\t\tsuite.Equal(\"404\", neighbors[1].Id)\n\t\tsuite.Equal(\"1219\", neighbors[2].Id)\n\t}\n}\n\nfunc (suite *GorseClientTestSuite) TestRecommend() {\n\tctx := suite.T().Context()\n\t_, err := suite.client.InsertUser(ctx, client.User{UserId: \"3000\"})\n\tsuite.NoError(err)\n\trecommendations, err := suite.client.GetRecommend(ctx, \"3000\", \"\", 3, 0)\n\tsuite.NoError(err)\n\tsuite.Len(recommendations, 3)\n\tif suite.Len(recommendations, 3) {\n\t\tsuite.Equal(\"315\", recommendations[0])\n\t\tsuite.Equal(\"1432\", recommendations[1])\n\t\tsuite.Equal(\"918\", recommendations[2])\n\t}\n}\n\nfunc TestGorseClientTestSuite(t *testing.T) {\n\tsuite.Run(t, new(GorseClientTestSuite))\n}\n"
  },
  {
    "path": "client/config.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage client\n\nimport _ \"embed\"\n\n//go:embed config.toml\nvar ConfigTOML string\n"
  },
  {
    "path": "client/config.toml",
    "content": "[master]\n\n# GRPC port of the master node. The default value is 8086.\nport = 8086\n\n# gRPC host of the master node. The default values is \"0.0.0.0\".\nhost = \"0.0.0.0\"\n\n# Enable SSL for the gRPC communication. The default value is false.\nssl_mode = false\n\n# SSL certification authority for the gRPC communication.\nssl_ca = \"\"\n\n# SSL certification for the gRPC communication.\nssl_cert = \"\"\n\n# SSL certification key for the gRPC communication.\nssl_key = \"\"\n\n# HTTP port of the master node. The default values is 8088.\nhttp_port = 8088\n\n# HTTP host of the master node. The default values is \"0.0.0.0\".\nhttp_host = \"0.0.0.0\"\n\n# AllowedDomains is a list of allowed values for Http Origin.\n# The list may contain the special wildcard string \".*\" ; all is allowed\n# If empty all are allowed.\nhttp_cors_domains = []\n\n# AllowedMethods is either empty or has a list of http methods names. Checking is case-insensitive.\nhttp_cors_methods = []\n\n# Number of working jobs in the master node. The default value is 1.\nn_jobs = 1\n\n# Meta information timeout. The default value is 10s.\nmeta_timeout = \"10s\"\n\n# Username for the master node dashboard.\ndashboard_user_name = \"\"\n\n# Password for the master node dashboard.\ndashboard_password = \"\"\n\n# Secret key for admin APIs (SSL required).\nadmin_api_key = \"\"\n\n[server]\n\n# Default number of returned items. The default value is 10.\ndefault_n = 10\n\n# Secret key for RESTful APIs (SSL required).\napi_key = \"\"\n\n# Clock error in the cluster. The default value is 5s.\nclock_error = \"5s\"\n\n# Insert new users while inserting feedback. The default value is true.\nauto_insert_user = true\n\n# Insert new items while inserting feedback. The default value is true.\nauto_insert_item = true\n\n# Server-side cache expire time. The default value is 10s.\ncache_expire = \"10s\"\n\n[recommend]\n\n# The cache size for recommended/popular/latest items. The default value is 10.\ncache_size = 100\n\n# Recommended cache expire time. The default value is 72h.\ncache_expire = \"72h\"\n\n# The context size for online recommendations. Online recommendations can't use all user feedbacks to generate\n# recommendations for efficiency consideration. Instead, only the latest `context_size` feedbacks are used.\n# The default value is 100.\ncontext_size = 100\n\n# The time-to-live (days) of active users, 0 means disabled. Recommendation won't be cached for inactive users. The default value is 0.\nactive_user_ttl = 0\n\n[recommend.data_source]\n\n# The feedback types for positive events.\npositive_feedback_types = [\"rating>=4\"]\n\n# The feedback types for read events.\nread_feedback_types = [\"rating\"]\n\n# The time-to-live (days) of positive feedback, 0 means disabled. The default value is 0.\npositive_feedback_ttl = 0\n\n# The time-to-live (days) of items, 0 means disabled. The default value is 0.\nitem_ttl = 0\n\n[[recommend.non-personalized]]\n\n# The name of the leaderboard.\nname = \"popular\"\n\n# The score function for items in the leaderboard.\nscore = \"len(feedback)\"\n\n[[recommend.item-to-item]]\n\n# The name of the item-to-item recommender.\nname = \"neighbors\"\n\n# The type of the item-to-item recommender. There are three types:\n#   embedding: recommend by Euclidean distance of embeddings.\n#   tags: recommend by number of common tags.\n#   users: recommend by number of common users.\n#   chat: recommend by chat completion model.\ntype = \"embedding\"\n\n# The column of the item embeddings. Leave blank if type is \"users\".\ncolumn = \"item.Labels.embedding\"\n\n[[recommend.user-to-user]]\n\n# The name of the user-to-user recommender.\nname = \"neighbors\"\n\n# The type of the user-to-user recommender. There are three types:\n#   embedding: recommend by Euclidean distance of embeddings.\n#   tags: recommend by number of common tags.\n#   items: recommend by number of common items.\ntype = \"items\"\n\n[recommend.collaborative]\n\n# The type of collaborative filtering. Supported values:\n#   none: disable collaborative filtering.\n#   mf: matrix factorization.\ntype = \"mf\"\n\n# The time period for model fitting. The default value is \"60m\".\nfit_period = \"60m\"\n\n# The number of epochs for model fitting. The default value is 100.\nfit_epoch = 100\n\n[recommend.collaborative.early_stopping]\n\n# Number of epochs to wait if no improvement and then stop the training. The default value is 10.\npatience = 10\n\n[recommend.ranker]\n\n# The type of the ranker. There are two types:\n#   none: no ranking.\n#   fm: factorization machines.\ntype = \"none\"\n\n# The recommenders used to fetch candidate items before ranking. The default values is all recommenders.\nrecommenders = [\"latest\"]\n"
  },
  {
    "path": "client/docker-compose.yml.j2",
    "content": "version: \"3\"\nservices:\n\n  {% if database == 'mysql' %}\n\n  mysql:\n    image: mysql/mysql-server\n    restart: unless-stopped\n    ports:\n      - 3306:3306\n    environment:\n      MYSQL_ROOT_PASSWORD: root_pass\n      MYSQL_DATABASE: gorse\n      MYSQL_USER: gorse\n      MYSQL_PASSWORD: gorse_pass\n    healthcheck:\n      test: mysqladmin ping\n      interval: 10s\n      timeout: 5s\n      retries: 5\n\n  {% elif database == 'postgres' %}\n\n  postgres:\n    image: postgres:10.0\n    ports:\n      - 5432:5432\n    environment:\n      POSTGRES_DB: gorse\n      POSTGRES_USER: gorse\n      POSTGRES_PASSWORD: gorse_pass\n    healthcheck:\n      test: pg_isready\n      interval: 10s\n      timeout: 5s\n      retries: 5\n\n  {% elif database == 'mongo' %}\n\n  mongo:\n    image: mongo:4.0\n    ports:\n      - 27017:27017\n    environment:\n      MONGO_INITDB_DATABASE: gorse\n      MONGO_INITDB_ROOT_USERNAME: root\n      MONGO_INITDB_ROOT_PASSWORD: password\n    healthcheck:\n      test: mongo\n      interval: 10s\n      timeout: 5s\n      retries: 5\n\n  {% elif database == 'clickhouse+redis' %}\n\n  clickhouse:\n    image: clickhouse/clickhouse-server:23\n    ports:\n      - 8123:8123\n    environment:\n      CLICKHOUSE_DB: gorse\n      CLICKHOUSE_USER: gorse\n      CLICKHOUSE_PASSWORD: gorse_pass\n    healthcheck:\n      test: clickhouse-client --user $$CLICKHOUSE_USER --password $$CLICKHOUSE_PASSWORD --query \"SELECT 1\"\n      interval: 10s\n      timeout: 5s\n      retries: 5\n\n  redis:\n    image: redis/redis-stack:6.2.6-v9\n    restart: unless-stopped\n    ports:\n      - 6379:6379\n    healthcheck:\n      test: redis-cli ping\n      interval: 10s\n      timeout: 5s\n      retries: 5\n\n  {% endif %}\n\n  worker:\n    build:\n      context: .\n      dockerfile: cmd/gorse-worker/Dockerfile\n      cache_from:\n        - type=gha\n      cache_to:\n        - type=gha,mode=max\n    restart: unless-stopped\n    ports:\n      - 8089:8089\n    command: >\n      --master-host master --master-port 8086 \n      --http-host 0.0.0.0 --http-port 8089\n      --log-path /var/log/gorse/worker.log \n      --cache-path /var/lib/gorse/worker_cache.data\n    depends_on:\n      - master\n\n  server:\n    build:\n      context: .\n      dockerfile: cmd/gorse-server/Dockerfile\n      cache_from:\n        - type=gha\n      cache_to:\n        - type=gha,mode=max\n    restart: unless-stopped\n    ports:\n      - 8087:8087\n    command: >\n      --master-host master --master-port 8086 \n      --http-host 0.0.0.0 --http-port 8087\n      --log-path /var/log/gorse/server.log \n      --cache-path /var/lib/gorse/server_cache.data\n    depends_on:\n      - master\n\n  master:\n    build:\n      context: .\n      dockerfile: cmd/gorse-master/Dockerfile\n      cache_from:\n        - type=gha\n      cache_to:\n        - type=gha,mode=max\n    restart: unless-stopped\n    ports:\n      - 8086:8086\n      - 8088:8088\n    environment:\n      {% if database == 'mysql' %}\n      GORSE_DATA_STORE: mysql://gorse:gorse_pass@tcp(mysql:3306)/gorse\n      GORSE_CACHE_STORE: mysql://gorse:gorse_pass@tcp(mysql:3306)/gorse\n      {% elif database == 'postgres' %}\n      GORSE_DATA_STORE: postgres://gorse:gorse_pass@postgres/gorse?sslmode=disable\n      GORSE_CACHE_STORE: postgres://gorse:gorse_pass@postgres/gorse?sslmode=disable\n      {% elif database == 'mongo' %}\n      GORSE_DATA_STORE: mongodb://root:password@mongo:27017/gorse?authSource=admin&connect=direct\n      GORSE_CACHE_STORE: mongodb://root:password@mongo:27017/gorse?authSource=admin&connect=direct\n      {% elif database == 'clickhouse+redis' %}\n      GORSE_DATA_STORE: clickhouse://gorse:gorse_pass@clickhouse:8123/gorse?mutations_sync=2\n      GORSE_CACHE_STORE: redis://redis:6379\n      {% elif database == 'sqlite' %}\n      GORSE_DATA_STORE: sqlite:///var/lib/gorse/data.sqlite3\n      GORSE_CACHE_STORE: sqlite:///var/lib/gorse/cache.sqlite3\n      {% endif %}\n    command: >\n      -c /etc/gorse/config.toml \n      --log-path /var/log/gorse/master.log \n      --cache-path /var/lib/gorse\n    volumes:\n      - ./client/config.toml:/etc/gorse/config.toml\n    {% if database != 'sqlite' %}\n    depends_on:\n      {% if database == 'mysql' %}\n      mysql:\n        condition: service_healthy\n      {% elif database == 'postgres' %}\n      postgres:\n        condition: service_healthy\n      {% elif database == 'mongo' %}\n      mongo:\n        condition: service_healthy\n      {% elif database == 'clickhouse+redis' %}\n      clickhouse:\n        condition: service_healthy\n      redis:\n        condition: service_healthy\n      {% endif %}\n    {% endif %}\n"
  },
  {
    "path": "client/setup-test.sh",
    "content": "#!/bin/bash\nset -e\n\n# Download config\nif [ ! -f ./config.toml ]; then\n    wget https://github.com/gorse-io/gorse/raw/refs/heads/master/client/config.toml\nfi\n\n# Create docker-compose.yml\nif [ ! -f ./docker-compose.yml ]; then\n    cat > docker-compose.yml <<EOF\nversion: '3'\nservices:\n    master:\n        image: zhenghaoz/gorse-in-one:latest\n        ports:\n            - 8088:8088\n        volumes:\n            - ./config.toml:/etc/gorse/config.toml\n        environment:\n            - GORSE_DATA_STORE=sqlite:///var/lib/gorse/data.sqlite3\n            - GORSE_CACHE_STORE=sqlite:///var/lib/gorse/cache.sqlite3\n        command: [\"-c\", \"/etc/gorse/config.toml\", \"--cache-path\", \"/var/lib/gorse\"]\nEOF\nfi\n\n# Start Gorse\ndocker compose up -d\n\n# Download MovieLens 100k dataset\nif [ ! -f ./ml-100k.bin ]; then\n    wget https://cdn.gorse.io/example/ml-100k.bin.gz\n    gzip -d ml-100k.bin.gz\nfi\n\n# Wait for Gorse to be ready\nmax_attempts=6\nattempt=0\nuntil curl -s -o /dev/null -w \"%{http_code}\" \"http://127.0.0.1:8088/api/health/ready\" | grep -q \"200\" || [ $attempt -ge $max_attempts ]; do\n    echo \"Waiting for Gorse to be ready... (attempt $((attempt+1))/$max_attempts)\"\n    sleep 5\n    attempt=$((attempt+1))\ndone\nif [ $attempt -ge $max_attempts ]; then\n    echo \"Warning: Maximum attempts reached. Gorse may not be ready.\"\nfi\n\n# Import MovieLens 100k dataset\ncurl -X POST \"http://localhost:8088/api/restore\" --data-binary \"@./ml-100k.bin\"\n\n# Restart Gorse to apply changes\ndocker compose restart master\nmax_attempts=18\nattempt=0\nuntil curl -s \"http://localhost:8088/api/dashboard/tasks\" | grep -q \"Train Collaborative Filtering Model\" || [ $attempt -ge $max_attempts ]; do\n    echo \"Waiting for recommendation... (attempt $((attempt+1))/$max_attempts)\"\n    sleep 10\n    attempt=$((attempt+1))\ndone\nif [ $attempt -ge $max_attempts ]; then\n    echo \"Warning: Maximum attempts reached. Recommendation may not be ready.\"\nfi\n"
  },
  {
    "path": "cmd/goat/README.md",
    "content": "GOAT has been moved to https://github.com/gorse-io/goat\n"
  },
  {
    "path": "cmd/gorse-cli/main.go",
    "content": "// Copyright 2026 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage main\n\nimport (\n\t\"context\"\n\t\"encoding/csv\"\n\t\"fmt\"\n\t\"maps\"\n\t\"math\"\n\t\"os\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"sync\"\n\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/expr-lang/expr\"\n\t\"github.com/expr-lang/expr/vm\"\n\t\"github.com/gorse-io/gorse/common/floats\"\n\t\"github.com/gorse-io/gorse/common/heap\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/common/parallel\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/logics\"\n\t\"github.com/gorse-io/gorse/master\"\n\t\"github.com/gorse-io/gorse/model/cf\"\n\t\"github.com/gorse-io/gorse/model/ctr\"\n\t\"github.com/gorse-io/gorse/storage\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/olekukonko/tablewriter\"\n\t\"github.com/samber/lo\"\n\t\"github.com/sashabaranov/go-openai\"\n\t\"github.com/schollz/progressbar/v3\"\n\t\"github.com/spf13/cobra\"\n\t\"go.uber.org/atomic\"\n\t\"go.uber.org/zap\"\n)\n\nvar rootCmd = &cobra.Command{\n\tUse:   \"gorse-cli\",\n\tShort: \"Gorse command line tool\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\t_ = cmd.Help()\n\t},\n}\n\nvar benchLLMCmd = &cobra.Command{\n\tUse:   \"bench-llm\",\n\tShort: \"Benchmark LLM models for ranking\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\t// Load configuration\n\t\tconfigPath, _ := cmd.Flags().GetString(\"config\")\n\t\tcfg, err := config.LoadConfig(configPath)\n\t\tif err != nil {\n\t\t\tlog.Logger().Fatal(\"failed to load config\", zap.Error(err))\n\t\t}\n\n\t\t// Load dataset\n\t\tm := master.NewMaster(cfg, os.TempDir(), false, configPath)\n\t\tm.DataClient, err = data.Open(m.Config.Database.DataStore, m.Config.Database.DataTablePrefix,\n\t\t\tstorage.WithIsolationLevel(m.Config.Database.MySQL.IsolationLevel))\n\t\tif err != nil {\n\t\t\tlog.Logger().Fatal(\"failed to open data client\", zap.Error(err))\n\t\t}\n\t\tevaluator := master.NewOnlineEvaluator(\n\t\t\tm.Config.Recommend.DataSource.PositiveFeedbackTypes,\n\t\t\tm.Config.Recommend.DataSource.ReadFeedbackTypes)\n\t\tdataset, cfDataset, err := m.LoadDataFromDatabase(context.Background(), m.DataClient,\n\t\t\tm.Config.Recommend.DataSource.PositiveFeedbackTypes,\n\t\t\tm.Config.Recommend.DataSource.ReadFeedbackTypes,\n\t\t\tm.Config.Recommend.DataSource.ItemTTL,\n\t\t\tm.Config.Recommend.DataSource.PositiveFeedbackTTL,\n\t\t\tevaluator,\n\t\t\tnil)\n\t\tif err != nil {\n\t\t\tlog.Logger().Fatal(\"failed to load dataset\", zap.Error(err))\n\t\t}\n\n\t\t// Split dataset\n\t\tvar scores sync.Map\n\t\ttrain, test := dataset.Split(0.2, 0)\n\n\t\ttable := tablewriter.NewWriter(os.Stdout)\n\t\ttable.Header([]string{\"\", \"#interactions\", \"#positive\", \"#negative\"})\n\t\tlo.Must0(table.Bulk([][]string{\n\t\t\t{\"total\", strconv.Itoa(dataset.Count()), strconv.Itoa(dataset.PositiveCount), strconv.Itoa(dataset.NegativeCount)},\n\t\t\t{\"train\", strconv.Itoa(train.Count()), strconv.Itoa(train.PositiveCount), strconv.Itoa(train.NegativeCount)},\n\t\t\t{\"test\", strconv.Itoa(test.Count()), strconv.Itoa(test.PositiveCount), strconv.Itoa(test.NegativeCount)},\n\t\t}))\n\t\tlo.Must0(table.Render())\n\n\t\texportUserAUC, _ := cmd.Flags().GetBool(\"user-auc\")\n\t\tgo EvaluateAFM(cfg, train, test, exportUserAUC, &scores)\n\t\tEvaluateLLM(cfg, train, test, cfDataset.GetItems(), exportUserAUC, &scores)\n\t\tdata := [][]string{{\"Ranker\", \"GAUC\"}}\n\t\tscores.Range(func(key, value any) bool {\n\t\t\tdata = append(data, []string{\n\t\t\t\tkey.(string),\n\t\t\t\tstrconv.FormatFloat(value.(float64), 'f', 4, 32),\n\t\t\t})\n\t\t\treturn true\n\t\t})\n\t\ttable = tablewriter.NewWriter(os.Stdout)\n\t\ttable.Header(data[0])\n\t\tlo.Must0(table.Bulk(data[1:]))\n\t\tlo.Must0(table.Render())\n\t},\n}\n\nfunc EvaluateAFM(cfg *config.Config, train, test *ctr.Dataset, exportUserAUC bool, scores *sync.Map) {\n\tml := ctr.NewAFM(nil)\n\tml.Fit(context.Background(), train, test,\n\t\tctr.NewFitConfig().\n\t\t\tSetVerbose(10).\n\t\t\tSetJobs(runtime.NumCPU()).\n\t\t\tSetPatience(10))\n\n\tfeedbackCount := make(map[int32]int)\n\tfor i := 0; i < train.Count(); i++ {\n\t\tindices, _, _, target := train.Get(i)\n\t\tif target > 0 {\n\t\t\tuserIndex := indices[0]\n\t\t\tfeedbackCount[userIndex]++\n\t\t}\n\t}\n\n\tvar features []lo.Tuple2[[]int32, []float32]\n\tvar embeddings [][][]float32\n\tpositives := make(map[int32][]int)\n\tnegatives := make(map[int32][]int)\n\tfor i := 0; i < test.Count(); i++ {\n\t\tindices, values, embedding, target := test.Get(i)\n\t\tfeatures = append(features, lo.Tuple2[[]int32, []float32]{A: indices, B: values})\n\t\tembeddings = append(embeddings, embedding)\n\t\tuserIndex := indices[0]\n\t\tif target > 0 {\n\t\t\tpositives[userIndex] = append(positives[userIndex], i)\n\t\t} else {\n\t\t\tnegatives[userIndex] = append(negatives[userIndex], i)\n\t\t}\n\t}\n\tpredictions := ml.BatchInternalPredict(features, embeddings, runtime.NumCPU())\n\n\tvar csvFile *os.File\n\tvar csvWriter *csv.Writer\n\tif exportUserAUC {\n\t\tvar err error\n\t\tcsvFile, err = os.Create(\"AFM.csv\")\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to create AFM.csv\", zap.Error(err))\n\t\t\texportUserAUC = false\n\t\t} else {\n\t\t\tdefer csvFile.Close()\n\t\t\tcsvWriter = csv.NewWriter(csvFile)\n\t\t\tdefer csvWriter.Flush()\n\t\t\t_ = csvWriter.Write([]string{\"Feedback\", \"Candidates\", \"AUC\"})\n\t\t}\n\t}\n\n\tvar sum float32\n\tvar count float32\n\tfor userIndex, posIndices := range positives {\n\t\tnegIndices := negatives[userIndex]\n\t\tif len(negIndices) == 0 || feedbackCount[userIndex] == 0 || feedbackCount[userIndex] > cfg.Recommend.ContextSize {\n\t\t\tcontinue\n\t\t}\n\t\tvar posPredictions, negPredictions []float32\n\t\tfor _, posIndex := range posIndices {\n\t\t\tposPredictions = append(posPredictions, predictions[posIndex])\n\t\t}\n\t\tfor _, negIndex := range negIndices {\n\t\t\tnegPredictions = append(negPredictions, predictions[negIndex])\n\t\t}\n\t\tuserAUC := ctr.AUC(posPredictions, negPredictions)\n\t\tif exportUserAUC {\n\t\t\t_ = csvWriter.Write([]string{\n\t\t\t\tstrconv.Itoa(feedbackCount[userIndex]),\n\t\t\t\tstrconv.Itoa(len(posIndices) + len(negIndices)),\n\t\t\t\tfmt.Sprintf(\"%.4f\", userAUC),\n\t\t\t})\n\t\t}\n\t\tuserCount := float32(len(posIndices) + len(negIndices))\n\t\tsum += userAUC * userCount\n\t\tcount += userCount\n\t}\n\n\tvar score float64\n\tif count > 0 {\n\t\tscore = float64(sum / count)\n\t}\n\tscores.Store(\"AFM\", score)\n}\n\nfunc EvaluateLLM(cfg *config.Config, train, test *ctr.Dataset, items []data.Item, exportUserAUC bool, scores *sync.Map) {\n\tchat, err := logics.NewChatReranker(\n\t\tcfg.Recommend.Ranker.RerankerAPI,\n\t\tcfg.Recommend.Ranker.QueryTemplate,\n\t\tcfg.Recommend.Ranker.DocumentTemplate,\n\t)\n\tif err != nil {\n\t\tlog.Logger().Fatal(\"failed to create chat ranker\", zap.Error(err))\n\t}\n\n\tfeedbacks := make(map[int32][]*logics.FeedbackItem)\n\tfor i := 0; i < train.Count(); i++ {\n\t\tindices, _, _, target := train.Get(i)\n\t\tif target <= 0 {\n\t\t\tcontinue\n\t\t}\n\t\tuserIndex := indices[0]\n\t\titemIndex := indices[1] - int32(train.CountUsers())\n\t\tfeedbacks[userIndex] = append(feedbacks[userIndex], &logics.FeedbackItem{\n\t\t\tItem: items[itemIndex],\n\t\t})\n\t}\n\n\tpositives := make(map[int32][]int32)\n\tnegatives := make(map[int32][]int32)\n\tfor i := 0; i < test.Count(); i++ {\n\t\tindices, _, _, target := test.Get(i)\n\t\tuserIndex := indices[0]\n\t\titemIndex := indices[1] - int32(test.CountUsers())\n\t\tif target > 0 {\n\t\t\tpositives[userIndex] = append(positives[userIndex], itemIndex)\n\t\t} else {\n\t\t\tnegatives[userIndex] = append(negatives[userIndex], itemIndex)\n\t\t}\n\t}\n\n\tvar csvFile *os.File\n\tvar csvWriter *csv.Writer\n\tvar csvMu sync.Mutex\n\tif exportUserAUC {\n\t\tvar err error\n\t\tcsvFile, err = os.Create(fmt.Sprintf(\"%s.csv\", cfg.Recommend.Ranker.RerankerAPI.Model))\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to create LLM.csv\", zap.Error(err))\n\t\t\texportUserAUC = false\n\t\t} else {\n\t\t\tdefer csvFile.Close()\n\t\t\tcsvWriter = csv.NewWriter(csvFile)\n\t\t\tdefer csvWriter.Flush()\n\t\t\t_ = csvWriter.Write([]string{\"Feedback\", \"Candidates\", \"AUC\"})\n\t\t}\n\t}\n\n\tvar sum atomic.Float32\n\tvar count atomic.Float32\n\tlo.Must0(parallel.ForEach(context.Background(), slices.Collect(maps.Keys(positives)), runtime.NumCPU(), func(_ int, userIndex int32) {\n\t\tposIndices := positives[userIndex]\n\t\tnegIndices := negatives[userIndex]\n\t\tif len(negIndices) == 0 {\n\t\t\treturn\n\t\t}\n\t\tcandidates := make([]*data.Item, 0, len(posIndices)+len(negIndices))\n\t\tpositiveItems := mapset.NewSet[string]()\n\t\tnegativeItems := mapset.NewSet[string]()\n\t\tfor _, negIndex := range negIndices {\n\t\t\titem := items[negIndex]\n\t\t\tcandidates = append(candidates, &item)\n\t\t\tnegativeItems.Add(item.ItemId)\n\t\t}\n\t\tfor _, posIndex := range posIndices {\n\t\t\titem := items[posIndex]\n\t\t\tcandidates = append(candidates, &item)\n\t\t\tpositiveItems.Add(item.ItemId)\n\t\t}\n\t\tfeedback := feedbacks[int32(userIndex)]\n\t\tif len(feedback) == 0 || len(feedback) > cfg.Recommend.ContextSize {\n\t\t\treturn\n\t\t}\n\t\tresult, err := chat.Rank(context.Background(), &data.User{}, feedback, candidates)\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to rank items for user\", zap.Int32(\"user_index\", userIndex), zap.Error(err))\n\t\t\treturn\n\t\t}\n\t\tvar posPredictions, negPredictions []float32\n\t\tfor _, item := range result {\n\t\t\tif positiveItems.Contains(item.Id) {\n\t\t\t\tposPredictions = append(posPredictions, float32(item.Score))\n\t\t\t} else if negativeItems.Contains(item.Id) {\n\t\t\t\tnegPredictions = append(negPredictions, float32(item.Score))\n\t\t\t}\n\t\t}\n\t\tuserAUC := ctr.AUC(posPredictions, negPredictions)\n\t\tif exportUserAUC {\n\t\t\tcsvMu.Lock()\n\t\t\t_ = csvWriter.Write([]string{\n\t\t\t\tstrconv.Itoa(len(feedback)),\n\t\t\t\tstrconv.Itoa(len(posIndices) + len(negIndices)),\n\t\t\t\tfmt.Sprintf(\"%.4f\", userAUC),\n\t\t\t})\n\t\t\tcsvMu.Unlock()\n\t\t}\n\t\tuserCount := float32(len(posIndices) + len(negIndices))\n\t\tsum.Add(userAUC * userCount)\n\t\tcount.Add(userCount)\n\t}))\n\n\tvar score float64\n\tif count.Load() > 0 {\n\t\tscore = float64(sum.Load() / count.Load())\n\t}\n\tscores.Store(cfg.Recommend.Ranker.RerankerAPI.Model, score)\n}\n\nfunc EvaluateEmbedding(cfg *config.Config, train, test dataset.CFSplit, embeddingExpr, textExpr string, topK, jobs int, scores *sync.Map) {\n\t// Compile expression\n\tvar embeddingProgram, textProgram *vm.Program\n\tvar err error\n\tif embeddingExpr != \"\" {\n\t\tembeddingProgram, err = expr.Compile(embeddingExpr, expr.Env(map[string]any{\n\t\t\t\"item\": data.Item{},\n\t\t}))\n\t\tif err != nil {\n\t\t\tlog.Logger().Fatal(\"failed to compile embedding expression\", zap.Error(err))\n\t\t}\n\t} else if textExpr != \"\" {\n\t\ttextProgram, err = expr.Compile(textExpr, expr.Env(map[string]any{\n\t\t\t\"item\": data.Item{},\n\t\t}))\n\t\tif err != nil {\n\t\t\tlog.Logger().Fatal(\"failed to compile text expression\", zap.Error(err))\n\t\t}\n\t} else {\n\t\tlog.Logger().Fatal(\"one of embedding-column or text-column is required\")\n\t}\n\n\t// Extract embeddings\n\tvar dimensions atomic.Int64\n\tembeddings := make([][]float32, test.CountItems())\n\tif textExpr != \"\" {\n\t\tclientConfig := openai.DefaultConfig(cfg.OpenAI.AuthToken)\n\t\tclientConfig.BaseURL = cfg.OpenAI.BaseURL\n\t\tclient := openai.NewClientWithConfig(clientConfig)\n\t\t// Generate embeddings\n\t\tbar := progressbar.Default(int64(test.CountItems()))\n\t\tlo.Must0(parallel.For(context.Background(), test.CountItems(), jobs, func(i int) {\n\t\t\t_ = bar.Add(1)\n\t\t\titem := &test.GetItems()[i]\n\t\t\tresult, err := expr.Run(textProgram, map[string]any{\n\t\t\t\t\"item\": *item,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttext, ok := result.(string)\n\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Generate embedding\n\t\t\tresp, err := client.CreateEmbeddings(context.Background(), openai.EmbeddingRequest{\n\t\t\t\tInput:      text,\n\t\t\t\tModel:      openai.EmbeddingModel(cfg.OpenAI.EmbeddingModel),\n\t\t\t\tDimensions: cfg.OpenAI.EmbeddingDimensions,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tlog.Logger().Error(\"failed to create embeddings\", zap.String(\"item_id\", item.ItemId), zap.Error(err))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tembeddings[i] = resp.Data[0].Embedding\n\t\t\tif dimensions.Load() == 0 {\n\t\t\t\tdimensions.Store(int64(len(resp.Data[0].Embedding)))\n\t\t\t}\n\t\t}))\n\t} else {\n\t\tlo.Must0(parallel.For(context.Background(), test.CountItems(), jobs, func(i int) {\n\t\t\titem := test.GetItems()[i]\n\t\t\tresult, err := expr.Run(embeddingProgram, map[string]any{\n\t\t\t\t\"item\": item,\n\t\t\t})\n\t\t\tif err == nil {\n\t\t\t\tif e, ok := result.([]float32); ok {\n\t\t\t\t\tembeddings[i] = e\n\t\t\t\t\tif dim := dimensions.Swap(int64(len(e))); dim == 0 {\n\t\t\t\t\t\tdimensions.Store(int64(len(e)))\n\t\t\t\t\t} else if dim != int64(len(e)) {\n\t\t\t\t\t\tlog.Logger().Fatal(\"inconsistent embedding dimensions\",\n\t\t\t\t\t\t\tzap.Int64(\"expected\", dim),\n\t\t\t\t\t\t\tzap.Int64(\"got\", int64(len(e))))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}))\n\t}\n\n\tvar (\n\t\tndcg      atomic.Float32\n\t\tprecision atomic.Float32\n\t\trecall    atomic.Float32\n\t\tcount     atomic.Float32\n\t)\n\tnegatives := test.SampleUserNegatives(train, 99)\n\tlo.Must0(parallel.For(context.Background(), test.CountUsers(), jobs, func(userIdx int) {\n\t\ttargetSet := mapset.NewSet(test.GetUserFeedback()[userIdx]...)\n\t\tnegativeSample := negatives[userIdx]\n\t\tif len(test.GetUserFeedback()[userIdx]) == 0 {\n\t\t\treturn\n\t\t}\n\n\t\tcandidates := make([]int32, 0, targetSet.Cardinality()+len(negativeSample))\n\t\tcandidates = append(candidates, test.GetUserFeedback()[userIdx]...)\n\t\tcandidates = append(candidates, negativeSample...)\n\n\t\tfeedback := train.GetUserFeedback()[userIdx]\n\t\tif len(feedback) == 0 {\n\t\t\treturn\n\t\t}\n\n\t\th := heap.NewTopKFilter[int32, float32](topK)\n\t\tfor _, candidateIdx := range candidates {\n\t\t\tcandidateEmbedding := embeddings[candidateIdx]\n\t\t\tif candidateEmbedding == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tvar totalDistance float32\n\t\t\tvar validShots int\n\t\t\tfor _, shotIdx := range feedback {\n\t\t\t\tshotEmbedding := embeddings[shotIdx]\n\t\t\t\tif shotEmbedding == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\ttotalDistance -= floats.Euclidean(candidateEmbedding, shotEmbedding)\n\t\t\t\tvalidShots++\n\t\t\t}\n\t\t\tif validShots > 0 {\n\t\t\t\th.Push(candidateIdx, totalDistance)\n\t\t\t}\n\t\t}\n\n\t\tif h.Len() == 0 {\n\t\t\treturn\n\t\t}\n\n\t\trankList := h.PopAllValues()\n\t\tndcg.Add(cf.NDCG(targetSet, rankList))\n\t\tprecision.Add(cf.Precision(targetSet, rankList))\n\t\trecall.Add(cf.Recall(targetSet, rankList))\n\t\tcount.Add(1)\n\t}))\n\n\tvar score cf.Score\n\tif count.Load() > 0 {\n\t\tscore = cf.Score{\n\t\t\tNDCG:      ndcg.Load() / count.Load(),\n\t\t\tPrecision: precision.Load() / count.Load(),\n\t\t\tRecall:    recall.Load() / count.Load(),\n\t\t}\n\t}\n\tscores.Store(fmt.Sprintf(\"%s (%d)\", cfg.OpenAI.EmbeddingModel, dimensions.Load()), score)\n}\n\nvar benchEmbeddingCmd = &cobra.Command{\n\tUse:   \"bench-embedding\",\n\tShort: \"Benchmark embedding models for item-to-item\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\t// Load configuration\n\t\tconfigPath, _ := cmd.Flags().GetString(\"config\")\n\t\tcfg, err := config.LoadConfig(configPath)\n\t\tif err != nil {\n\t\t\tlog.Logger().Fatal(\"failed to load config\", zap.Error(err))\n\t\t}\n\t\tshots, _ := cmd.Flags().GetInt(\"shots\")\n\t\tembeddingColumn, _ := cmd.Flags().GetString(\"embedding-column\")\n\t\ttextColumn, _ := cmd.Flags().GetString(\"text-column\")\n\t\tif embeddingColumn == \"\" && textColumn == \"\" {\n\t\t\tlog.Logger().Fatal(\"one of embedding-column or text-column is required\")\n\t\t}\n\n\t\t// Load dataset\n\t\tm := master.NewMaster(cfg, os.TempDir(), false, configPath)\n\t\tm.DataClient, err = data.Open(m.Config.Database.DataStore, m.Config.Database.DataTablePrefix,\n\t\t\tstorage.WithIsolationLevel(m.Config.Database.MySQL.IsolationLevel))\n\t\tif err != nil {\n\t\t\tlog.Logger().Fatal(\"failed to open data client\", zap.Error(err))\n\t\t}\n\t\tevaluator := master.NewOnlineEvaluator(\n\t\t\tm.Config.Recommend.DataSource.PositiveFeedbackTypes,\n\t\t\tm.Config.Recommend.DataSource.ReadFeedbackTypes)\n\t\t_, dataset, err := m.LoadDataFromDatabase(context.Background(), m.DataClient,\n\t\t\tm.Config.Recommend.DataSource.PositiveFeedbackTypes,\n\t\t\tm.Config.Recommend.DataSource.ReadFeedbackTypes,\n\t\t\tm.Config.Recommend.DataSource.ItemTTL,\n\t\t\tm.Config.Recommend.DataSource.PositiveFeedbackTTL,\n\t\t\tevaluator,\n\t\t\tnil)\n\t\tif err != nil {\n\t\t\tlog.Logger().Fatal(\"failed to load dataset\", zap.Error(err))\n\t\t}\n\n\t\t// Override config\n\t\tif cmd.Flags().Changed(\"embedding-model\") {\n\t\t\tcfg.OpenAI.EmbeddingModel = cmd.Flag(\"embedding-model\").Value.String()\n\t\t}\n\t\tdimensions, _ := cmd.Flags().GetInt(\"embedding-dimensions\")\n\t\tcfg.OpenAI.EmbeddingDimensions = dimensions\n\n\t\t// Split dataset\n\t\tvar scores sync.Map\n\t\ttrain, test := dataset.SplitLatest(shots)\n\t\ttest.SampleUserNegatives(dataset, 99)\n\n\t\ttable := tablewriter.NewWriter(os.Stdout)\n\t\ttable.Header([]string{\"\", \"#users\", \"#items\", \"#interactions\"})\n\t\tlo.Must0(table.Bulk([][]string{\n\t\t\t{\"total\", strconv.Itoa(dataset.CountUsers()), strconv.Itoa(dataset.CountItems()), strconv.Itoa(dataset.CountFeedback())},\n\t\t\t{\"train\", strconv.Itoa(train.CountUsers()), strconv.Itoa(train.CountItems()), strconv.Itoa(train.CountFeedback())},\n\t\t\t{\"test\", strconv.Itoa(test.CountUsers()), strconv.Itoa(test.CountItems()), strconv.Itoa(test.CountFeedback())},\n\t\t}))\n\t\tlo.Must0(table.Render())\n\n\t\ttopK, _ := cmd.Flags().GetInt(\"top\")\n\t\tjobs, _ := cmd.Flags().GetInt(\"jobs\")\n\t\tEvaluateEmbedding(cfg, train, test, embeddingColumn, textColumn, topK, jobs, &scores)\n\t\tdata := [][]string{{\n\t\t\t\"Embedding Model\",\n\t\t\tfmt.Sprintf(\"NDCG@%d\", topK),\n\t\t\tfmt.Sprintf(\"Precision@%d\", topK),\n\t\t\tfmt.Sprintf(\"Recall@%d\", topK),\n\t\t}}\n\t\tscores.Range(func(key, value any) bool {\n\t\t\tscore := value.(cf.Score)\n\t\t\tdata = append(data, []string{\n\t\t\t\tkey.(string),\n\t\t\t\tfmt.Sprintf(\"%.4f\", score.NDCG),\n\t\t\t\tfmt.Sprintf(\"%.4f\", score.Precision),\n\t\t\t\tfmt.Sprintf(\"%.4f\", score.Recall),\n\t\t\t})\n\t\t\treturn true\n\t\t})\n\t\ttable = tablewriter.NewWriter(os.Stdout)\n\t\ttable.Header(data[0])\n\t\tlo.Must0(table.Bulk(data[1:]))\n\t\tlo.Must0(table.Render())\n\t},\n}\n\nfunc init() {\n\trootCmd.PersistentFlags().StringP(\"config\", \"c\", \"\", \"Path to configuration file\")\n\trootCmd.PersistentFlags().IntP(\"jobs\", \"j\", runtime.NumCPU(), \"Number of jobs to run in parallel\")\n\trootCmd.AddCommand(benchLLMCmd)\n\trootCmd.AddCommand(benchEmbeddingCmd)\n\tbenchLLMCmd.PersistentFlags().Bool(\"user-auc\", false, \"Export user-level AUC scores to CSV file\")\n\tbenchEmbeddingCmd.PersistentFlags().IntP(\"top\", \"k\", 10, \"Number of top items to evaluate for each user\")\n\tbenchEmbeddingCmd.PersistentFlags().IntP(\"shots\", \"s\", math.MaxInt, \"Number of shots for each user\")\n\tbenchEmbeddingCmd.PersistentFlags().Int(\"embedding-dimensions\", 0, \"Embedding dimensions\")\n\tbenchEmbeddingCmd.PersistentFlags().String(\"embedding-model\", \"\", \"Embedding model\")\n\tbenchEmbeddingCmd.PersistentFlags().String(\"embedding-column\", \"\", \"Column name of embedding in item label\")\n\tbenchEmbeddingCmd.PersistentFlags().String(\"text-column\", \"\", \"Column name of text in item label\")\n}\n\nfunc main() {\n\tif err := rootCmd.Execute(); err != nil {\n\t\tlog.Logger().Fatal(\"failed to execute command\", zap.Error(err))\n\t}\n}\n"
  },
  {
    "path": "cmd/gorse-in-one/Dockerfile",
    "content": "# syntax = docker/dockerfile:1\n\n############################\n# STEP 1 build executable binary\n############################\nFROM --platform=$BUILDPLATFORM golang:1.26\n\nWORKDIR /src\n\nCOPY go.* ./\n\nRUN go mod download\n\nCOPY . ./\n\nARG TARGETOS\n\nARG TARGETARCH\n\nRUN --mount=type=cache,target=/root/.cache/go-build \\\n    cd cmd/gorse-in-one && \\\n    GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -ldflags=\" \\\n    -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))' \\\n    -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)' \\\n    -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)' \\\n    -X 'github.com/gorse-io/gorse/config.RootDir=/var/lib/gorse'\" . && \\\n    mv gorse-in-one /usr/bin\n\n############################\n# STEP 2 build a small image\n############################\nFROM scratch\n\nCOPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/\n\nCOPY --from=0 /usr/bin/gorse-in-one /usr/bin/gorse-in-one\n\nENV USER=root\n\nENTRYPOINT [\"/usr/bin/gorse-in-one\", \"-c\", \"/etc/gorse/config.toml\"]\n"
  },
  {
    "path": "cmd/gorse-in-one/Dockerfile.cuda",
    "content": "# syntax = docker/dockerfile:1\n\n############################\n# STEP 1 build executable binary\n############################\nFROM nvidia/cuda:12.8.1-devel-ubuntu24.04\n\nCOPY --from=golang:1.26 /usr/local/go/ /usr/local/go/\n\nENV PATH=/usr/local/go/bin:$PATH\n\nRUN apt update && apt install -y git\n\nWORKDIR /src\n\nCOPY go.* ./\n\nRUN go mod download\n\nCOPY . ./\n\nRUN cd common/blas/cublas && make\n\nRUN --mount=type=cache,target=/root/.cache/go-build \\\n    cd cmd/gorse-in-one && \\\n    go build -tags xla -ldflags=\" \\\n       -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)' \\\n       -X 'github.com/gorse-io/gorse/config.RootDir=/var/lib/gorse'\" . && \\\n       mv gorse-in-one /usr/bin\n\n############################\n# STEP 2 build runtime image\n############################\nFROM nvidia/cuda:12.8.1-runtime-ubuntu24.04\n\nCOPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/\n\nCOPY --from=0 /usr/bin/gorse-in-one /usr/bin/gorse-in-one\n\nRUN /usr/bin/gorse-in-one --version\n\nENV USER=root\n\nENTRYPOINT [\"/usr/bin/gorse-in-one\", \"-c\", \"/etc/gorse/config.toml\"]\n"
  },
  {
    "path": "cmd/gorse-in-one/Dockerfile.mkl",
    "content": "# syntax = docker/dockerfile:1\n\n############################\n# STEP 1 build executable binary\n############################\nFROM golang:1.26-bookworm\n\n# Install Intel® oneAPI Toolkits\n\nRUN apt update && apt install -y wget gnupg2 git\n\nRUN wget -O- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB \\\n    | gpg --dearmor | tee /usr/share/keyrings/oneapi-archive-keyring.gpg > /dev/null\n\nRUN echo \"deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main\" | tee /etc/apt/sources.list.d/oneAPI.list\n\nRUN apt update && apt install -y intel-oneapi-base-toolkit\n\n# Build Gorse\n\nWORKDIR /src\n\nCOPY go.* ./\n\nRUN go mod download\n\nCOPY . ./\n\nRUN --mount=type=cache,target=/root/.cache/go-build \\\n    . /opt/intel/oneapi/setvars.sh && \\\n    cd cmd/gorse-in-one && \\\n    go build -tags mkl -ldflags=\" \\\n       -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)' \\\n       -X 'github.com/gorse-io/gorse/config.RootDir=/var/lib/gorse'\" .\n\n############################\n# STEP 2 build runtime image\n############################\nFROM debian:bookworm-slim\n\nRUN apt update && apt install -y ca-certificates && rm -rf /var/lib/apt/lists/*\n\nCOPY --from=0 /src/cmd/gorse-in-one/gorse-in-one /usr/bin/gorse-in-one\n\nRUN /usr/bin/gorse-in-one --version\n\nENV USER=root\n\nENTRYPOINT [\"/usr/bin/gorse-in-one\", \"-c\", \"/etc/gorse/config.toml\"]\n"
  },
  {
    "path": "cmd/gorse-in-one/Dockerfile.openblas",
    "content": "# syntax = docker/dockerfile:1\n\n############################\n# STEP 1 build executable binary\n############################\nFROM --platform=$BUILDPLATFORM golang:1.26\n\n# Install OpenBLAS\n\nARG BUILDARCH\n\nARG TARGETARCH\n\nRUN if [ \"${TARGETARCH}\" = \"amd64\" ]; then \\\n    dpkg --add-architecture ${TARGETARCH} && apt update && apt install -y gcc-x86-64-linux-gnu libopenblas-dev:${TARGETARCH} git; \\\nelif [ \"${TARGETARCH}\" = \"arm64\" ]; then \\\n    dpkg --add-architecture ${TARGETARCH} && apt update && apt install -y gcc-aarch64-linux-gnu libopenblas-dev:${TARGETARCH} git; \\\nelif [ \"${TARGETARCH}\" = \"riscv64\" ]; then \\\n    dpkg --add-architecture ${TARGETARCH} && apt update && apt install -y gcc-riscv64-linux-gnu libopenblas-dev:${TARGETARCH} git; \\\nelif [ \"${TARGETARCH}\" = \"ppc64le\" ]; then \\\n    dpkg --add-architecture ppc64el && apt update && apt install -y gcc-powerpc64le-linux-gnu libopenblas-dev:ppc64el git; \\\nelif [ \"${TARGETARCH}\" = \"s390x\" ]; then \\\n    dpkg --add-architecture ${TARGETARCH} && apt update && apt install -y gcc-s390x-linux-gnu libopenblas-dev:${TARGETARCH} git; \\\nelif [ \"${TARGETARCH}\" = \"loong64\" ]; then \\\n    dpkg --add-architecture ${TARGETARCH} && apt update && apt install -y gcc-loongarch64-linux-gnu libopenblas-dev:${TARGETARCH} git; \\\nelse \\\n    echo \"Unsupported TARGETARCH ${TARGETARCH}\"; exit 1; \\\nfi\n\n# Build Gorse\n\nWORKDIR /src\n\nCOPY go.* ./\n\nRUN go mod download\n\nCOPY . ./\n\nARG TARGETOS\n\nRUN --mount=type=cache,target=/root/.cache/go-build \\\n    if [ \"${TARGETARCH}\" = \"amd64\" ]; then \\\n        export CC=x86_64-linux-gnu-gcc; \\\n    elif [ \"${TARGETARCH}\" = \"arm64\" ]; then \\\n        export CC=aarch64-linux-gnu-gcc; \\\n    elif [ \"${TARGETARCH}\" = \"riscv64\" ]; then \\\n        export CC=riscv64-linux-gnu-gcc; \\\n    elif [ \"${TARGETARCH}\" = \"ppc64le\" ]; then \\\n        export CC=powerpc64le-linux-gnu-gcc; \\\n    elif [ \"${TARGETARCH}\" = \"s390x\" ]; then \\\n        export CC=s390x-linux-gnu-gcc; \\\n    elif [ \"${TARGETARCH}\" = \"loong64\" ]; then \\\n        export CC=loongarch64-linux-gnu-gcc; \\\n    else \\\n        echo \"Unsupported TARGETARCH ${TARGETARCH}\"; exit 1; \\\n    fi && \\\n    cd cmd/gorse-in-one && \\\n    GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=1 go build -tags openblas -ldflags=\" \\\n       -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)' \\\n       -X 'github.com/gorse-io/gorse/config.RootDir=/var/lib/gorse'\" .\n\n############################\n# STEP 2 build runtime image\n############################\nFROM debian:trixie-slim\n\nRUN apt update && apt install -y ca-certificates && rm -rf /var/lib/apt/lists/*\n\nCOPY --from=0 /src/cmd/gorse-in-one/gorse-in-one /usr/bin/gorse-in-one\n\nRUN /usr/bin/gorse-in-one --version\n\nENV USER=root\n\nENTRYPOINT [\"/usr/bin/gorse-in-one\", \"-c\", \"/etc/gorse/config.toml\"]\n"
  },
  {
    "path": "cmd/gorse-in-one/Dockerfile.windows",
    "content": "############################\n# STEP 1 build executable binary\n############################\nFROM golang:1.26\n\nWORKDIR /src\n\nCOPY . ./\n\nENV CGO_ENABLED=0\n\nRUN go build -o / -ldflags=\"\\\" \\\n        -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))' \\\n        -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)' \\\n        -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)'\\\"\" ./cmd/...\n\n############################\n# STEP 2 build a small image\n############################\nFROM mcr.microsoft.com/windows/servercore:ltsc2022\n\nCOPY --from=0 /gorse-in-one.exe /gorse-in-one.exe\n\nRUN /gorse-in-one.exe --version\n\nENTRYPOINT [ \"/gorse-in-one.exe\" ]\n"
  },
  {
    "path": "cmd/gorse-in-one/main.go",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage main\n\nimport (\n\t\"compress/gzip\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"runtime\"\n\n\t\"github.com/gorse-io/gorse/client\"\n\t\"github.com/gorse-io/gorse/cmd/version\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/master\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/klauspost/cpuid/v2\"\n\t\"github.com/schollz/progressbar/v3\"\n\t\"github.com/spf13/cobra\"\n\t\"go.uber.org/zap\"\n)\n\nvar oneCommand = &cobra.Command{\n\tUse:   \"gorse-in-one\",\n\tShort: \"The all in one distribution of gorse recommender system.\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\t// Show version\n\t\tif showVersion, _ := cmd.PersistentFlags().GetBool(\"version\"); showVersion {\n\t\t\tfmt.Println(version.BuildInfo())\n\t\t\treturn\n\t\t}\n\n\t\t// setup logger\n\t\tdebug, _ := cmd.PersistentFlags().GetBool(\"debug\")\n\t\tlog.SetLogger(cmd.PersistentFlags(), debug)\n\n\t\t// locate config file\n\t\tvar configPath string\n\t\tcachePath, _ := cmd.PersistentFlags().GetString(\"cache-path\")\n\t\tplayground, _ := cmd.PersistentFlags().GetString(\"playground\")\n\t\tif playground != \"\" {\n\t\t\tuserHomeDir, err := os.UserHomeDir()\n\t\t\tif err != nil {\n\t\t\t\tlog.Logger().Fatal(\"failed to get user home directory\", zap.Error(err))\n\t\t\t}\n\t\t\tetcDir := filepath.Join(userHomeDir, \".gorse\", \"etc\")\n\t\t\tif err = os.MkdirAll(etcDir, os.ModePerm); err != nil {\n\t\t\t\tlog.Logger().Fatal(\"failed to create config directory\", zap.Error(err))\n\t\t\t}\n\t\t\tconfigPath = filepath.Join(etcDir, \"config.toml\")\n\t\t\tif playground == \"ml-100k\" {\n\t\t\t\terr = os.WriteFile(configPath, []byte(client.ConfigTOML), 0644)\n\t\t\t} else {\n\t\t\t\terr = os.WriteFile(configPath, []byte(config.ConfigTOML), 0644)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tlog.Logger().Fatal(\"failed to write playground config\", zap.Error(err))\n\t\t\t}\n\t\t\tfmt.Println(\"Generated config file:\", configPath)\n\t\t\tfmt.Println(\"Using cache directory:\", cachePath)\n\t\t} else {\n\t\t\tconfigPath, _ = cmd.PersistentFlags().GetString(\"config\")\n\t\t\tlog.Logger().Info(\"load config\", zap.String(\"config\", configPath))\n\t\t}\n\n\t\t// load config\n\t\tconf, err := config.LoadConfig(configPath)\n\t\tif err != nil {\n\t\t\tlog.Logger().Fatal(\"failed to load config\", zap.Error(err))\n\t\t}\n\n\t\t// create master\n\t\tm := master.NewMaster(conf, cachePath, true, configPath)\n\n\t\tif playground != \"\" {\n\t\t\tsetup(m, playground)\n\t\t}\n\n\t\t// Stop master\n\t\tdone := make(chan struct{})\n\t\tgo func() {\n\t\t\tsigint := make(chan os.Signal, 1)\n\t\t\tsignal.Notify(sigint, os.Interrupt)\n\t\t\t<-sigint\n\t\t\tm.Shutdown()\n\t\t\tclose(done)\n\t\t}()\n\t\t// Start master\n\t\tm.Serve()\n\t\t<-done\n\t\tlog.Logger().Info(\"stop gorse-in-one successfully\")\n\t},\n}\n\nfunc init() {\n\tlog.AddFlags(oneCommand.PersistentFlags())\n\toneCommand.PersistentFlags().Bool(\"debug\", false, \"use debug log mode\")\n\toneCommand.PersistentFlags().BoolP(\"version\", \"v\", false, \"gorse version\")\n\toneCommand.PersistentFlags().String(\"playground\", \"\", \"playground mode (setup a recommender system for GitHub repositories)\")\n\toneCommand.PersistentFlags().Lookup(\"playground\").NoOptDefVal = \"default\"\n\toneCommand.PersistentFlags().StringP(\"config\", \"c\", \"\", \"configuration file path\")\n\toneCommand.PersistentFlags().String(\"cache-path\", config.MkDir(\"master\"), \"path of cache folder\")\n}\n\nfunc main() {\n\tif err := oneCommand.Execute(); err != nil {\n\t\tlog.Logger().Fatal(\"failed to execute\", zap.Error(err))\n\t}\n}\n\nfunc setup(m *master.Master, playground string) {\n\t// set database to user home directory\n\tm.Config.Database.DataStore = config.GetDefaultConfig().Database.DataStore\n\tfmt.Println(\"Using database:\", m.Config.Database.DataStore)\n\tm.Config.Database.CacheStore = config.GetDefaultConfig().Database.CacheStore\n\tfmt.Println(\"Using cache:\", m.Config.Database.CacheStore)\n\tm.Config.Master.NumJobs = runtime.NumCPU()\n\tfmt.Printf(\"Using %d CPU cores: %s\\n\", m.Config.Master.NumJobs, cpuid.CPU.BrandName)\n\n\t// connect database\n\tvar err error\n\tdataOpts := m.Config.Database.StorageOptions(m.Config.Database.DataStore)\n\tm.DataClient, err = data.Open(m.Config.Database.DataStore, m.Config.Database.DataTablePrefix, dataOpts...)\n\tif err != nil {\n\t\tlog.Logger().Fatal(\"failed to connect data database\", zap.Error(err),\n\t\t\tzap.String(\"database\", log.RedactDBURL(m.Config.Database.DataStore)))\n\t}\n\tdefer m.DataClient.Close()\n\tif err = m.DataClient.Init(); err != nil {\n\t\tlog.Logger().Fatal(\"failed to init database\", zap.Error(err))\n\t}\n\n\t// import playground data\n\tvar resp *http.Response\n\tif playground == \"ml-100k\" {\n\t\tresp, err = http.Get(\"https://cdn.gorse.io/example/ml-100k.bin.gz\")\n\t} else {\n\t\tresp, err = http.Get(\"https://cdn.gorse.io/example/github.bin.gz\")\n\t}\n\tif err != nil {\n\t\tlog.Logger().Fatal(\"failed to download playground data\", zap.Error(err))\n\t}\n\tdefer resp.Body.Close()\n\tbar := progressbar.DefaultBytes(\n\t\tresp.ContentLength,\n\t\t\"Importing data\",\n\t)\n\tp := progressbar.NewReader(resp.Body, bar)\n\td, err := gzip.NewReader(&p)\n\tif err != nil {\n\t\tlog.Logger().Fatal(\"failed to decompress playground data\", zap.Error(err))\n\t}\n\t_, err = m.Restore(d)\n\tif err != nil {\n\t\tlog.Logger().Fatal(\"failed to import playground data\", zap.Error(err))\n\t}\n\n\t// show info\n\tfmt.Printf(\"Welcome to Gorse Playground\\n\")\n\tfmt.Println()\n\tfmt.Printf(\"    Dashboard:     http://127.0.0.1:%d/overview\\n\", m.Config.Master.HttpPort)\n\tfmt.Printf(\"    RESTful APIs:  http://127.0.0.1:%d/apidocs\\n\", m.Config.Master.HttpPort)\n\tfmt.Printf(\"    Documentation: https://gorse.io\\n\")\n\tfmt.Println()\n}\n"
  },
  {
    "path": "cmd/gorse-master/Dockerfile",
    "content": "# syntax = docker/dockerfile:1\n\n############################\n# STEP 1 build executable binary\n############################\nFROM --platform=$BUILDPLATFORM golang:1.26\n\nWORKDIR /src\n\nCOPY go.* ./\n\nRUN go mod download\n\nCOPY . ./\n\nARG TARGETOS\n\nARG TARGETARCH\n\nRUN --mount=type=cache,target=/root/.cache/go-build \\\n    cd cmd/gorse-master && \\\n    GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -ldflags=\" \\\n    -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))' \\\n    -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)' \\\n    -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)' \\\n    -X 'github.com/gorse-io/gorse/config.RootDir=/var/lib/gorse'\" .\n\n############################\n# STEP 2 build a small image\n############################\nFROM scratch\n\nCOPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/\n\nCOPY --from=0 /src/cmd/gorse-master/gorse-master /usr/bin/gorse-master\n\nENV USER=root\n\nENTRYPOINT [\"/usr/bin/gorse-master\", \"-c\", \"/etc/gorse/config.toml\"]\n"
  },
  {
    "path": "cmd/gorse-master/Dockerfile.cuda",
    "content": "# syntax = docker/dockerfile:1\n\n############################\n# STEP 1 build executable binary\n############################\nFROM nvidia/cuda:12.8.1-devel-ubuntu24.04\n\nCOPY --from=golang:1.26 /usr/local/go/ /usr/local/go/\n\nENV PATH=/usr/local/go/bin:$PATH\n\nRUN apt update && apt install -y git\n\nWORKDIR /src\n\nCOPY go.* ./\n\nRUN go mod download\n\nCOPY . ./\n\nRUN cd common/blas/cublas && make\n\nRUN --mount=type=cache,target=/root/.cache/go-build \\\n    cd cmd/gorse-master && \\\n    go build -tags xla -ldflags=\" \\\n       -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)' \\\n       -X 'github.com/gorse-io/gorse/config.RootDir=/var/lib/gorse'\" .\n\n############################\n# STEP 2 build runtime image\n############################\nFROM nvidia/cuda:12.8.1-runtime-ubuntu24.04\n\nCOPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/\n\nCOPY --from=0 /src/cmd/gorse-master/gorse-master /usr/bin/gorse-master\n\nRUN /usr/bin/gorse-master --version\n\nENV USER=root\n\nENTRYPOINT [\"/usr/bin/gorse-master\", \"-c\", \"/etc/gorse/config.toml\"]\n"
  },
  {
    "path": "cmd/gorse-master/Dockerfile.mkl",
    "content": "# syntax = docker/dockerfile:1\n\n############################\n# STEP 1 build executable binary\n############################\nFROM golang:1.26-bookworm\n\n# Install Intel® oneAPI Toolkits\n\nRUN apt update && apt install -y wget gnupg2 git\n\nRUN wget -O- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB \\\n    | gpg --dearmor | tee /usr/share/keyrings/oneapi-archive-keyring.gpg > /dev/null\n\nRUN echo \"deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main\" | tee /etc/apt/sources.list.d/oneAPI.list\n\nRUN apt update && apt install -y intel-oneapi-base-toolkit\n\n# Build Gorse\n\nWORKDIR /src\n\nCOPY go.* ./\n\nRUN go mod download\n\nCOPY . ./\n\nRUN --mount=type=cache,target=/root/.cache/go-build \\\n    . /opt/intel/oneapi/setvars.sh && \\\n    cd cmd/gorse-master && \\\n    go build -tags mkl -ldflags=\" \\\n       -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)' \\\n       -X 'github.com/gorse-io/gorse/config.RootDir=/var/lib/gorse'\" .\n\n############################\n# STEP 2 build runtime image\n############################\nFROM debian:bookworm-slim\n\nRUN apt update && apt install -y ca-certificates && rm -rf /var/lib/apt/lists/*\n\nCOPY --from=0 /src/cmd/gorse-master/gorse-master /usr/bin/gorse-master\n\nRUN /usr/bin/gorse-master --version\n\nENV USER=root\n\nENTRYPOINT [\"/usr/bin/gorse-master\", \"-c\", \"/etc/gorse/config.toml\"]\n"
  },
  {
    "path": "cmd/gorse-master/Dockerfile.openblas",
    "content": "# syntax = docker/dockerfile:1\n\n############################\n# STEP 1 build executable binary\n############################\nFROM --platform=$BUILDPLATFORM golang:1.26\n\n# Install OpenBLAS\n\nARG BUILDARCH\n\nARG TARGETARCH\n\nRUN if [ \"${TARGETARCH}\" = \"amd64\" ]; then \\\n    dpkg --add-architecture ${TARGETARCH} && apt update && apt install -y gcc-x86-64-linux-gnu libopenblas-dev:${TARGETARCH} git; \\\nelif [ \"${TARGETARCH}\" = \"arm64\" ]; then \\\n    dpkg --add-architecture ${TARGETARCH} && apt update && apt install -y gcc-aarch64-linux-gnu libopenblas-dev:${TARGETARCH} git; \\\nelif [ \"${TARGETARCH}\" = \"riscv64\" ]; then \\\n    dpkg --add-architecture ${TARGETARCH} && apt update && apt install -y gcc-riscv64-linux-gnu libopenblas-dev:${TARGETARCH} git; \\\nelif [ \"${TARGETARCH}\" = \"ppc64le\" ]; then \\\n    dpkg --add-architecture ppc64el && apt update && apt install -y gcc-powerpc64le-linux-gnu libopenblas-dev:ppc64el git; \\\nelif [ \"${TARGETARCH}\" = \"s390x\" ]; then \\\n    dpkg --add-architecture ${TARGETARCH} && apt update && apt install -y gcc-s390x-linux-gnu libopenblas-dev:${TARGETARCH} git; \\\nelif [ \"${TARGETARCH}\" = \"loong64\" ]; then \\\n    dpkg --add-architecture ${TARGETARCH} && apt update && apt install -y gcc-loongarch64-linux-gnu libopenblas-dev:${TARGETARCH} git; \\\nelse \\\n    echo \"Unsupported TARGETARCH ${TARGETARCH}\"; exit 1; \\\nfi\n\n# Build Gorse\n\nWORKDIR /src\n\nCOPY go.* ./\n\nRUN go mod download\n\nCOPY . ./\n\nARG TARGETOS\n\nRUN --mount=type=cache,target=/root/.cache/go-build \\\n    if [ \"${TARGETARCH}\" = \"amd64\" ]; then \\\n        export CC=x86_64-linux-gnu-gcc; \\\n    elif [ \"${TARGETARCH}\" = \"arm64\" ]; then \\\n        export CC=aarch64-linux-gnu-gcc; \\\n    elif [ \"${TARGETARCH}\" = \"riscv64\" ]; then \\\n        export CC=riscv64-linux-gnu-gcc; \\\n    elif [ \"${TARGETARCH}\" = \"ppc64le\" ]; then \\\n        export CC=powerpc64le-linux-gnu-gcc; \\\n    elif [ \"${TARGETARCH}\" = \"s390x\" ]; then \\\n        export CC=s390x-linux-gnu-gcc; \\\n    elif [ \"${TARGETARCH}\" = \"loong64\" ]; then \\\n        export CC=loongarch64-linux-gnu-gcc; \\\n    else \\\n        echo \"Unsupported TARGETARCH ${TARGETARCH}\"; exit 1; \\\n    fi && \\\n    cd cmd/gorse-master && \\\n    GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=1 go build -tags openblas -ldflags=\" \\\n       -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)' \\\n       -X 'github.com/gorse-io/gorse/config.RootDir=/var/lib/gorse'\" .\n\n############################\n# STEP 2 build runtime image\n############################\nFROM debian:trixie-slim\n\nRUN apt update && apt install -y ca-certificates && rm -rf /var/lib/apt/lists/*\n\nCOPY --from=0 /src/cmd/gorse-master/gorse-master /usr/bin/gorse-master\n\nRUN /usr/bin/gorse-master --version\n\nENV USER=root\n\nENTRYPOINT [\"/usr/bin/gorse-master\", \"-c\", \"/etc/gorse/config.toml\"]\n"
  },
  {
    "path": "cmd/gorse-master/Dockerfile.windows",
    "content": "############################\n# STEP 1 build executable binary\n############################\nFROM golang:1.26\n\nWORKDIR /src\n\nCOPY . ./\n\nENV CGO_ENABLED=0\n\nRUN go build -o / -ldflags=\"\\\" \\\n        -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))' \\\n        -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)' \\\n        -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)'\\\"\" ./cmd/...\n\n############################\n# STEP 2 build a small image\n############################\nFROM mcr.microsoft.com/windows/servercore:ltsc2022\n\nCOPY --from=0 /gorse-master.exe /gorse-master.exe\n\nRUN /gorse-master.exe --version\n\nENTRYPOINT [ \"/gorse-master.exe\" ]\n"
  },
  {
    "path": "cmd/gorse-master/main.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\n\t\"github.com/gorse-io/gorse/cmd/version\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/master\"\n\t\"github.com/spf13/cobra\"\n\t\"go.uber.org/zap\"\n)\n\nvar masterCommand = &cobra.Command{\n\tUse:   \"gorse-master\",\n\tShort: \"The master node of gorse recommender system.\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\t// Show version\n\t\tif showVersion, _ := cmd.PersistentFlags().GetBool(\"version\"); showVersion {\n\t\t\tfmt.Println(version.BuildInfo())\n\t\t\treturn\n\t\t}\n\t\t// setup logger\n\t\tdebug, _ := cmd.PersistentFlags().GetBool(\"debug\")\n\t\tlog.SetLogger(cmd.PersistentFlags(), debug)\n\n\t\t// Create master\n\t\tconfigPath, _ := cmd.PersistentFlags().GetString(\"config\")\n\t\tlog.Logger().Info(\"load config\", zap.String(\"config\", configPath))\n\t\tconf, err := config.LoadConfig(configPath)\n\t\tif err != nil {\n\t\t\tlog.Logger().Fatal(\"failed to load config\", zap.Error(err))\n\t\t}\n\t\tcachePath, _ := cmd.PersistentFlags().GetString(\"cache-path\")\n\t\tm := master.NewMaster(conf, cachePath, false, configPath)\n\t\t// Stop master\n\t\tdone := make(chan struct{})\n\t\tgo func() {\n\t\t\tsigint := make(chan os.Signal, 1)\n\t\t\tsignal.Notify(sigint, os.Interrupt)\n\t\t\t<-sigint\n\t\t\tm.Shutdown()\n\t\t\tclose(done)\n\t\t}()\n\t\t// Start master\n\t\tm.Serve()\n\t\t<-done\n\t\tlog.Logger().Info(\"stop gorse master successfully\")\n\t},\n}\n\nfunc init() {\n\tlog.AddFlags(masterCommand.PersistentFlags())\n\tmasterCommand.PersistentFlags().Bool(\"debug\", false, \"use debug log mode\")\n\tmasterCommand.PersistentFlags().StringP(\"config\", \"c\", \"\", \"configuration file path\")\n\tmasterCommand.PersistentFlags().BoolP(\"version\", \"v\", false, \"gorse version\")\n\tmasterCommand.PersistentFlags().String(\"cache-path\", config.MkDir(\"master\"), \"path of cache folder\")\n}\n\nfunc main() {\n\tif err := masterCommand.Execute(); err != nil {\n\t\tlog.Logger().Fatal(\"failed to execute\", zap.Error(err))\n\t}\n}\n"
  },
  {
    "path": "cmd/gorse-server/Dockerfile",
    "content": "# syntax = docker/dockerfile:1\n\n############################\n# STEP 1 build executable binary\n############################\nFROM --platform=$BUILDPLATFORM golang:1.26\n\nWORKDIR /src\n\nCOPY go.* ./\n\nRUN go mod download\n\nCOPY . ./\n\nARG TARGETOS\n\nARG TARGETARCH\n\nRUN --mount=type=cache,target=/root/.cache/go-build \\\n    cd cmd/gorse-server && \\\n    GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -ldflags=\" \\\n    -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))' \\\n    -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)' \\\n    -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)' \\\n    -X 'github.com/gorse-io/gorse/config.RootDir=/var/lib/gorse'\" .\n\n############################\n# STEP 2 build a small image\n############################\nFROM scratch\n\nCOPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/\n\nCOPY --from=0 /src/cmd/gorse-server/gorse-server /usr/bin/gorse-server\n\nENV USER=root\n\nENTRYPOINT [\"/usr/bin/gorse-server\"]\n"
  },
  {
    "path": "cmd/gorse-server/Dockerfile.cuda",
    "content": "# syntax = docker/dockerfile:1\n\n############################\n# STEP 1 build executable binary\n############################\nFROM nvidia/cuda:12.8.1-devel-ubuntu24.04\n\nCOPY --from=golang:1.26 /usr/local/go/ /usr/local/go/\n\nENV PATH=/usr/local/go/bin:$PATH\n\nRUN apt update && apt install -y git\n\nWORKDIR /src\n\nCOPY go.* ./\n\nRUN go mod download\n\nCOPY . ./\n\nRUN cd common/blas/cublas && make\n\nRUN --mount=type=cache,target=/root/.cache/go-build \\\n    cd cmd/gorse-server && \\\n    go build -tags xla -ldflags=\" \\\n       -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)' \\\n       -X 'github.com/gorse-io/gorse/config.RootDir=/var/lib/gorse'\" .\n\n############################\n# STEP 2 build runtime image\n############################\nFROM nvidia/cuda:12.8.1-runtime-ubuntu24.04\n\nCOPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/\n\nCOPY --from=0 /src/cmd/gorse-server/gorse-server /usr/bin/gorse-server\n\nRUN /usr/bin/gorse-server --version\n\nENV USER=root\n\nENTRYPOINT [\"/usr/bin/gorse-server\"]\n"
  },
  {
    "path": "cmd/gorse-server/Dockerfile.mkl",
    "content": "# syntax = docker/dockerfile:1\n\n############################\n# STEP 1 build executable binary\n############################\nFROM golang:1.26-bookworm\n\n# Install Intel® oneAPI Toolkits\n\nRUN apt update && apt install -y wget gnupg2 git\n\nRUN wget -O- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB \\\n    | gpg --dearmor | tee /usr/share/keyrings/oneapi-archive-keyring.gpg > /dev/null\n\nRUN echo \"deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main\" | tee /etc/apt/sources.list.d/oneAPI.list\n\nRUN apt update && apt install -y intel-oneapi-base-toolkit\n\n# Build Gorse\n\nWORKDIR /src\n\nCOPY go.* ./\n\nRUN go mod download\n\nCOPY . ./\n\nRUN --mount=type=cache,target=/root/.cache/go-build \\\n    . /opt/intel/oneapi/setvars.sh && \\\n    cd cmd/gorse-server && \\\n    go build -tags mkl -ldflags=\" \\\n       -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)' \\\n       -X 'github.com/gorse-io/gorse/config.RootDir=/var/lib/gorse'\" .\n\n############################\n# STEP 2 build runtime image\n############################\nFROM debian:bookworm-slim\n\nRUN apt update && apt install -y ca-certificates && rm -rf /var/lib/apt/lists/*\n\nCOPY --from=0 /src/cmd/gorse-server/gorse-server /usr/bin/gorse-server\n\nRUN /usr/bin/gorse-server --version\n\nENV USER=root\n\nENTRYPOINT [\"/usr/bin/gorse-server\"]\n"
  },
  {
    "path": "cmd/gorse-server/Dockerfile.openblas",
    "content": "# syntax = docker/dockerfile:1\n\n############################\n# STEP 1 build executable binary\n############################\nFROM --platform=$BUILDPLATFORM golang:1.26\n\n# Install OpenBLAS\n\nARG BUILDARCH\n\nARG TARGETARCH\n\nRUN if [ \"${TARGETARCH}\" = \"amd64\" ]; then \\\n    dpkg --add-architecture ${TARGETARCH} && apt update && apt install -y gcc-x86-64-linux-gnu libopenblas-dev:${TARGETARCH} git; \\\nelif [ \"${TARGETARCH}\" = \"arm64\" ]; then \\\n    dpkg --add-architecture ${TARGETARCH} && apt update && apt install -y gcc-aarch64-linux-gnu libopenblas-dev:${TARGETARCH} git; \\\nelif [ \"${TARGETARCH}\" = \"riscv64\" ]; then \\\n    dpkg --add-architecture ${TARGETARCH} && apt update && apt install -y gcc-riscv64-linux-gnu libopenblas-dev:${TARGETARCH} git; \\\nelif [ \"${TARGETARCH}\" = \"ppc64le\" ]; then \\\n    dpkg --add-architecture ppc64el && apt update && apt install -y gcc-powerpc64le-linux-gnu libopenblas-dev:ppc64el git; \\\nelif [ \"${TARGETARCH}\" = \"s390x\" ]; then \\\n    dpkg --add-architecture ${TARGETARCH} && apt update && apt install -y gcc-s390x-linux-gnu libopenblas-dev:${TARGETARCH} git; \\\nelif [ \"${TARGETARCH}\" = \"loong64\" ]; then \\\n    dpkg --add-architecture ${TARGETARCH} && apt update && apt install -y gcc-loongarch64-linux-gnu libopenblas-dev:${TARGETARCH} git; \\\nelse \\\n    echo \"Unsupported TARGETARCH ${TARGETARCH}\"; exit 1; \\\nfi\n\n# Build Gorse\n\nWORKDIR /src\n\nCOPY go.* ./\n\nRUN go mod download\n\nCOPY . ./\n\nARG TARGETOS\n\nRUN --mount=type=cache,target=/root/.cache/go-build \\\n    if [ \"${TARGETARCH}\" = \"amd64\" ]; then \\\n        export CC=x86_64-linux-gnu-gcc; \\\n    elif [ \"${TARGETARCH}\" = \"arm64\" ]; then \\\n        export CC=aarch64-linux-gnu-gcc; \\\n    elif [ \"${TARGETARCH}\" = \"riscv64\" ]; then \\\n        export CC=riscv64-linux-gnu-gcc; \\\n    elif [ \"${TARGETARCH}\" = \"ppc64le\" ]; then \\\n        export CC=powerpc64le-linux-gnu-gcc; \\\n    elif [ \"${TARGETARCH}\" = \"s390x\" ]; then \\\n        export CC=s390x-linux-gnu-gcc; \\\n    elif [ \"${TARGETARCH}\" = \"loong64\" ]; then \\\n        export CC=loongarch64-linux-gnu-gcc; \\\n    else \\\n        echo \"Unsupported TARGETARCH ${TARGETARCH}\"; exit 1; \\\n    fi && \\\n    cd cmd/gorse-server && \\\n    GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=1 go build -tags openblas -ldflags=\" \\\n       -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)' \\\n       -X 'github.com/gorse-io/gorse/config.RootDir=/var/lib/gorse'\" .\n\n############################\n# STEP 2 build runtime image\n############################\nFROM debian:trixie-slim\n\nRUN apt update && apt install -y ca-certificates && rm -rf /var/lib/apt/lists/*\n\nCOPY --from=0 /src/cmd/gorse-server/gorse-server /usr/bin/gorse-server\n\nRUN /usr/bin/gorse-server --version\n\nENV USER=root\n\nENTRYPOINT [\"/usr/bin/gorse-server\"]\n"
  },
  {
    "path": "cmd/gorse-server/Dockerfile.windows",
    "content": "############################\n# STEP 1 build executable binary\n############################\nFROM golang:1.26\n\nWORKDIR /src\n\nCOPY . ./\n\nENV CGO_ENABLED=0\n\nRUN go build -o / -ldflags=\"\\\" \\\n        -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))' \\\n        -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)' \\\n        -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)'\\\"\" ./cmd/...\n\n############################\n# STEP 2 build a small image\n############################\nFROM mcr.microsoft.com/windows/servercore:ltsc2022\n\nCOPY --from=0 /gorse-server.exe /gorse-server.exe\n\nRUN /gorse-server.exe --version\n\nENTRYPOINT [ \"/gorse-server.exe\" ]\n"
  },
  {
    "path": "cmd/gorse-server/main.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\n\t\"github.com/gorse-io/gorse/cmd/version\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/common/util\"\n\t\"github.com/gorse-io/gorse/server\"\n\t\"github.com/spf13/cobra\"\n\t\"go.uber.org/zap\"\n)\n\nvar serverCommand = &cobra.Command{\n\tUse:   \"gorse-server\",\n\tShort: \"The server node of gorse recommender system.\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\t// show version\n\t\tshowVersion, _ := cmd.PersistentFlags().GetBool(\"version\")\n\t\tif showVersion {\n\t\t\tfmt.Println(version.BuildInfo())\n\t\t\treturn\n\t\t}\n\n\t\t// setup logger\n\t\tdebug, _ := cmd.PersistentFlags().GetBool(\"debug\")\n\t\tlog.SetLogger(cmd.PersistentFlags(), debug)\n\n\t\t// create server\n\t\tmasterPort, _ := cmd.PersistentFlags().GetInt(\"master-port\")\n\t\tmasterHost, _ := cmd.PersistentFlags().GetString(\"master-host\")\n\t\thttpPort, _ := cmd.PersistentFlags().GetInt(\"http-port\")\n\t\thttpHost, _ := cmd.PersistentFlags().GetString(\"http-host\")\n\t\tcachePath, _ := cmd.PersistentFlags().GetString(\"cache-path\")\n\t\tcaFile, _ := cmd.PersistentFlags().GetString(\"ssl-ca\")\n\t\tcertFile, _ := cmd.PersistentFlags().GetString(\"ssl-cert\")\n\t\tkeyFile, _ := cmd.PersistentFlags().GetString(\"ssl-key\")\n\t\tvar tlsConfig *util.TLSConfig\n\t\tif caFile != \"\" && certFile != \"\" && keyFile != \"\" {\n\t\t\ttlsConfig = &util.TLSConfig{\n\t\t\t\tSSLCA:   caFile,\n\t\t\t\tSSLCert: certFile,\n\t\t\t\tSSLKey:  keyFile,\n\t\t\t}\n\t\t} else if caFile == \"\" && certFile == \"\" && keyFile == \"\" {\n\t\t\ttlsConfig = nil\n\t\t} else {\n\t\t\tlog.Logger().Fatal(\"incomplete SSL configuration\",\n\t\t\t\tzap.String(\"ssl_ca\", caFile),\n\t\t\t\tzap.String(\"ssl_cert\", certFile),\n\t\t\t\tzap.String(\"ssl_key\", keyFile))\n\t\t}\n\t\ts := server.NewServer(masterHost, masterPort, httpHost, httpPort, cachePath, tlsConfig)\n\n\t\t// stop server\n\t\tdone := make(chan struct{})\n\t\tgo func() {\n\t\t\tsigint := make(chan os.Signal, 1)\n\t\t\tsignal.Notify(sigint, os.Interrupt)\n\t\t\t<-sigint\n\t\t\ts.Shutdown()\n\t\t\tclose(done)\n\t\t}()\n\n\t\t// start server\n\t\ts.Serve()\n\t\t<-done\n\t\tlog.Logger().Info(\"stop gorse server successfully\")\n\t},\n}\n\nfunc init() {\n\tlog.AddFlags(serverCommand.PersistentFlags())\n\tserverCommand.PersistentFlags().BoolP(\"version\", \"v\", false, \"gorse version\")\n\tserverCommand.PersistentFlags().Int(\"master-port\", 8086, \"port of master node\")\n\tserverCommand.PersistentFlags().String(\"master-host\", \"127.0.0.1\", \"host of master node\")\n\tserverCommand.PersistentFlags().Int(\"http-port\", 8087, \"host for RESTful APIs and Prometheus metrics export\")\n\tserverCommand.PersistentFlags().String(\"http-host\", \"127.0.0.1\", \"port for RESTful APIs and Prometheus metrics export\")\n\tserverCommand.PersistentFlags().Bool(\"debug\", false, \"use debug log mode\")\n\tserverCommand.PersistentFlags().String(\"cache-path\", \"server_cache.data\", \"path of cache file\")\n\tserverCommand.PersistentFlags().String(\"ssl-ca\", \"\", \"path of SSL CA\")\n\tserverCommand.PersistentFlags().String(\"ssl-cert\", \"\", \"path of SSL certificate\")\n\tserverCommand.PersistentFlags().String(\"ssl-key\", \"\", \"path of SSL key\")\n}\n\nfunc main() {\n\tif err := serverCommand.Execute(); err != nil {\n\t\tlog.Logger().Fatal(\"failed to execute\", zap.Error(err))\n\t}\n}\n"
  },
  {
    "path": "cmd/gorse-worker/Dockerfile",
    "content": "# syntax = docker/dockerfile:1\n\n############################\n# STEP 1 build executable binary\n############################\nFROM --platform=$BUILDPLATFORM golang:1.26\n\nWORKDIR /src\n\nCOPY go.* ./\n\nRUN go mod download\n\nCOPY . ./\n\nARG TARGETOS\n\nARG TARGETARCH\n\nRUN --mount=type=cache,target=/root/.cache/go-build \\\n    cd cmd/gorse-worker && \\\n    GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -ldflags=\" \\\n    -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))' \\\n    -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)' \\\n    -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)' \\\n    -X 'github.com/gorse-io/gorse/config.RootDir=/var/lib/gorse'\" .\n\n############################\n# STEP 2 build a small image\n############################\nFROM scratch\n\nCOPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/\n\nCOPY --from=0 /src/cmd/gorse-worker/gorse-worker /usr/bin/gorse-worker\n\nENV USER=root\n\nENTRYPOINT [\"/usr/bin/gorse-worker\"]\n"
  },
  {
    "path": "cmd/gorse-worker/Dockerfile.cuda",
    "content": "# syntax = docker/dockerfile:1\n\n############################\n# STEP 1 build executable binary\n############################\nFROM nvidia/cuda:12.8.1-devel-ubuntu24.04\n\nCOPY --from=golang:1.26 /usr/local/go/ /usr/local/go/\n\nENV PATH=/usr/local/go/bin:$PATH\n\nRUN apt update && apt install -y git\n\nWORKDIR /src\n\nCOPY go.* ./\n\nRUN go mod download\n\nCOPY . ./\n\nRUN cd common/blas/cublas && make\n\nRUN --mount=type=cache,target=/root/.cache/go-build \\\n    cd cmd/gorse-worker && \\\n    go build -tags xla -ldflags=\" \\\n       -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)' \\\n       -X 'github.com/gorse-io/gorse/config.RootDir=/var/lib/gorse'\" .\n\n############################\n# STEP 2 build runtime image\n############################\nFROM nvidia/cuda:12.8.1-runtime-ubuntu24.04\n\nCOPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/\n\nCOPY --from=0 /src/cmd/gorse-worker/gorse-worker /usr/bin/gorse-worker\n\nRUN /usr/bin/gorse-worker --version\n\nENV USER=root\n\nENTRYPOINT [\"/usr/bin/gorse-worker\"]\n"
  },
  {
    "path": "cmd/gorse-worker/Dockerfile.mkl",
    "content": "# syntax = docker/dockerfile:1\n\n############################\n# STEP 1 build executable binary\n############################\nFROM golang:1.26-bookworm\n\n# Install Intel® oneAPI Toolkits\n\nRUN apt update && apt install -y wget gnupg2 git\n\nRUN wget -O- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB \\\n    | gpg --dearmor | tee /usr/share/keyrings/oneapi-archive-keyring.gpg > /dev/null\n\nRUN echo \"deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main\" | tee /etc/apt/sources.list.d/oneAPI.list\n\nRUN apt update && apt install -y intel-oneapi-base-toolkit\n\n# Build Gorse\n\nWORKDIR /src\n\nCOPY go.* ./\n\nRUN go mod download\n\nCOPY . ./\n\nRUN --mount=type=cache,target=/root/.cache/go-build \\\n    . /opt/intel/oneapi/setvars.sh && \\\n    cd cmd/gorse-worker && \\\n    go build -tags mkl -ldflags=\" \\\n       -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)' \\\n       -X 'github.com/gorse-io/gorse/config.RootDir=/var/lib/gorse'\" .\n\n############################\n# STEP 2 build runtime image\n############################\nFROM debian:bookworm-slim\n\nRUN apt update && apt install -y ca-certificates && rm -rf /var/lib/apt/lists/*\n\nCOPY --from=0 /src/cmd/gorse-worker/gorse-worker /usr/bin/gorse-worker\n\nRUN /usr/bin/gorse-worker --version\n\nENV USER=root\n\nENTRYPOINT [\"/usr/bin/gorse-worker\"]\n"
  },
  {
    "path": "cmd/gorse-worker/Dockerfile.openblas",
    "content": "# syntax = docker/dockerfile:1\n\n############################\n# STEP 1 build executable binary\n############################\nFROM --platform=$BUILDPLATFORM golang:1.26\n\n# Install OpenBLAS\n\nARG BUILDARCH\n\nARG TARGETARCH\n\nRUN if [ \"${TARGETARCH}\" = \"amd64\" ]; then \\\n    dpkg --add-architecture ${TARGETARCH} && apt update && apt install -y gcc-x86-64-linux-gnu libopenblas-dev:${TARGETARCH} git; \\\nelif [ \"${TARGETARCH}\" = \"arm64\" ]; then \\\n    dpkg --add-architecture ${TARGETARCH} && apt update && apt install -y gcc-aarch64-linux-gnu libopenblas-dev:${TARGETARCH} git; \\\nelif [ \"${TARGETARCH}\" = \"riscv64\" ]; then \\\n    dpkg --add-architecture ${TARGETARCH} && apt update && apt install -y gcc-riscv64-linux-gnu libopenblas-dev:${TARGETARCH} git; \\\nelif [ \"${TARGETARCH}\" = \"ppc64le\" ]; then \\\n    dpkg --add-architecture ppc64el && apt update && apt install -y gcc-powerpc64le-linux-gnu libopenblas-dev:ppc64el git; \\\nelif [ \"${TARGETARCH}\" = \"s390x\" ]; then \\\n    dpkg --add-architecture ${TARGETARCH} && apt update && apt install -y gcc-s390x-linux-gnu libopenblas-dev:${TARGETARCH} git; \\\nelif [ \"${TARGETARCH}\" = \"loong64\" ]; then \\\n    dpkg --add-architecture ${TARGETARCH} && apt update && apt install -y gcc-loongarch64-linux-gnu libopenblas-dev:${TARGETARCH} git; \\\nelse \\\n    echo \"Unsupported TARGETARCH ${TARGETARCH}\"; exit 1; \\\nfi\n\n# Build Gorse\n\nWORKDIR /src\n\nCOPY go.* ./\n\nRUN go mod download\n\nCOPY . ./\n\nARG TARGETOS\n\nRUN --mount=type=cache,target=/root/.cache/go-build \\\n    if [ \"${TARGETARCH}\" = \"amd64\" ]; then \\\n        export CC=x86_64-linux-gnu-gcc; \\\n    elif [ \"${TARGETARCH}\" = \"arm64\" ]; then \\\n        export CC=aarch64-linux-gnu-gcc; \\\n    elif [ \"${TARGETARCH}\" = \"riscv64\" ]; then \\\n        export CC=riscv64-linux-gnu-gcc; \\\n    elif [ \"${TARGETARCH}\" = \"ppc64le\" ]; then \\\n        export CC=powerpc64le-linux-gnu-gcc; \\\n    elif [ \"${TARGETARCH}\" = \"s390x\" ]; then \\\n        export CC=s390x-linux-gnu-gcc; \\\n    elif [ \"${TARGETARCH}\" = \"loong64\" ]; then \\\n        export CC=loongarch64-linux-gnu-gcc; \\\n    else \\\n        echo \"Unsupported TARGETARCH ${TARGETARCH}\"; exit 1; \\\n    fi && \\\n    cd cmd/gorse-worker && \\\n    GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=1 go build -tags openblas -ldflags=\" \\\n       -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)' \\\n       -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)' \\\n       -X 'github.com/gorse-io/gorse/config.RootDir=/var/lib/gorse'\" .\n\n############################\n# STEP 2 build runtime image\n############################\nFROM debian:trixie-slim\n\nRUN apt update && apt install -y ca-certificates && rm -rf /var/lib/apt/lists/*\n\nCOPY --from=0 /src/cmd/gorse-worker/gorse-worker /usr/bin/gorse-worker\n\nRUN /usr/bin/gorse-worker --version\n\nENV USER=root\n\nENTRYPOINT [\"/usr/bin/gorse-worker\"]\n"
  },
  {
    "path": "cmd/gorse-worker/Dockerfile.windows",
    "content": "############################\n# STEP 1 build executable binary\n############################\nFROM golang:1.26\n\nWORKDIR /src\n\nCOPY . ./\n\nENV CGO_ENABLED=0\n\nRUN go build -o / -ldflags=\"\\\" \\\n        -X 'github.com/gorse-io/gorse/cmd/version.Version=$(git describe --tags $(git rev-parse HEAD))' \\\n        -X 'github.com/gorse-io/gorse/cmd/version.GitCommit=$(git rev-parse HEAD)' \\\n        -X 'github.com/gorse-io/gorse/cmd/version.BuildTime=$(date)'\\\"\" ./cmd/...\n\n############################\n# STEP 2 build a small image\n############################\nFROM mcr.microsoft.com/windows/servercore:ltsc2022\n\nCOPY --from=0 /gorse-worker.exe /gorse-worker.exe\n\nRUN /gorse-worker.exe --version\n\nENTRYPOINT [ \"/gorse-worker.exe\" ]\n"
  },
  {
    "path": "cmd/gorse-worker/main.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage main\n\nimport (\n\t\"fmt\"\n\t_ \"net/http/pprof\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/cmd/version\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/common/util\"\n\t\"github.com/gorse-io/gorse/worker\"\n\t\"github.com/spf13/cobra\"\n\t\"go.uber.org/zap\"\n)\n\nvar workerCommand = &cobra.Command{\n\tUse:   \"gorse-worker\",\n\tShort: \"The worker node of gorse recommender system.\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\t// show version\n\t\tshowVersion, _ := cmd.PersistentFlags().GetBool(\"version\")\n\t\tif showVersion {\n\t\t\tfmt.Println(version.BuildInfo())\n\t\t\treturn\n\t\t}\n\t\tmasterHost, _ := cmd.PersistentFlags().GetString(\"master-host\")\n\t\tmasterPort, _ := cmd.PersistentFlags().GetInt(\"master-port\")\n\t\thttpHost, _ := cmd.PersistentFlags().GetString(\"http-host\")\n\t\thttpPort, _ := cmd.PersistentFlags().GetInt(\"http-port\")\n\t\tworkingJobs, _ := cmd.PersistentFlags().GetInt(\"jobs\")\n\t\t// setup logger\n\t\tdebug, _ := cmd.PersistentFlags().GetBool(\"debug\")\n\t\tlog.SetLogger(cmd.PersistentFlags(), debug)\n\t\t// create worker\n\t\tcachePath, _ := cmd.PersistentFlags().GetString(\"cache-path\")\n\t\tcaFile, _ := cmd.PersistentFlags().GetString(\"ssl-ca\")\n\t\tcertFile, _ := cmd.PersistentFlags().GetString(\"ssl-cert\")\n\t\tkeyFile, _ := cmd.PersistentFlags().GetString(\"ssl-key\")\n\t\tvar tlsConfig *util.TLSConfig\n\t\tif caFile != \"\" && certFile != \"\" && keyFile != \"\" {\n\t\t\ttlsConfig = &util.TLSConfig{\n\t\t\t\tSSLCA:   caFile,\n\t\t\t\tSSLCert: certFile,\n\t\t\t\tSSLKey:  keyFile,\n\t\t\t}\n\t\t} else if caFile == \"\" && certFile == \"\" && keyFile == \"\" {\n\t\t\ttlsConfig = nil\n\t\t} else {\n\t\t\tlog.Logger().Fatal(\"incomplete SSL configuration\",\n\t\t\t\tzap.String(\"ssl_ca\", caFile),\n\t\t\t\tzap.String(\"ssl_cert\", certFile),\n\t\t\t\tzap.String(\"ssl_key\", keyFile))\n\t\t}\n\t\tinterval, _ := cmd.PersistentFlags().GetDuration(\"interval\")\n\t\tw := worker.NewWorker(masterHost, masterPort, httpHost, httpPort, workingJobs, cachePath, tlsConfig, interval)\n\t\tw.Serve()\n\t},\n}\n\nfunc init() {\n\tlog.AddFlags(workerCommand.PersistentFlags())\n\tworkerCommand.PersistentFlags().BoolP(\"version\", \"v\", false, \"gorse version\")\n\tworkerCommand.PersistentFlags().String(\"master-host\", \"127.0.0.1\", \"host of master node\")\n\tworkerCommand.PersistentFlags().Int(\"master-port\", 8086, \"port of master node\")\n\tworkerCommand.PersistentFlags().String(\"http-host\", \"127.0.0.1\", \"host for Prometheus metrics export\")\n\tworkerCommand.PersistentFlags().Int(\"http-port\", 8089, \"port for Prometheus metrics export\")\n\tworkerCommand.PersistentFlags().Bool(\"debug\", false, \"use debug log mode\")\n\tworkerCommand.PersistentFlags().IntP(\"jobs\", \"j\", 1, \"number of working jobs.\")\n\tworkerCommand.PersistentFlags().String(\"cache-path\", \"worker_cache.data\", \"path of cache file\")\n\tworkerCommand.PersistentFlags().String(\"ssl-ca\", \"\", \"path of SSL CA\")\n\tworkerCommand.PersistentFlags().String(\"ssl-cert\", \"\", \"path to SSL certificate\")\n\tworkerCommand.PersistentFlags().String(\"ssl-key\", \"\", \"path to SSL key\")\n\tworkerCommand.PersistentFlags().Duration(\"interval\", time.Minute, \"interval between checking users\")\n}\n\nfunc main() {\n\tif err := workerCommand.Execute(); err != nil {\n\t\tlog.Logger().Fatal(\"failed to execute\", zap.Error(err))\n\t}\n}\n"
  },
  {
    "path": "cmd/version/version.go",
    "content": "package version\n\nimport (\n\t\"fmt\"\n\t\"runtime\"\n)\n\n// Default build-time variable.\n// These values are overridden via ldflags\nvar (\n\tVersion    = \"unknown-version\"\n\tGitCommit  = \"unknown-commit\"\n\tBuildTime  = \"unknown-buildtime\"\n\tAPIVersion = \"v0.2.7\"\n)\n\nfunc BuildInfo() string {\n\tvar buildInfo string\n\tbuildInfo += fmt.Sprintln(\"Version:\\t\", Version)\n\tbuildInfo += fmt.Sprintln(\"API version:\\t\", APIVersion)\n\tbuildInfo += fmt.Sprintln(\"Go version:\\t\", runtime.Version())\n\tbuildInfo += fmt.Sprintln(\"Git commit:\\t\", GitCommit)\n\tbuildInfo += fmt.Sprintln(\"Built:\\t\\t\", BuildTime)\n\tbuildInfo += fmt.Sprintf(\"OS/Arch:\\t %s/%s\\n\", runtime.GOOS, runtime.GOARCH)\n\treturn buildInfo\n}\n"
  },
  {
    "path": "codecov.yml",
    "content": "coverage:\n  status:\n    patch:\n      default:\n        enabled: no\n\nignore:\n  - \"protocol/*.pb.go\"\n  - \"cmd/**\"\n"
  },
  {
    "path": "common/ann/ann.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage ann\n\nimport (\n\t\"github.com/samber/lo\"\n)\n\ntype Index interface {\n\tAdd(v []float32) int\n\tSearchIndex(q, k int, prune0 bool) ([]lo.Tuple2[int, float32], error)\n\tSearchVector(q []float32, k int, prune0 bool) []lo.Tuple2[int, float32]\n}\n"
  },
  {
    "path": "common/ann/ann_test.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage ann\n\nimport (\n\t\"bufio\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/gorse-io/gorse/common/datautil\"\n\t\"github.com/gorse-io/gorse/common/floats\"\n\t\"github.com/gorse-io/gorse/common/util\"\n\t\"github.com/samber/lo\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"go.uber.org/atomic\"\n)\n\nconst (\n\ttrainSize = 6000\n\ttestSize  = 1000\n)\n\nfunc recall(gt, pred []lo.Tuple2[int, float32]) float64 {\n\ts := mapset.NewSet[int]()\n\tfor _, pair := range gt {\n\t\ts.Add(pair.A)\n\t}\n\thit := 0\n\tfor _, pair := range pred {\n\t\tif s.Contains(pair.A) {\n\t\t\thit++\n\t\t}\n\t}\n\treturn float64(hit) / float64(len(gt))\n}\n\ntype MNIST struct {\n\tTrainImages [][]float32\n\tTrainLabels []uint8\n\tTestImages  [][]float32\n\tTestLabels  []uint8\n}\n\nfunc mnist() (*MNIST, error) {\n\t// Download and unzip dataset\n\tpath, err := datautil.DownloadAndUnzip(\"mnist\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// Open dataset\n\tm := new(MNIST)\n\tm.TrainImages, m.TrainLabels, err = m.openFile(filepath.Join(path, \"train.libfm\"))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tm.TestImages, m.TestLabels, err = m.openFile(filepath.Join(path, \"test.libfm\"))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn m, nil\n}\n\nfunc (m *MNIST) openFile(path string) ([][]float32, []uint8, error) {\n\t// Open file\n\tf, err := os.Open(path)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tdefer f.Close()\n\t// Read data line by line\n\tvar (\n\t\timages [][]float32\n\t\tlabels []uint8\n\t)\n\tscanner := bufio.NewScanner(f)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tsplits := strings.Split(line, \" \")\n\t\t// Parse label\n\t\tlabel, err := util.ParseUInt[uint8](splits[0])\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\tlabels = append(labels, label)\n\t\t// Parse image\n\t\timage := make([]float32, 784)\n\t\tfor _, split := range splits[1:] {\n\t\t\tkv := strings.Split(split, \":\")\n\t\t\tindex, err := strconv.Atoi(kv[0])\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t\tvalue, err := util.ParseFloat[float32](kv[1])\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t\timage[index] = value\n\t\t}\n\t\timages = append(images, image)\n\t}\n\treturn images, labels, nil\n}\n\nfunc TestMNIST(t *testing.T) {\n\tdat, err := mnist()\n\tassert.NoError(t, err)\n\n\t// Create brute-force index\n\tbf := NewBruteforce(floats.Euclidean)\n\tfor _, image := range dat.TrainImages[:trainSize] {\n\t\t_ = bf.Add(image)\n\t}\n\n\t// Create HNSW index\n\thnsw := NewHNSW(floats.Euclidean)\n\tfor _, image := range dat.TrainImages[:trainSize] {\n\t\t_ = hnsw.Add(image)\n\t}\n\n\t// Test search\n\tr := 0.0\n\tfor _, image := range dat.TestImages[:testSize] {\n\t\tgt := bf.SearchVector(image, 100, false)\n\t\tassert.Len(t, gt, 100)\n\t\tscores := hnsw.SearchVector(image, 100, false)\n\t\tassert.Len(t, scores, 100)\n\t\tr += recall(gt, scores)\n\t}\n\tr /= float64(testSize)\n\tassert.Greater(t, r, 0.99)\n\n\t// Test save and load\n\tpath := filepath.Join(t.TempDir(), \"mnist.bin\")\n\tf, err := os.Create(path)\n\tassert.NoError(t, err)\n\tdefer f.Close()\n\tassert.NoError(t, hnsw.Marshal(f))\n\n\tf, err = os.Open(path)\n\tassert.NoError(t, err)\n\tdefer f.Close()\n\tassert.NoError(t, hnsw.Unmarshal(f))\n\tr = 0\n\tfor _, image := range dat.TestImages[:testSize] {\n\t\tgt := bf.SearchVector(image, 100, false)\n\t\tassert.Len(t, gt, 100)\n\t\tscores := hnsw.SearchVector(image, 100, false)\n\t\tassert.Len(t, scores, 100)\n\t\tr += recall(gt, scores)\n\t}\n\tr /= float64(testSize)\n\tassert.Greater(t, r, 0.99)\n}\n\nfunc TestMultithread(t *testing.T) {\n\tdat, err := mnist()\n\tassert.NoError(t, err)\n\n\t// Create HNSW index\n\tindices := make([]int, trainSize)\n\thnsw := NewHNSW(floats.Euclidean)\n\tvar wg1 sync.WaitGroup\n\twg1.Add(trainSize)\n\tfor i := range dat.TrainImages[:trainSize] {\n\t\tgo func(i int) {\n\t\t\tdefer wg1.Done()\n\t\t\tindices[i] = hnsw.Add(dat.TrainImages[i])\n\t\t}(i)\n\t}\n\twg1.Wait()\n\n\t// Create brute-force index\n\treverse := make([]int, trainSize)\n\tfor i, index := range indices {\n\t\treverse[index] = i\n\t}\n\tbf := NewBruteforce(floats.Euclidean)\n\tfor i := range reverse {\n\t\t_ = bf.Add(dat.TrainImages[reverse[i]])\n\t}\n\n\t// Test search\n\tvar r atomic.Float64\n\tvar wg2 sync.WaitGroup\n\twg2.Add(testSize)\n\tfor _, image := range dat.TestImages[:testSize] {\n\t\tgo func(image []float32) {\n\t\t\tdefer wg2.Done()\n\t\t\tgt := bf.SearchVector(image, 100, false)\n\t\t\tassert.Len(t, gt, 100)\n\t\t\tscores := hnsw.SearchVector(image, 100, false)\n\t\t\tassert.Len(t, scores, 100)\n\t\t\tr.Add(recall(gt, scores))\n\t\t}(image)\n\t}\n\twg2.Wait()\n\tassert.Greater(t, r.Load()/float64(testSize), 0.99)\n}\n\nfunc movieLens() ([][]int, error) {\n\t// Download and unzip dataset\n\tpath, err := datautil.DownloadAndUnzip(\"ml-1m\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// Open file\n\tf, err := os.Open(filepath.Join(path, \"train.txt\"))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer f.Close()\n\t// Read data line by line\n\tmovies := make([][]int, 0)\n\tscanner := bufio.NewScanner(f)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tsplits := strings.Split(line, \"\\t\")\n\t\tuserId, err := strconv.Atoi(splits[0])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tmovieId, err := strconv.Atoi(splits[1])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor movieId >= len(movies) {\n\t\t\tmovies = append(movies, make([]int, 0))\n\t\t}\n\t\tmovies[movieId] = append(movies[movieId], userId)\n\t}\n\treturn movies, nil\n}\n\nfunc jaccard(a, b []int) float32 {\n\tvar i, j, intersection int\n\tfor i < len(a) && j < len(b) {\n\t\tif a[i] == b[j] {\n\t\t\tintersection++\n\t\t\ti++\n\t\t\tj++\n\t\t} else if a[i] < b[j] {\n\t\t\ti++\n\t\t} else {\n\t\t\tj++\n\t\t}\n\t}\n\tif len(a)+len(b)-intersection == 0 {\n\t\treturn 1\n\t}\n\treturn 1 - float32(intersection)/float32(len(a)+len(b)-intersection)\n}\n\nfunc TestMovieLens(t *testing.T) {\n\tmovies, err := movieLens()\n\tassert.NoError(t, err)\n\n\t// Create brute-force index\n\tbf := NewBruteforce(jaccard)\n\tfor _, movie := range movies {\n\t\t_ = bf.Add(movie)\n\t}\n\n\t// Create HNSW index\n\thnsw := NewHNSW(jaccard)\n\tfor _, movie := range movies {\n\t\t_ = hnsw.Add(movie)\n\t}\n\n\t// Test search\n\tr := 0.0\n\tfor i := range movies[:testSize] {\n\t\tgt, err := bf.SearchIndex(i, 100, false)\n\t\tassert.NoError(t, err)\n\t\tscores, err := hnsw.SearchIndex(i, 100, false)\n\t\tassert.NoError(t, err)\n\t\tr += recall(gt, scores)\n\t}\n\tr /= float64(testSize)\n\tassert.Greater(t, r, 0.98)\n}\n"
  },
  {
    "path": "common/ann/bruteforce.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage ann\n\nimport (\n\t\"github.com/gorse-io/gorse/common/heap\"\n\t\"github.com/juju/errors\"\n\t\"github.com/samber/lo\"\n)\n\n// Bruteforce is a naive implementation of vector index.\ntype Bruteforce[T any] struct {\n\tdistanceFunc func(a, b T) float32\n\tvectors      []T\n}\n\nfunc NewBruteforce[T any](distanceFunc func(a, b T) float32) *Bruteforce[T] {\n\treturn &Bruteforce[T]{distanceFunc: distanceFunc}\n}\n\nfunc (b *Bruteforce[T]) Add(v T) int {\n\t// Add vector\n\tb.vectors = append(b.vectors, v)\n\treturn len(b.vectors)\n}\n\nfunc (b *Bruteforce[T]) SearchIndex(q, k int, prune0 bool) ([]lo.Tuple2[int, float32], error) {\n\t// Check index\n\tif q < 0 || q >= len(b.vectors) {\n\t\treturn nil, errors.Errorf(\"index out of range: %v\", q)\n\t}\n\t// Search\n\tpq := heap.NewPriorityQueue(true)\n\tfor i, vec := range b.vectors {\n\t\tif i != q {\n\t\t\tpq.Push(int32(i), b.distanceFunc(b.vectors[q], vec))\n\t\t\tif pq.Len() > k {\n\t\t\t\tpq.Pop()\n\t\t\t}\n\t\t}\n\t}\n\tpq = pq.Reverse()\n\tscores := make([]lo.Tuple2[int, float32], 0)\n\tfor pq.Len() > 0 {\n\t\tvalue, score := pq.Pop()\n\t\tif !prune0 || score > 0 {\n\t\t\tscores = append(scores, lo.Tuple2[int, float32]{A: int(value), B: score})\n\t\t}\n\t}\n\treturn scores, nil\n}\n\nfunc (b *Bruteforce[T]) SearchVector(q T, k int, prune0 bool) []lo.Tuple2[int, float32] {\n\t// Search\n\tpq := heap.NewPriorityQueue(true)\n\tfor i, vec := range b.vectors {\n\t\tpq.Push(int32(i), b.distanceFunc(q, vec))\n\t\tif pq.Len() > k {\n\t\t\tpq.Pop()\n\t\t}\n\t}\n\tpq = pq.Reverse()\n\tscores := make([]lo.Tuple2[int, float32], 0)\n\tfor pq.Len() > 0 {\n\t\tvalue, score := pq.Pop()\n\t\tif !prune0 || score > 0 {\n\t\t\tscores = append(scores, lo.Tuple2[int, float32]{A: int(value), B: score})\n\t\t}\n\t}\n\treturn scores\n}\n"
  },
  {
    "path": "common/ann/hnsw.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage ann\n\nimport (\n\t\"encoding/binary\"\n\t\"io\"\n\t\"math/rand\"\n\t\"sync\"\n\n\t\"github.com/chewxy/math32\"\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/gorse-io/gorse/common/encoding\"\n\t\"github.com/gorse-io/gorse/common/heap\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/samber/lo\"\n\t\"modernc.org/mathutil\"\n)\n\n// HNSW is a vector index based on Hierarchical Navigable Small Worlds.\ntype HNSW[T any] struct {\n\tdistanceFunc    func(a, b T) float32\n\tvectors         []T\n\tbottomNeighbors []*heap.PriorityQueue\n\tupperNeighbors  []sync.Map\n\tenterPoint      int32\n\tinitOnce        sync.Once\n\tindexMutex      sync.Mutex\n\trootMutex       sync.Mutex\n\tbottomMutex     []*sync.RWMutex\n\n\tlevelFactor    float32\n\tmaxConnection  int // maximum number of connections for each element per layer\n\tmaxConnection0 int\n\tef             int\n\tefConstruction int\n}\n\nfunc NewHNSW[T any](distanceFunc func(a, b T) float32) *HNSW[T] {\n\treturn &HNSW[T]{\n\t\tdistanceFunc:   distanceFunc,\n\t\tlevelFactor:    1.0 / math32.Log(48),\n\t\tmaxConnection:  48,\n\t\tmaxConnection0: 96,\n\t\tefConstruction: 100,\n\t}\n}\n\nfunc (h *HNSW[T]) Add(v T) int {\n\t// Add vector\n\th.indexMutex.Lock()\n\th.vectors = append(h.vectors, v)\n\th.bottomNeighbors = append(h.bottomNeighbors, heap.NewPriorityQueue(false))\n\th.bottomMutex = append(h.bottomMutex, new(sync.RWMutex))\n\tq := len(h.vectors) - 1\n\th.indexMutex.Unlock()\n\th.insert(int32(q))\n\treturn q\n}\n\nfunc (h *HNSW[T]) SearchIndex(q, k int, prune0 bool) ([]lo.Tuple2[int, float32], error) {\n\t// Check index\n\tif q < 0 || q >= len(h.vectors) {\n\t\treturn nil, errors.Errorf(\"index out of range: %v\", q)\n\t}\n\tw := h.knnSearch(h.vectors[q], k, h.efSearchValue(k))\n\tscores := make([]lo.Tuple2[int, float32], 0)\n\tfor w.Len() > 0 {\n\t\tvalue, score := w.Pop()\n\t\tif !prune0 || score > 0 {\n\t\t\tscores = append(scores, lo.Tuple2[int, float32]{A: int(value), B: score})\n\t\t}\n\t}\n\treturn scores, nil\n}\n\nfunc (h *HNSW[T]) SearchVector(q T, k int, prune0 bool) []lo.Tuple2[int, float32] {\n\tw := h.knnSearch(q, k, h.efSearchValue(k))\n\tscores := make([]lo.Tuple2[int, float32], 0)\n\tfor w.Len() > 0 {\n\t\tvalue, score := w.Pop()\n\t\tif !prune0 || score > 0 {\n\t\t\tscores = append(scores, lo.Tuple2[int, float32]{A: int(value), B: score})\n\t\t}\n\t}\n\treturn scores\n}\n\nfunc (h *HNSW[T]) knnSearch(q T, k, ef int) *heap.PriorityQueue {\n\tvar (\n\t\tw           *heap.PriorityQueue                    // set for the current the nearest element\n\t\tenterPoints = h.distance(q, []int32{h.enterPoint}) // get enter point for hnsw\n\t\ttopLayer    = len(h.upperNeighbors)                // top layer for hnsw\n\t)\n\tfor currentLayer := topLayer; currentLayer > 0; currentLayer-- {\n\t\tw = h.searchLayer(q, enterPoints, 1, currentLayer)\n\t\tenterPoints = heap.NewPriorityQueue(false)\n\t\tenterPoints.Push(w.Peek())\n\t}\n\tw = h.searchLayer(q, enterPoints, ef, 0)\n\treturn h.selectNeighbors(q, w, k)\n}\n\n// insert i-th vector into the vector index.\nfunc (h *HNSW[T]) insert(q int32) {\n\t// insert first point\n\tvar isFirstPoint bool\n\th.initOnce.Do(func() {\n\t\tif h.upperNeighbors == nil {\n\t\t\th.bottomNeighbors[q] = heap.NewPriorityQueue(false)\n\t\t\th.upperNeighbors = make([]sync.Map, 0)\n\t\t\th.enterPoint = q\n\t\t\tisFirstPoint = true\n\t\t\treturn\n\t\t}\n\t})\n\tif isFirstPoint {\n\t\treturn\n\t}\n\n\th.rootMutex.Lock()\n\tvar (\n\t\tw           *heap.PriorityQueue                               // list for the currently found nearest elements\n\t\tenterPoints = h.distance(h.vectors[q], []int32{h.enterPoint}) // get enter point for hnsw\n\t\tl           = int(math32.Floor(-math32.Log(rand.Float32()) * h.levelFactor))\n\t\ttopLayer    = len(h.upperNeighbors)\n\t)\n\tif l <= topLayer {\n\t\th.rootMutex.Unlock()\n\t} else {\n\t\tdefer h.rootMutex.Unlock()\n\t}\n\n\tfor currentLayer := topLayer; currentLayer >= l+1; currentLayer-- {\n\t\tw = h.searchLayer(h.vectors[q], enterPoints, 1, currentLayer)\n\t\tenterPoints = h.selectNeighbors(h.vectors[q], w, 1)\n\t}\n\n\th.bottomMutex[q].Lock()\n\tfor currentLayer := mathutil.Min(topLayer, l); currentLayer >= 0; currentLayer-- {\n\t\tw = h.searchLayer(h.vectors[q], enterPoints, h.efConstruction, currentLayer)\n\t\tneighbors := h.selectNeighbors(h.vectors[q], w, h.maxConnection)\n\t\t// add bidirectional connections from upperNeighbors to q at layer l_c\n\t\th.setNeighbourhood(q, currentLayer, neighbors)\n\t\tfor _, e := range neighbors.Elems() {\n\t\t\th.bottomMutex[e.Value].Lock()\n\t\t\th.getNeighbourhood(e.Value, currentLayer).Push(q, e.Weight)\n\t\t\tconnections := h.getNeighbourhood(e.Value, currentLayer)\n\t\t\tvar currentMaxConnection int\n\t\t\tif currentLayer == 0 {\n\t\t\t\tcurrentMaxConnection = h.maxConnection0\n\t\t\t} else {\n\t\t\t\tcurrentMaxConnection = h.maxConnection\n\t\t\t}\n\t\t\tif connections.Len() > currentMaxConnection {\n\t\t\t\t// shrink connections of e if lc = 0 then M_max = M_max0\n\t\t\t\tnewConnections := h.selectNeighbors(h.vectors[q], connections, h.maxConnection)\n\t\t\t\th.setNeighbourhood(e.Value, currentLayer, newConnections)\n\t\t\t}\n\t\t\th.bottomMutex[e.Value].Unlock()\n\t\t}\n\t\tenterPoints = w\n\t}\n\th.bottomMutex[q].Unlock()\n\n\tif l > topLayer {\n\t\t// set enter point for hnsw to q\n\t\th.enterPoint = q\n\t\th.upperNeighbors = append(h.upperNeighbors, sync.Map{})\n\t\th.setNeighbourhood(q, topLayer+1, heap.NewPriorityQueue(false))\n\t}\n}\n\nfunc (h *HNSW[T]) searchLayer(q T, enterPoints *heap.PriorityQueue, ef, currentLayer int) *heap.PriorityQueue {\n\tvar (\n\t\tv          = mapset.NewSet(enterPoints.Values()...) // set of visited elements\n\t\tcandidates = enterPoints.Clone()                    // set of candidates\n\t\tw          = enterPoints.Reverse()                  // dynamic list of found nearest upperNeighbors\n\t)\n\tfor candidates.Len() > 0 {\n\t\t// extract nearest element from candidates to q\n\t\tc, cq := candidates.Pop()\n\t\t// get the furthest element from w to q\n\t\t_, fq := w.Peek()\n\n\t\tif cq > fq {\n\t\t\tbreak // all elements in w are evaluated\n\t\t}\n\n\t\t// update candidates and w\n\t\th.bottomMutex[c].RLock()\n\t\tneighbors := h.getNeighbourhood(c, currentLayer).Values()\n\t\th.bottomMutex[c].RUnlock()\n\t\tfor _, e := range neighbors {\n\t\t\tif !v.Contains(e) {\n\t\t\t\tv.Add(e)\n\t\t\t\t// get the furthest element from w to q\n\t\t\t\t_, fq = w.Peek()\n\t\t\t\tif eq := h.distanceFunc(h.vectors[e], q); eq < fq || w.Len() < ef {\n\t\t\t\t\tcandidates.Push(e, eq)\n\t\t\t\t\tw.Push(e, eq)\n\t\t\t\t\tif w.Len() > ef {\n\t\t\t\t\t\t// remove the furthest element from w to q\n\t\t\t\t\t\tw.Pop()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn w.Reverse()\n}\n\nfunc (h *HNSW[T]) setNeighbourhood(e int32, currentLayer int, connections *heap.PriorityQueue) {\n\tif currentLayer == 0 {\n\t\th.bottomNeighbors[e] = connections\n\t} else {\n\t\th.upperNeighbors[currentLayer-1].Store(e, connections)\n\t}\n}\n\nfunc (h *HNSW[T]) getNeighbourhood(e int32, currentLayer int) *heap.PriorityQueue {\n\tif currentLayer == 0 {\n\t\treturn h.bottomNeighbors[e]\n\t} else {\n\t\tif connections, ok := h.upperNeighbors[currentLayer-1].Load(e); ok {\n\t\t\treturn connections.(*heap.PriorityQueue)\n\t\t}\n\t\treturn nil\n\t}\n}\n\nfunc (h *HNSW[T]) selectNeighbors(_ T, candidates *heap.PriorityQueue, m int) *heap.PriorityQueue {\n\tpq := candidates.Reverse()\n\tfor pq.Len() > m {\n\t\tpq.Pop()\n\t}\n\treturn pq.Reverse()\n}\n\nfunc (h *HNSW[T]) distance(q T, points []int32) *heap.PriorityQueue {\n\tpq := heap.NewPriorityQueue(false)\n\tfor _, point := range points {\n\t\tpq.Push(point, h.distanceFunc(h.vectors[point], q))\n\t}\n\treturn pq\n}\n\n// efSearchValue returns the efSearch value to use, given the current number of elements desired.\nfunc (h *HNSW[T]) efSearchValue(n int) int {\n\tif h.ef > 0 {\n\t\treturn mathutil.Max(h.ef, n)\n\t}\n\treturn mathutil.Max(h.efConstruction, n)\n}\n\nfunc (h *HNSW[T]) Marshal(w io.Writer) error {\n\tif err := binary.Write(w, binary.LittleEndian, h.levelFactor); err != nil {\n\t\treturn err\n\t}\n\tif err := binary.Write(w, binary.LittleEndian, int64(h.maxConnection)); err != nil {\n\t\treturn err\n\t}\n\tif err := binary.Write(w, binary.LittleEndian, int64(h.maxConnection0)); err != nil {\n\t\treturn err\n\t}\n\tif err := binary.Write(w, binary.LittleEndian, int64(h.ef)); err != nil {\n\t\treturn err\n\t}\n\tif err := binary.Write(w, binary.LittleEndian, int64(h.efConstruction)); err != nil {\n\t\treturn err\n\t}\n\n\t// save vectors\n\tnumVectors := int64(len(h.vectors))\n\tif err := binary.Write(w, binary.LittleEndian, numVectors); err != nil {\n\t\treturn err\n\t}\n\tfor i := int64(0); i < numVectors; i++ {\n\t\tif err := encoding.WriteGob(w, h.vectors[i]); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// save neighbors\n\tfor i := int64(0); i < numVectors; i++ {\n\t\tif err := h.bottomNeighbors[i].Marshal(w); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tnumLayers := len(h.upperNeighbors)\n\tif err := binary.Write(w, binary.LittleEndian, int64(numLayers)); err != nil {\n\t\treturn err\n\t}\n\tfor i := 0; i < numLayers; i++ {\n\t\tvar elements []lo.Tuple2[int32, *heap.PriorityQueue]\n\t\th.upperNeighbors[i].Range(func(key, value any) bool {\n\t\t\telements = append(elements, lo.Tuple2[int32, *heap.PriorityQueue]{\n\t\t\t\tA: key.(int32), B: value.(*heap.PriorityQueue)})\n\t\t\treturn true\n\t\t})\n\t\tnumElements := int32(len(elements))\n\t\tif err := binary.Write(w, binary.LittleEndian, numElements); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor j := int32(0); j < numElements; j++ {\n\t\t\tif err := binary.Write(w, binary.LittleEndian, elements[j].A); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := elements[j].B.Marshal(w); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\tif err := binary.Write(w, binary.LittleEndian, h.enterPoint); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (h *HNSW[T]) Unmarshal(r io.Reader) error {\n\tif err := binary.Read(r, binary.LittleEndian, &h.levelFactor); err != nil {\n\t\treturn err\n\t}\n\tvar maxConnection int64\n\tif err := binary.Read(r, binary.LittleEndian, &maxConnection); err != nil {\n\t\treturn err\n\t}\n\th.maxConnection = int(maxConnection)\n\tvar maxConnection0 int64\n\tif err := binary.Read(r, binary.LittleEndian, &maxConnection0); err != nil {\n\t\treturn err\n\t}\n\th.maxConnection0 = int(maxConnection0)\n\tvar ef int64\n\tif err := binary.Read(r, binary.LittleEndian, &ef); err != nil {\n\t\treturn err\n\t}\n\th.ef = int(ef)\n\tvar efConstruction int64\n\tif err := binary.Read(r, binary.LittleEndian, &efConstruction); err != nil {\n\t\treturn err\n\t}\n\th.efConstruction = int(efConstruction)\n\n\t// read vectors\n\tvar numVectors int64\n\tif err := binary.Read(r, binary.LittleEndian, &numVectors); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\th.vectors = make([]T, numVectors)\n\tfor i := int64(0); i < numVectors; i++ {\n\t\tif err := encoding.ReadGob(r, &h.vectors[i]); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t}\n\n\t// save neighbors\n\th.bottomNeighbors = make([]*heap.PriorityQueue, numVectors)\n\tfor i := int64(0); i < numVectors; i++ {\n\t\th.bottomNeighbors[i] = heap.NewPriorityQueue(false)\n\t\tif err := h.bottomNeighbors[i].Unmarshal(r); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t}\n\tvar numLayers int64\n\tif err := binary.Read(r, binary.LittleEndian, &numLayers); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\th.upperNeighbors = make([]sync.Map, numLayers)\n\tfor i := int64(0); i < numLayers; i++ {\n\t\tvar numElements int32\n\t\tif err := binary.Read(r, binary.LittleEndian, &numElements); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t\tfor j := int32(0); j < numElements; j++ {\n\t\t\tvar e int32\n\t\t\tif err := binary.Read(r, binary.LittleEndian, &e); err != nil {\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\t\t\tpq := heap.NewPriorityQueue(false)\n\t\t\tif err := pq.Unmarshal(r); err != nil {\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\t\t\th.upperNeighbors[i].Store(e, pq)\n\t\t}\n\t}\n\tif err := binary.Read(r, binary.LittleEndian, &h.enterPoint); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\n\th.bottomMutex = make([]*sync.RWMutex, numVectors)\n\tfor i := int64(0); i < numVectors; i++ {\n\t\th.bottomMutex[i] = new(sync.RWMutex)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "common/blas/blas.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage blas\n\ntype Order int\n\nconst RowMajor Order = 101\n\ntype Transpose int\n\nconst (\n\tNoTrans Transpose = 111\n\tTrans   Transpose = 112\n)\n\nfunc NewTranspose(transpose bool) Transpose {\n\tif transpose {\n\t\treturn Trans\n\t} else {\n\t\treturn NoTrans\n\t}\n}\n"
  },
  {
    "path": "common/blas/blas_darwin_arm64.go",
    "content": "//go:build cgo\n\n// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage blas\n\n// #cgo CFLAGS: -DACCELERATE_NEW_LAPACK\n// #cgo LDFLAGS: -framework Accelerate\n// #include <Accelerate/Accelerate.h>\nimport \"C\"\n\nfunc SGEMM(order Order, transA, transB Transpose, m, n, k int, alpha float32, a []float32, lda int, b []float32, ldb int, beta float32, c []float32, ldc int) {\n\tC.cblas_sgemm(uint32(order), uint32(transA), uint32(transB), C.int(m), C.int(n), C.int(k), C.float(alpha),\n\t\t(*C.float)(&a[0]), C.int(lda), (*C.float)(&b[0]), C.int(ldb), C.float(beta), (*C.float)(&c[0]), C.int(ldc))\n}\n"
  },
  {
    "path": "common/blas/blas_mkl.go",
    "content": "//go:build cgo && mkl\n\n// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage blas\n\n// #cgo CFLAGS: -I/opt/intel/oneapi/mkl/latest/include\n// #cgo LDFLAGS: -L/opt/intel/oneapi/mkl/latest/lib/intel64 -Wl,--start-group -lmkl_intel_lp64 -lmkl_sequential -lmkl_core -Wl,--end-group -lpthread -lm -ldl -static\n// #include \"mkl.h\"\nimport \"C\"\n\nfunc SGEMM(order Order, transA, transB Transpose, m, n, k int, alpha float32, a []float32, lda int, b []float32, ldb int, beta float32, c []float32, ldc int) {\n\tC.cblas_sgemm(C.CBLAS_LAYOUT(order), C.CBLAS_TRANSPOSE(transA), C.CBLAS_TRANSPOSE(transB), C.int(m), C.int(n), C.int(k), C.float(alpha),\n\t\t(*C.float)(&a[0]), C.int(lda), (*C.float)(&b[0]), C.int(ldb), C.float(beta), (*C.float)(&c[0]), C.int(ldc))\n}\n"
  },
  {
    "path": "common/blas/blas_openblas.go",
    "content": "//go:build cgo && openblas\n\n// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage blas\n\n// #cgo LDFLAGS: -lopenblas -lm -pthread -static\n// #include <cblas.h>\nimport \"C\"\n\nfunc SGEMM(order Order, transA, transB Transpose, m, n, k int, alpha float32, a []float32, lda int, b []float32, ldb int, beta float32, c []float32, ldc int) {\n\tC.cblas_sgemm(uint32(order), uint32(transA), uint32(transB), C.int(m), C.int(n), C.int(k), C.float(alpha),\n\t\t(*C.float)(&a[0]), C.int(lda), (*C.float)(&b[0]), C.int(ldb), C.float(beta), (*C.float)(&c[0]), C.int(ldc))\n}\n"
  },
  {
    "path": "common/copier/copier.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage copier\n\nimport (\n\t\"encoding\"\n\t\"reflect\"\n\n\t\"github.com/juju/errors\"\n)\n\nfunc Copy(dst, src interface{}) error {\n\tdstPtr := reflect.ValueOf(dst)\n\tif dstPtr.Kind() != reflect.Ptr {\n\t\treturn errors.NotValidf(\"expect dst to be a pointer, but receive %v\", dstPtr.Kind())\n\t}\n\n\tdstValue := dstPtr.Elem()\n\tsrcValue := reflect.ValueOf(src)\n\treturn copyValue(dstValue, srcValue)\n}\n\nfunc copyValue(dst, src reflect.Value) error {\n\tif dst.Kind() != src.Kind() {\n\t\tif dst.Kind() == reflect.Interface {\n\t\t\tnewValuePointer := reflect.New(src.Type())\n\t\t\terr := copyValue(newValuePointer.Elem(), src)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tdst.Set(newValuePointer.Elem())\n\t\t\treturn nil\n\t\t} else {\n\t\t\treturn errors.NotValidf(\"different type: %v != %v\", dst.Kind(), src.Kind())\n\t\t}\n\t}\n\n\tswitch dst.Kind() {\n\tcase reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint,\n\t\treflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64,\n\t\treflect.Complex64, reflect.Complex128, reflect.String:\n\t\tdst.Set(src)\n\tcase reflect.Slice:\n\t\tif dst.IsNil() || (!dst.CanAddr() && dst.Len() != src.Len()) || dst.Cap() < src.Len() {\n\t\t\tnewSlice := reflect.MakeSlice(src.Type(), src.Len(), src.Len())\n\t\t\tdst.Set(newSlice)\n\t\t} else if dst.CanAddr() {\n\t\t\tdst.SetLen(src.Len())\n\t\t}\n\t\tfor i := 0; i < src.Len(); i++ {\n\t\t\terr := copyValue(dst.Index(i), src.Index(i))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\tcase reflect.Map:\n\t\tif !reflect.DeepEqual(dst.Interface(), src.Interface()) {\n\t\t\tdst.Set(reflect.MakeMap(dst.Type()))\n\t\t\tkeys := src.MapKeys()\n\t\t\tfor _, k := range keys {\n\t\t\t\tvalue := src.MapIndex(k)\n\t\t\t\tnewValuePointer := reflect.New((value).Type())\n\t\t\t\terr := copyValue(newValuePointer.Elem(), src.MapIndex(k))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tdst.SetMapIndex(k, newValuePointer.Elem())\n\t\t\t}\n\t\t}\n\tcase reflect.Struct:\n\t\tif dst.Type().Name() != src.Type().Name() {\n\t\t\treturn errors.NotValidf(\"different struct: %v != %v\", dst.Type().Name(), src.Type().Name())\n\t\t}\n\n\t\tdstPointer := reflect.New(dst.Type())\n\t\tsrcPointer := reflect.New(src.Type())\n\t\tsrcPointer.Elem().Set(src)\n\t\tsrcMarshaller, hasSrcMarshaller := srcPointer.Interface().(encoding.BinaryMarshaler)\n\t\tdstUnmarshaler, hasDstUnmarshaler := dstPointer.Interface().(encoding.BinaryUnmarshaler)\n\n\t\tif hasDstUnmarshaler && hasSrcMarshaller {\n\t\t\tdstByte, err := srcMarshaller.MarshalBinary()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\terr = dstUnmarshaler.UnmarshalBinary(dstByte)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tdst.Set(dstPointer.Elem())\n\t\t} else {\n\t\t\tnumFiled := src.NumField()\n\t\t\tfor i := 0; i < numFiled; i++ {\n\t\t\t\tfieldDST := dst.Field(i)\n\t\t\t\tfieldSRC := src.Field(i)\n\t\t\t\tif !fieldDST.CanSet() {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\terr := copyValue(fieldDST, fieldSRC)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\tcase reflect.Ptr:\n\t\tif src.IsNil() {\n\t\t\t// If source is nil, set dst to nil.\n\t\t\tdst.Set(reflect.Zero(dst.Type()))\n\t\t\treturn nil\n\t\t}\n\t\tif dst.IsNil() {\n\t\t\tdst.Set(reflect.New(src.Elem().Type()))\n\t\t}\n\t\tsrcElem := src.Elem()\n\t\tdstElem := dst.Elem()\n\t\terr := copyValue(dstElem, srcElem)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\tcase reflect.Interface:\n\t\tif src.IsNil() {\n\t\t\t// If source is nil, set dst to nil.\n\t\t\tdst.Set(reflect.Zero(dst.Type()))\n\t\t\treturn nil\n\t\t}\n\t\tif !dst.IsNil() {\n\t\t\tswitch dst.Elem().Kind() {\n\t\t\tcase reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint,\n\t\t\t\treflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64,\n\t\t\t\treflect.Complex64, reflect.Complex128, reflect.String:\n\t\t\t\tnewValuePointer := reflect.New(src.Elem().Type())\n\t\t\t\terr := copyValue(newValuePointer.Elem(), src.Elem())\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tdst.Set(newValuePointer.Elem())\n\t\t\tdefault:\n\t\t\t\terr := copyValue(dst.Elem(), src.Elem())\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tnewValuePointer := reflect.New(src.Elem().Type())\n\t\t\terr := copyValue(newValuePointer.Elem(), src.Elem())\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tdst.Set(newValuePointer.Elem())\n\t\t}\n\tdefault:\n\t\treturn errors.NotValidf(\"unsupported type: %v\", dst.Kind())\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "common/copier/copier_test.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage copier\n\nimport (\n\t\"testing\"\n\n\t\"github.com/juju/errors\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestPrimitives(t *testing.T) {\n\tvar a = 1\n\tvar b int\n\terr := Copy(&b, a)\n\tassert.NoError(t, err)\n\tassert.Equal(t, a, b)\n\t// not a pointer\n\terr = Copy(b, a)\n\tassert.True(t, errors.Is(err, errors.NotValid))\n\t// test type mismatch\n\tvar c bool\n\terr = Copy(&c, a)\n\tassert.True(t, errors.Is(err, errors.NotValid))\n\t// copy to interface\n\tvar d interface{}\n\terr = Copy(&d, a)\n\tassert.NoError(t, err)\n\tassert.Equal(t, a, d)\n}\n\nfunc TestSlice(t *testing.T) {\n\ta := [][]int{{1}, {2}, {3}, {4}}\n\tb := make([][]int, 0)\n\terr := Copy(&b, a)\n\tassert.NoError(t, err)\n\tassert.Equal(t, a, b)\n\t// test deep copy\n\ta[0][0] = 100\n\tassert.Equal(t, 1, b[0][0])\n\t// test reuse memory\n\tvar integers = []int{10}\n\tc := [][]int{integers, {20}, {30}, {40}}\n\terr = Copy(&c, a)\n\tassert.NoError(t, err)\n\tintegers[0] = 100\n\tassert.Equal(t, 100, c[0][0])\n\t// copy to interface\n\tvar d interface{}\n\terr = Copy(&d, a)\n\tassert.NoError(t, err)\n\tassert.Equal(t, a, d)\n\t// copy empty slice\n\tvar e interface{}\n\terr = Copy(&e, make([]int, 0))\n\tassert.NoError(t, err)\n\tassert.NotNil(t, e)\n\t// copy to larger slice\n\tvar f = [][]int{{10}, {20}, {30}, {40}, {50}}\n\terr = Copy(&f, a)\n\tassert.NoError(t, err)\n\tassert.Equal(t, a, f)\n\tassert.Equal(t, 5, cap(f))\n}\n\nfunc TestMap(t *testing.T) {\n\ta := map[int64][]int64{1: {1}, 2: {1}, 3: {1}}\n\tb := map[int64][]int64{4: {100}, 5: {200}, 6: {300}}\n\terr := Copy(&b, a)\n\tassert.NoError(t, err)\n\tassert.Equal(t, a, b)\n\t// test deep copy\n\ta[1][0] = 100\n\tassert.Equal(t, int64(1), b[1][0])\n\t// copy to interface\n\tvar d interface{}\n\terr = Copy(&d, a)\n\tassert.NoError(t, err)\n\tassert.Equal(t, a, d)\n\t// test no copy\n\tvar integers = []int64{100}\n\tc := map[int64][]int64{1: integers, 2: {1}, 3: {1}}\n\terr = Copy(&c, a)\n\tassert.NoError(t, err)\n\tassert.Equal(t, a, c)\n\tintegers[0] = 10\n\tassert.Equal(t, int64(10), c[1][0])\n}\n\ntype Foo struct {\n\tA int64\n\tB []string\n}\n\ntype Bar struct {\n\tA int64\n}\n\nfunc TestStruct(t *testing.T) {\n\ta := Foo{A: 3, B: []string{\"3\"}}\n\tb := Foo{A: 4, B: []string{\"4\"}}\n\terr := Copy(&b, a)\n\tassert.NoError(t, err)\n\tassert.Equal(t, a, b)\n\t// test deep copy\n\ta.B[0] = \"100\"\n\tassert.Equal(t, \"3\", b.B[0])\n\t// test type mismatch\n\tvar c Bar\n\terr = Copy(&c, a)\n\tassert.True(t, errors.Is(err, errors.NotValid))\n}\n\nfunc TestPtr(t *testing.T) {\n\ta := &Foo{A: 3, B: []string{\"3\"}}\n\tb := &Foo{A: 4, B: []string{\"4\"}}\n\terr := Copy(&b, a)\n\tassert.NoError(t, err)\n\tassert.Equal(t, a, b)\n}\n\nfunc TestInterface(t *testing.T) {\n\tvar a = []interface{}{&Foo{A: 3, B: []string{\"3\"}}, []int{100}, 1}\n\tvar b []interface{}\n\terr := Copy(&b, a)\n\tassert.NoError(t, err)\n\tassert.Equal(t, a, b)\n\t// test reuse memory\n\tvar strings = []string{\"30\"}\n\tvar c = []interface{}{&Foo{A: 30, B: strings}, []int{1000}, 10}\n\terr = Copy(&c, a)\n\tassert.NoError(t, err)\n\tassert.Equal(t, a, c)\n\tstrings[0] = \"123\"\n\tassert.Equal(t, \"123\", c[0].(*Foo).B[0])\n}\n\ntype PrivateStruct struct {\n\ttext *string\n}\n\nfunc (ps *PrivateStruct) MarshalBinary() (data []byte, err error) {\n\treturn []byte(*ps.text), nil\n}\n\nfunc (ps *PrivateStruct) UnmarshalBinary(data []byte) error {\n\tps.text = new(string(data))\n\treturn nil\n}\n\nfunc TestPrivate(t *testing.T) {\n\tvar a = PrivateStruct{new(\"hello\")}\n\tvar b PrivateStruct\n\terr := Copy(&b, a)\n\tassert.NoError(t, err)\n\tassert.Equal(t, a, b)\n\t// test deep copy\n\t*a.text = \"world\"\n\tassert.Equal(t, \"hello\", *b.text)\n}\n\ntype NilInterface interface{}\n\ntype NilStruct struct {\n\tInterface NilInterface\n\tPointer   *Foo\n}\n\nfunc TestNil(t *testing.T) {\n\tvar d = NilStruct{\n\t\tInterface: 100,\n\t\tPointer:   &Foo{A: 100},\n\t}\n\tvar e NilStruct\n\terr := Copy(&d, e)\n\tassert.NoError(t, err)\n\tassert.Nil(t, d.Interface)\n\tassert.Nil(t, d.Pointer)\n}\n"
  },
  {
    "path": "common/datautil/datautil.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage datautil\n\nimport (\n\t\"archive/zip\"\n\t\"encoding/csv\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/user\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/common/util\"\n\t\"go.uber.org/zap\"\n)\n\nvar (\n\ttempDir    string\n\tdatasetDir string\n)\n\nfunc init() {\n\tusr, err := user.Current()\n\tif err != nil {\n\t\tlog.Logger().Fatal(\"failed to get user directory\", zap.Error(err))\n\t}\n\tdatasetDir = filepath.Join(usr.HomeDir, \".gorse\", \"dataset\")\n\ttempDir = filepath.Join(usr.HomeDir, \".gorse\", \"temp\")\n}\n\nfunc LoadIris() ([][]float32, []int, error) {\n\t// Download dataset\n\tpath, err := DownloadAndUnzip(\"iris\")\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tdataFile := filepath.Join(path, \"iris.data\")\n\t// Load data\n\tf, err := os.Open(dataFile)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\treader := csv.NewReader(f)\n\trows, err := reader.ReadAll()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\t// Parse data\n\tdata := make([][]float32, len(rows))\n\ttarget := make([]int, len(rows))\n\ttypes := make(map[string]int)\n\tfor i, row := range rows {\n\t\tdata[i] = make([]float32, 4)\n\t\tfor j, cell := range row[:4] {\n\t\t\tdata[i][j], err = util.ParseFloat[float32](cell)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t}\n\t\tif _, exist := types[row[4]]; !exist {\n\t\t\ttypes[row[4]] = len(types)\n\t\t}\n\t\ttarget[i] = types[row[4]]\n\t}\n\treturn data, target, nil\n}\n\nfunc DownloadAndUnzip(name string) (string, error) {\n\turl := fmt.Sprintf(\"https://cdn.gorse.io/datasets/%s.zip\", name)\n\tpath := filepath.Join(datasetDir, name)\n\tif _, err := os.Stat(path); os.IsNotExist(err) {\n\t\tzipFileName, _ := downloadFromUrl(url, tempDir)\n\t\tif _, err := unzip(zipFileName, datasetDir); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\treturn path, nil\n}\n\n// downloadFromUrl downloads file from URL.\nfunc downloadFromUrl(src, dst string) (string, error) {\n\tlog.Logger().Info(\"Download dataset\", zap.String(\"source\", src), zap.String(\"destination\", dst))\n\t// Extract file name\n\ttokens := strings.Split(src, \"/\")\n\tfileName := filepath.Join(dst, tokens[len(tokens)-1])\n\t// Create file\n\tif err := os.MkdirAll(filepath.Dir(fileName), os.ModePerm); err != nil {\n\t\treturn fileName, err\n\t}\n\toutput, err := os.Create(fileName)\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to create file\", zap.Error(err), zap.String(\"filename\", fileName))\n\t\treturn fileName, err\n\t}\n\tdefer output.Close()\n\t// Download file\n\tresponse, err := http.Get(src)\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to download\", zap.Error(err), zap.String(\"source\", src))\n\t\treturn fileName, err\n\t}\n\tdefer response.Body.Close()\n\t// Save file\n\t_, err = io.Copy(output, response.Body)\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to download\", zap.Error(err), zap.String(\"source\", src))\n\t\treturn fileName, err\n\t}\n\treturn fileName, nil\n}\n\n// unzip zip file.\nfunc unzip(src, dst string) ([]string, error) {\n\tvar fileNames []string\n\t// Open zip file\n\tr, err := zip.OpenReader(src)\n\tif err != nil {\n\t\treturn fileNames, err\n\t}\n\tdefer r.Close()\n\t// Extract files\n\tfor _, f := range r.File {\n\t\t// Open file\n\t\trc, err := f.Open()\n\t\tif err != nil {\n\t\t\treturn fileNames, err\n\t\t}\n\t\t// Store filename/path for returning and using later on\n\t\tfilePath := filepath.Join(dst, f.Name)\n\t\t// Check for ZipSlip. More Info: http://bit.ly/2MsjAWE\n\t\tif !strings.HasPrefix(filePath, filepath.Clean(dst)+string(os.PathSeparator)) {\n\t\t\treturn fileNames, fmt.Errorf(\"%s: illegal file path\", filePath)\n\t\t}\n\t\t// Add filename\n\t\tfileNames = append(fileNames, filePath)\n\t\tif f.FileInfo().IsDir() {\n\t\t\t// Create folder\n\t\t\tif err = os.MkdirAll(filePath, os.ModePerm); err != nil {\n\t\t\t\treturn fileNames, err\n\t\t\t}\n\t\t} else {\n\t\t\t// Create all folders\n\t\t\tif err = os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {\n\t\t\t\treturn fileNames, err\n\t\t\t}\n\t\t\t// Create file\n\t\t\toutFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())\n\t\t\tif err != nil {\n\t\t\t\treturn fileNames, err\n\t\t\t}\n\t\t\t// Save file\n\t\t\t_, err = io.Copy(outFile, rc)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\t// Close the file without defer to close before next iteration of loop\n\t\t\terr = outFile.Close()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\t// Close file\n\t\terr = rc.Close()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn fileNames, nil\n}\n"
  },
  {
    "path": "common/datautil/datautil_test.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage datautil\n\nimport (\n\t\"github.com/stretchr/testify/assert\"\n\t\"testing\"\n)\n\nfunc TestLoadIris(t *testing.T) {\n\tdata, target, err := LoadIris()\n\tassert.NoError(t, err)\n\tassert.Len(t, data, 150)\n\tassert.Len(t, data[0], 4)\n\tassert.Len(t, target, 150)\n}\n"
  },
  {
    "path": "common/encoding/encoding.go",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage encoding\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"encoding/gob\"\n\t\"github.com/pkg/errors\"\n\t\"io\"\n)\n\n// WriteSlice writes matrix to byte stream.\nfunc WriteSlice[T any](w io.Writer, s []T) error {\n\tif err := binary.Write(w, binary.LittleEndian, int32(len(s))); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\treturn binary.Write(w, binary.LittleEndian, s)\n}\n\n// ReadSlice reads matrix from byte stream.\nfunc ReadSlice[T any](r io.Reader, s *[]T) error {\n\tvar length int32\n\tif err := binary.Read(r, binary.LittleEndian, &length); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\t*s = make([]T, length)\n\treturn binary.Read(r, binary.LittleEndian, *s)\n}\n\n// WriteString writes string to byte stream.\nfunc WriteString(w io.Writer, s string) error {\n\treturn WriteBytes(w, []byte(s))\n}\n\n// ReadString reads string from byte stream.\nfunc ReadString(r io.Reader) (string, error) {\n\tdata, err := ReadBytes(r)\n\treturn string(data), err\n}\n\n// WriteBytes writes bytes to byte stream.\nfunc WriteBytes(w io.Writer, s []byte) error {\n\terr := binary.Write(w, binary.LittleEndian, int32(len(s)))\n\tif err != nil {\n\t\treturn err\n\t}\n\tn, err := w.Write(s)\n\tif err != nil {\n\t\treturn err\n\t} else if n != len(s) {\n\t\treturn errors.New(\"fail to write string\")\n\t}\n\treturn nil\n}\n\n// ReadBytes reads bytes from byte stream.\nfunc ReadBytes(r io.Reader) ([]byte, error) {\n\tvar length int32\n\terr := binary.Read(r, binary.LittleEndian, &length)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdata := make([]byte, length)\n\treadCount := 0\n\tfor {\n\t\tn, err := r.Read(data[readCount:])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treadCount += n\n\t\tif readCount == len(data) {\n\t\t\treturn data, nil\n\t\t} else if n == 0 {\n\t\t\treturn nil, errors.New(\"fail to read string\")\n\t\t}\n\t}\n}\n\n// WriteGob writes object to byte stream.\nfunc WriteGob(w io.Writer, v interface{}) error {\n\tbuffer := bytes.NewBuffer(nil)\n\tencoder := gob.NewEncoder(buffer)\n\terr := encoder.Encode(v)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn WriteBytes(w, buffer.Bytes())\n}\n\n// ReadGob read object from byte stream.\nfunc ReadGob(r io.Reader, v interface{}) error {\n\tdata, err := ReadBytes(r)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbuffer := bytes.NewBuffer(data)\n\tdecoder := gob.NewDecoder(buffer)\n\treturn decoder.Decode(v)\n}\n"
  },
  {
    "path": "common/encoding/encoding_test.go",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage encoding\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestReadWriteSlice(t *testing.T) {\n\ta := []float32{1, 2, 3, 4}\n\tbuf := bytes.NewBuffer(nil)\n\terr := WriteSlice(buf, a)\n\tassert.NoError(t, err)\n\tvar b []float32\n\terr = ReadSlice(buf, &b)\n\tassert.NoError(t, err)\n\tassert.Equal(t, a, b)\n}\n\nfunc TestWriteString(t *testing.T) {\n\ta := \"abc\"\n\tbuf := bytes.NewBuffer(nil)\n\terr := WriteString(buf, a)\n\tassert.NoError(t, err)\n\tvar b string\n\tb, err = ReadString(buf)\n\tassert.NoError(t, err)\n\tassert.Equal(t, a, b)\n}\n\nfunc TestWriteGob(t *testing.T) {\n\ta := \"abc\"\n\tbuf := bytes.NewBuffer(nil)\n\terr := WriteGob(buf, a)\n\tassert.NoError(t, err)\n\tvar b string\n\terr = ReadGob(buf, &b)\n\tassert.NoError(t, err)\n\tassert.Equal(t, a, b)\n}\n"
  },
  {
    "path": "common/expression/expression.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage expression\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strconv\"\n\n\t\"github.com/gorse-io/gorse/protocol\"\n\t\"github.com/samber/lo\"\n)\n\nvar expressionPattern = regexp.MustCompile(`^(?P<feedback_type>[a-zA-Z][a-zA-Z0-9_]*)(?P<expr_type><=|>=|<|>|=)?(?P<value>[0-9]*\\.?[0-9]*)$`)\n\ntype ExprType int\n\nconst (\n\tNone ExprType = iota\n\tLess\n\tLessOrEqual\n\tGreater\n\tGreaterOrEqual\n)\n\nfunc (typ ExprType) String() string {\n\tswitch typ {\n\tcase Less:\n\t\treturn \"<\"\n\tcase LessOrEqual:\n\t\treturn \"<=\"\n\tcase Greater:\n\t\treturn \">\"\n\tcase GreaterOrEqual:\n\t\treturn \">=\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\ntype FeedbackTypeExpression struct {\n\tFeedbackType string\n\tExprType     ExprType\n\tValue        float64\n}\n\nfunc (f *FeedbackTypeExpression) String() string {\n\tif f.ExprType == None {\n\t\treturn f.FeedbackType\n\t} else {\n\t\treturn fmt.Sprintf(\"%s%v%v\", f.FeedbackType, f.ExprType, f.Value)\n\t}\n}\n\nfunc (f *FeedbackTypeExpression) FromString(data string) error {\n\tgroupNames := expressionPattern.SubexpNames()\n\tsubMatches := expressionPattern.FindStringSubmatch(data)\n\tif len(subMatches) == 0 {\n\t\treturn errors.New(\"invalid expression format, expected format: <feedback_type>[<operator><value>]\")\n\t}\n\tfor i, match := range subMatches {\n\t\tswitch groupNames[i] {\n\t\tcase \"feedback_type\":\n\t\t\tf.FeedbackType = match\n\t\tcase \"expr_type\":\n\t\t\tswitch match {\n\t\t\tcase \"<\":\n\t\t\t\tf.ExprType = Less\n\t\t\tcase \"<=\":\n\t\t\t\tf.ExprType = LessOrEqual\n\t\t\tcase \">\":\n\t\t\t\tf.ExprType = Greater\n\t\t\tcase \">=\":\n\t\t\t\tf.ExprType = GreaterOrEqual\n\t\t\tdefault:\n\t\t\t\tf.ExprType = None\n\t\t\t}\n\t\tcase \"value\":\n\t\t\tif len(match) > 0 {\n\t\t\t\tvar err error\n\t\t\t\tf.Value, err = strconv.ParseFloat(match, 64)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"invalid value: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (f *FeedbackTypeExpression) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(f.String())\n}\n\nfunc (f *FeedbackTypeExpression) UnmarshalJSON(data []byte) error {\n\tvar s string\n\tif err := json.Unmarshal(data, &s); err != nil {\n\t\treturn fmt.Errorf(\"unmarshal FeedbackTypeExpression: %w\", err)\n\t}\n\tif err := f.FromString(s); err != nil {\n\t\treturn fmt.Errorf(\"unmarshal FeedbackTypeExpression: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (f *FeedbackTypeExpression) ToPB() *protocol.FeedbackTypeExpression {\n\tpb := &protocol.FeedbackTypeExpression{}\n\tpb.FeedbackType = f.FeedbackType\n\tswitch f.ExprType {\n\tcase None:\n\t\tpb.ExpressionType = protocol.ExpressionType_None\n\tcase Less:\n\t\tpb.ExpressionType = protocol.ExpressionType_Less\n\tcase LessOrEqual:\n\t\tpb.ExpressionType = protocol.ExpressionType_LessOrEqual\n\tcase Greater:\n\t\tpb.ExpressionType = protocol.ExpressionType_Greater\n\tcase GreaterOrEqual:\n\t\tpb.ExpressionType = protocol.ExpressionType_GreaterOrEqual\n\t}\n\tpb.Value = f.Value\n\treturn pb\n}\n\nfunc (f *FeedbackTypeExpression) FromPB(pb *protocol.FeedbackTypeExpression) {\n\tf.FeedbackType = pb.FeedbackType\n\tswitch pb.ExpressionType {\n\tcase protocol.ExpressionType_None:\n\t\tf.ExprType = None\n\tcase protocol.ExpressionType_Less:\n\t\tf.ExprType = Less\n\tcase protocol.ExpressionType_LessOrEqual:\n\t\tf.ExprType = LessOrEqual\n\tcase protocol.ExpressionType_Greater:\n\t\tf.ExprType = Greater\n\tcase protocol.ExpressionType_GreaterOrEqual:\n\t\tf.ExprType = GreaterOrEqual\n\t}\n\tf.Value = pb.Value\n}\n\nfunc (f *FeedbackTypeExpression) Match(feedbackType string, value float64) bool {\n\tif f.FeedbackType != feedbackType {\n\t\treturn false\n\t}\n\tswitch f.ExprType {\n\tcase None:\n\t\treturn true\n\tcase Less:\n\t\treturn value < f.Value\n\tcase LessOrEqual:\n\t\treturn value <= f.Value\n\tcase Greater:\n\t\treturn value > f.Value\n\tcase GreaterOrEqual:\n\t\treturn value >= f.Value\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc MatchFeedbackTypeExpressions(exprs []FeedbackTypeExpression, feedbackType string, value float64) bool {\n\tfor _, expr := range exprs {\n\t\tif expr.Match(feedbackType, value) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc MustParseFeedbackTypeExpression(s string) FeedbackTypeExpression {\n\tvar expr FeedbackTypeExpression\n\tlo.Must0(expr.FromString(s))\n\treturn expr\n}\n"
  },
  {
    "path": "common/expression/expression_test.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage expression\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gorse-io/gorse/protocol\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestFeedbackTypeExpression_UnmarshalJSON(t *testing.T) {\n\tvar f FeedbackTypeExpression\n\terr := f.UnmarshalJSON([]byte(`\"test\"`))\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"test\", f.FeedbackType)\n\tassert.Equal(t, None, f.ExprType)\n\n\terr = f.UnmarshalJSON([]byte(`\"1a\"`))\n\tassert.Error(t, err)\n\n\terr = f.UnmarshalJSON([]byte(`\"test<16\"`))\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"test\", f.FeedbackType)\n\tassert.Equal(t, Less, f.ExprType)\n\tassert.Equal(t, 16.0, f.Value)\n\n\terr = f.UnmarshalJSON([]byte(`\"test<=16\"`))\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"test\", f.FeedbackType)\n\tassert.Equal(t, LessOrEqual, f.ExprType)\n\tassert.Equal(t, 16.0, f.Value)\n\n\terr = f.UnmarshalJSON([]byte(`\"test>16\"`))\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"test\", f.FeedbackType)\n\tassert.Equal(t, Greater, f.ExprType)\n\tassert.Equal(t, 16.0, f.Value)\n\n\terr = f.UnmarshalJSON([]byte(`\"test>=16\"`))\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"test\", f.FeedbackType)\n\tassert.Equal(t, GreaterOrEqual, f.ExprType)\n\tassert.Equal(t, 16.0, f.Value)\n}\n\nfunc TestFeedbackTypeExpression_MarshalJSON(t *testing.T) {\n\tf := FeedbackTypeExpression{FeedbackType: \"test\", Value: 16}\n\tbuf, err := f.MarshalJSON()\n\tassert.NoError(t, err)\n\tassert.Equal(t, `\"test\"`, string(buf))\n\n\tf.ExprType = Less\n\tbuf, err = f.MarshalJSON()\n\tassert.NoError(t, err)\n\tassert.Equal(t, `\"test\\u003c16\"`, string(buf))\n\n\tf.ExprType = LessOrEqual\n\tbuf, err = f.MarshalJSON()\n\tassert.NoError(t, err)\n\tassert.Equal(t, `\"test\\u003c=16\"`, string(buf))\n\n\tf.ExprType = Greater\n\tbuf, err = f.MarshalJSON()\n\tassert.NoError(t, err)\n\tassert.Equal(t, `\"test\\u003e16\"`, string(buf))\n\n\tf.ExprType = GreaterOrEqual\n\tbuf, err = f.MarshalJSON()\n\tassert.NoError(t, err)\n\tassert.Equal(t, `\"test\\u003e=16\"`, string(buf))\n}\n\nfunc TestFeedbackTypeExpression_ToPB(t *testing.T) {\n\tf := FeedbackTypeExpression{FeedbackType: \"test\", Value: 6}\n\tpb := f.ToPB()\n\tassert.Equal(t, \"test\", pb.FeedbackType)\n\tassert.Equal(t, protocol.ExpressionType_None, pb.ExpressionType)\n\tassert.Equal(t, 6.0, pb.Value)\n\n\tf.ExprType = Less\n\tpb = f.ToPB()\n\tassert.Equal(t, protocol.ExpressionType_Less, pb.ExpressionType)\n\n\tf.ExprType = LessOrEqual\n\tpb = f.ToPB()\n\tassert.Equal(t, protocol.ExpressionType_LessOrEqual, pb.ExpressionType)\n\n\tf.ExprType = Greater\n\tpb = f.ToPB()\n\tassert.Equal(t, protocol.ExpressionType_Greater, pb.ExpressionType)\n\n\tf.ExprType = GreaterOrEqual\n\tpb = f.ToPB()\n\tassert.Equal(t, protocol.ExpressionType_GreaterOrEqual, pb.ExpressionType)\n}\n\nfunc TestFeedbackTypeExpression_FromPB(t *testing.T) {\n\tpb := &protocol.FeedbackTypeExpression{\n\t\tFeedbackType:   \"test\",\n\t\tExpressionType: protocol.ExpressionType_Less,\n\t\tValue:          6.0,\n\t}\n\tf := FeedbackTypeExpression{}\n\tf.FromPB(pb)\n\tassert.Equal(t, \"test\", f.FeedbackType)\n\tassert.Equal(t, Less, f.ExprType)\n\tassert.Equal(t, 6.0, f.Value)\n\n\tpb.ExpressionType = protocol.ExpressionType_LessOrEqual\n\tf.FromPB(pb)\n\tassert.Equal(t, LessOrEqual, f.ExprType)\n\n\tpb.ExpressionType = protocol.ExpressionType_Greater\n\tf.FromPB(pb)\n\tassert.Equal(t, Greater, f.ExprType)\n\n\tpb.ExpressionType = protocol.ExpressionType_GreaterOrEqual\n\tf.FromPB(pb)\n\tassert.Equal(t, GreaterOrEqual, f.ExprType)\n\n\tpb.ExpressionType = protocol.ExpressionType_None\n\tf.FromPB(pb)\n\tassert.Equal(t, None, f.ExprType)\n}\n\nfunc TestFeedbackTypeExpression_Match(t *testing.T) {\n\tf := FeedbackTypeExpression{FeedbackType: \"test\", Value: 6}\n\tassert.True(t, f.Match(\"test\", 0))\n\tassert.False(t, f.Match(\"a\", 1))\n\n\tf.ExprType = Less\n\tassert.True(t, f.Match(\"test\", 5))\n\tassert.False(t, f.Match(\"test\", 6))\n\n\tf.ExprType = LessOrEqual\n\tassert.True(t, f.Match(\"test\", 6))\n\tassert.False(t, f.Match(\"test\", 7))\n\n\tf.ExprType = Greater\n\tassert.True(t, f.Match(\"test\", 7))\n\tassert.False(t, f.Match(\"test\", 6))\n\n\tf.ExprType = GreaterOrEqual\n\tassert.True(t, f.Match(\"test\", 6))\n\tassert.False(t, f.Match(\"test\", 5))\n}\n\nfunc TestMatchFeedbackTypeExpressions(t *testing.T) {\n\texpressions := []FeedbackTypeExpression{\n\t\t{FeedbackType: \"a\"},\n\t\t{FeedbackType: \"b\"},\n\t\t{FeedbackType: \"c\"},\n\t}\n\tassert.True(t, MatchFeedbackTypeExpressions(expressions, \"a\", 0))\n\tassert.False(t, MatchFeedbackTypeExpressions(expressions, \"d\", 0))\n}\n\nfunc TestMustParseFeedbackTypeExpression(t *testing.T) {\n\tassert.Panics(t, func() {\n\t\tMustParseFeedbackTypeExpression(\"test+\")\n\t})\n}\n"
  },
  {
    "path": "common/floats/floats.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage floats\n\nimport (\n\t\"math\"\n\n\t\"github.com/chewxy/math32\"\n)\n\nfunc dot(a, b []float32) (ret float32) {\n\tfor i := range a {\n\t\tret += a[i] * b[i]\n\t}\n\treturn\n}\n\nfunc euclidean(a, b []float32) (ret float32) {\n\tfor i := range a {\n\t\tret += (a[i] - b[i]) * (a[i] - b[i])\n\t}\n\treturn math32.Sqrt(ret)\n}\n\nfunc addConst(a []float32, c float32) {\n\tfor i := range a {\n\t\ta[i] += c\n\t}\n}\n\nfunc sub(a, b []float32) {\n\tfor i := range a {\n\t\ta[i] -= b[i]\n\t}\n}\n\nfunc subTo(a, b, c []float32) {\n\tfor i := range a {\n\t\tc[i] = a[i] - b[i]\n\t}\n}\n\nfunc mulTo(a, b, c []float32) {\n\tfor i := range a {\n\t\tc[i] = a[i] * b[i]\n\t}\n}\n\nfunc mulConstAddTo(a []float32, b float32, c []float32, dst []float32) {\n\tfor i := range a {\n\t\tdst[i] = a[i]*b + c[i]\n\t}\n}\n\nfunc mulConstAdd(a []float32, c float32, dst []float32) {\n\tfor i := range a {\n\t\tdst[i] += a[i] * c\n\t}\n}\n\nfunc mulConstTo(a []float32, b float32, c []float32) {\n\tfor i := range a {\n\t\tc[i] = a[i] * b\n\t}\n}\n\nfunc mulConst(a []float32, b float32) {\n\tfor i := range a {\n\t\ta[i] *= b\n\t}\n}\n\nfunc divTo(a, b, c []float32) {\n\tfor i := range a {\n\t\tc[i] = a[i] / b[i]\n\t}\n}\n\nfunc sqrtTo(a, b []float32) {\n\tfor i := range a {\n\t\tb[i] = math32.Sqrt(a[i])\n\t}\n}\n\n// MatZero fills zeros in a matrix of 32-bit floats.\nfunc MatZero(x [][]float32) {\n\tfor i := range x {\n\t\tfor j := range x[i] {\n\t\t\tx[i][j] = 0\n\t\t}\n\t}\n}\n\n// Zero fills zeros in a slice of 32-bit floats.\nfunc Zero(a []float32) {\n\tfor i := range a {\n\t\ta[i] = 0\n\t}\n}\n\n// SubTo subtracts one vector by another and saves the result in dst: dst = a - b\nfunc SubTo(a, b, dst []float32) {\n\tif len(dst) != len(b) || len(a) != len(b) {\n\t\tpanic(\"floats: slice lengths do not match\")\n\t}\n\tfeature.subTo(a, b, dst)\n}\n\n// Add two vectors: dst = dst + s\nfunc Add(dst, s []float32) {\n\tif len(dst) != len(s) {\n\t\tpanic(\"floats: slice lengths do not match\")\n\t}\n\tfor i := range dst {\n\t\tdst[i] += s[i]\n\t}\n}\n\n// MulConst multiplies a vector with a const: dst = dst * c\nfunc MulConst(dst []float32, c float32) {\n\tfeature.mulConst(dst, c)\n}\n\n// Div one vectors by another: dst = dst / s\nfunc Div(dst, s []float32) {\n\tif len(dst) != len(s) {\n\t\tpanic(\"floats: slice lengths do not match\")\n\t}\n\tfor i := range dst {\n\t\tdst[i] /= s[i]\n\t}\n}\n\nfunc MulTo(a, b, c []float32) {\n\tif len(a) != len(b) || len(a) != len(c) {\n\t\tpanic(\"floats: slice lengths do not match\")\n\t}\n\tfeature.mulTo(a, b, c)\n}\n\n// Sub one vector by another: dst = dst - s\nfunc Sub(dst, s []float32) {\n\tif len(dst) != len(s) {\n\t\tpanic(\"floats: slice lengths do not match\")\n\t}\n\tfeature.sub(dst, s)\n}\n\n// MulConstTo multiplies a vector and a const, then saves the result in dst: dst = a * c\nfunc MulConstTo(a []float32, c float32, dst []float32) {\n\tif len(a) != len(dst) {\n\t\tpanic(\"floats: slice lengths do not match\")\n\t}\n\tfeature.mulConstTo(a, c, dst)\n}\n\nfunc MulConstAddTo(a []float32, c float32, b, dst []float32) {\n\tif len(a) != len(b) || len(a) != len(dst) {\n\t\tpanic(\"floats: slice lengths do not match\")\n\t}\n\tfeature.mulConstAddTo(a, c, b, dst)\n}\n\n// MulConstAddTo multiplies a vector and a const, then adds to dst: dst = dst + a * c\nfunc MulConstAdd(a []float32, c float32, dst []float32) {\n\tif len(a) != len(dst) {\n\t\tpanic(\"floats: slice lengths do not match\")\n\t}\n\tfeature.mulConstAdd(a, c, dst)\n}\n\n// MulAddTo multiplies a vector and a vector, then adds to a vector: c += a * b\nfunc MulAddTo(a, b, c []float32) {\n\tif len(a) != len(b) || len(a) != len(c) {\n\t\tpanic(\"floats: slice lengths do not match\")\n\t}\n\tfor i := range a {\n\t\tc[i] += a[i] * b[i]\n\t}\n}\n\n// AddTo adds two vectors and saves the result in dst: dst = a + b\nfunc AddTo(a, b, dst []float32) {\n\tif len(a) != len(b) || len(a) != len(dst) {\n\t\tpanic(\"floats: slice lengths do not match\")\n\t}\n\tfor i := range a {\n\t\tdst[i] = a[i] + b[i]\n\t}\n}\n\nfunc AddConst(dst []float32, c float32) {\n\tfeature.addConst(dst, c)\n}\n\nfunc DivTo(a, b, c []float32) {\n\tif len(a) != len(b) || len(a) != len(c) {\n\t\tpanic(\"floats: slice lengths do not match\")\n\t}\n\tfeature.divTo(a, b, c)\n}\n\nfunc SqrtTo(a, b []float32) {\n\tif len(a) != len(b) {\n\t\tpanic(\"floats: slice lengths do not match\")\n\t}\n\tfeature.sqrtTo(a, b)\n}\n\nfunc Sqrt(a []float32) {\n\tfor i := range a {\n\t\ta[i] = float32(math.Sqrt(float64(a[i])))\n\t}\n}\n\n// Dot two vectors.\nfunc Dot(a, b []float32) (ret float32) {\n\tif len(a) != len(b) {\n\t\tpanic(\"floats: slice lengths do not match\")\n\t}\n\treturn feature.dot(a, b)\n}\n\nfunc Euclidean(a, b []float32) float32 {\n\tif len(a) != len(b) {\n\t\tpanic(\"floats: slice lengths do not match\")\n\t}\n\treturn feature.euclidean(a, b)\n}\n\nfunc MM(transA, transB bool, m, n, k int, a []float32, lda int, b []float32, ldb int, c []float32, ldc int) {\n\tfeature.mm(transA, transB, m, n, k, a, lda, b, ldb, c, ldc)\n}\n"
  },
  {
    "path": "common/floats/floats_amd64.go",
    "content": "//go:build !noasm\n\n// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage floats\n\nimport (\n\t\"strings\"\n\t\"unsafe\"\n\n\t\"golang.org/x/sys/cpu\"\n)\n\n//go:generate goat src/floats_avx.c -O3 -mavx\n//go:generate goat src/floats_avx512.c -O3 -mavx -mfma -mavx512f\n\ntype Feature uint64\n\nconst (\n\tAVX Feature = 1 << iota\n\tFMA\n\tAVX512F\n\tMKL\n\tOPENBLAS\n)\n\nconst AVX512 = AVX | FMA | AVX512F\n\nvar feature Feature\n\nfunc init() {\n\tif cpu.X86.HasAVX {\n\t\tfeature = feature | AVX\n\t}\n\tif cpu.X86.HasFMA {\n\t\tfeature = feature | FMA\n\t}\n\tif cpu.X86.HasAVX512F {\n\t\tfeature = feature | AVX512F\n\t}\n}\n\nfunc (feature Feature) String() string {\n\tvar features []string\n\tif feature&AVX512 == AVX512 {\n\t\tfeatures = append(features, \"AVX512\")\n\t} else if feature&AVX == AVX {\n\t\tfeatures = append(features, \"AVX\")\n\t}\n\tif len(features) == 0 {\n\t\treturn \"AMD64\"\n\t}\n\treturn strings.Join(features, \"+\")\n}\n\nfunc (feature Feature) mulConstAddTo(a []float32, b float32, c []float32, dst []float32) {\n\tif feature&AVX512 == AVX512 {\n\t\t_mm512_mul_const_add_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b), unsafe.Pointer(&c[0]), unsafe.Pointer(&dst[0]), int64(len(a)))\n\t} else if feature&AVX == AVX {\n\t\t_mm256_mul_const_add_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b), unsafe.Pointer(&c[0]), unsafe.Pointer(&dst[0]), int64(len(a)))\n\t} else {\n\t\tmulConstAddTo(a, b, c, dst)\n\t}\n}\n\nfunc (feature Feature) mulConstAdd(a []float32, b float32, c []float32) {\n\tif feature&AVX512 == AVX512 {\n\t\t_mm512_mul_const_add(unsafe.Pointer(&a[0]), unsafe.Pointer(&b), unsafe.Pointer(&c[0]), int64(len(a)))\n\t} else if feature&AVX == AVX {\n\t\t_mm256_mul_const_add(unsafe.Pointer(&a[0]), unsafe.Pointer(&b), unsafe.Pointer(&c[0]), int64(len(a)))\n\t} else {\n\t\tmulConstAdd(a, b, c)\n\t}\n}\n\nfunc (feature Feature) mulConstTo(a []float32, b float32, c []float32) {\n\tif feature&AVX512 == AVX512 {\n\t\t_mm512_mul_const_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b), unsafe.Pointer(&c[0]), int64(len(a)))\n\t} else if feature&AVX == AVX {\n\t\t_mm256_mul_const_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b), unsafe.Pointer(&c[0]), int64(len(a)))\n\t} else {\n\t\tmulConstTo(a, b, c)\n\t}\n}\n\nfunc (feature Feature) addConst(a []float32, b float32) {\n\tif feature&AVX512 == AVX512 {\n\t\t_mm512_add_const(unsafe.Pointer(&a[0]), unsafe.Pointer(&b), int64(len(a)))\n\t} else if feature&AVX == AVX {\n\t\t_mm256_add_const(unsafe.Pointer(&a[0]), unsafe.Pointer(&b), int64(len(a)))\n\t} else {\n\t\taddConst(a, b)\n\t}\n}\n\nfunc (feature Feature) sub(a, b []float32) {\n\tif feature&AVX512 == AVX512 {\n\t\t_mm512_sub(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), int64(len(a)))\n\t} else if feature&AVX == AVX {\n\t\t_mm256_sub(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), int64(len(a)))\n\t} else {\n\t\tsub(a, b)\n\t}\n}\n\nfunc (feature Feature) subTo(a, b, c []float32) {\n\tif feature&AVX512 == AVX512 {\n\t\t_mm512_sub_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), unsafe.Pointer(&c[0]), int64(len(a)))\n\t} else if feature&AVX == AVX {\n\t\t_mm256_sub_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), unsafe.Pointer(&c[0]), int64(len(a)))\n\t} else {\n\t\tsubTo(a, b, c)\n\t}\n}\n\nfunc (feature Feature) mulTo(a, b, c []float32) {\n\tif feature&AVX512 == AVX512 {\n\t\t_mm512_mul_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), unsafe.Pointer(&c[0]), int64(len(a)))\n\t} else if feature&AVX == AVX {\n\t\t_mm256_mul_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), unsafe.Pointer(&c[0]), int64(len(a)))\n\t} else {\n\t\tmulTo(a, b, c)\n\t}\n}\n\nfunc (feature Feature) mulConst(a []float32, b float32) {\n\tif feature&AVX512 == AVX512 {\n\t\t_mm512_mul_const(unsafe.Pointer(&a[0]), unsafe.Pointer(&b), int64(len(a)))\n\t} else if feature&AVX == AVX {\n\t\t_mm256_mul_const(unsafe.Pointer(&a[0]), unsafe.Pointer(&b), int64(len(a)))\n\t} else {\n\t\tmulConst(a, b)\n\t}\n}\n\nfunc (feature Feature) divTo(a, b, c []float32) {\n\tif feature&AVX512 == AVX512 {\n\t\t_mm512_div_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), unsafe.Pointer(&c[0]), int64(len(a)))\n\t} else if feature&AVX == AVX {\n\t\t_mm256_div_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), unsafe.Pointer(&c[0]), int64(len(a)))\n\t} else {\n\t\tdivTo(a, b, c)\n\t}\n}\n\nfunc (feature Feature) sqrtTo(a, b []float32) {\n\tif feature&AVX512 == AVX512 {\n\t\t_mm512_sqrt_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), int64(len(a)))\n\t} else if feature&AVX == AVX {\n\t\t_mm256_sqrt_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), int64(len(a)))\n\t} else {\n\t\tsqrtTo(a, b)\n\t}\n}\n\nfunc (feature Feature) dot(a, b []float32) float32 {\n\tif feature&AVX512 == AVX512 {\n\t\treturn _mm512_dot(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), int64(len(a)))\n\t} else if feature&AVX == AVX {\n\t\treturn _mm256_dot(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), int64(len(a)))\n\t} else {\n\t\treturn dot(a, b)\n\t}\n}\n\nfunc (feature Feature) euclidean(a, b []float32) float32 {\n\tif feature&AVX512 == AVX512 {\n\t\treturn _mm512_euclidean(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), int64(len(a)))\n\t} else if feature&AVX == AVX {\n\t\treturn _mm256_euclidean(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), int64(len(a)))\n\t} else {\n\t\treturn euclidean(a, b)\n\t}\n}\n\nfunc (feature Feature) mm(transA, transB bool, m, n, k int, a []float32, lda int, b []float32, ldb int, c []float32, ldc int) {\n\t// Bypass AVX512 optimizations when MKL or OpenBLAS is enabled.\n\tif feature&AVX512 == AVX512 && feature&MKL == 0 && feature&OPENBLAS == 0 {\n\t\t_mm512_mm(transA, transB, int64(m), int64(n), int64(k), unsafe.Pointer(&a[0]), int64(lda), unsafe.Pointer(&b[0]), int64(ldb), unsafe.Pointer(&c[0]), int64(ldc))\n\t} else if feature&AVX == AVX && feature&MKL == 0 && feature&OPENBLAS == 0 {\n\t\t_mm256_mm(transA, transB, int64(m), int64(n), int64(k), unsafe.Pointer(&a[0]), int64(lda), unsafe.Pointer(&b[0]), int64(ldb), unsafe.Pointer(&c[0]), int64(ldc))\n\t} else {\n\t\tmm(transA, transB, m, n, k, a, lda, b, ldb, c, ldc)\n\t}\n}\n"
  },
  {
    "path": "common/floats/floats_amd64_test.go",
    "content": "//go:build !noasm\n\n// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage floats\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/suite\"\n\t\"golang.org/x/sys/cpu\"\n)\n\nvar supportedFeatures []Feature\n\nfunc init() {\n\tsupportedFeatures = []Feature{0}\n\tif cpu.X86.HasAVX {\n\t\tsupportedFeatures = append(supportedFeatures, AVX)\n\t}\n\tif cpu.X86.HasAVX && cpu.X86.HasFMA && cpu.X86.HasAVX512F {\n\t\tsupportedFeatures = append(supportedFeatures, AVX512)\n\t}\n}\n\nfunc TestAVX(t *testing.T) {\n\tsuite.Run(t, &SIMDTestSuite{Feature: AVX})\n}\n\nfunc TestAVX512(t *testing.T) {\n\tsuite.Run(t, &SIMDTestSuite{Feature: AVX512})\n}\n\nfunc initializeFloat32Array(n int) []float32 {\n\tx := make([]float32, n)\n\tfor i := 0; i < n; i++ {\n\t\tx[i] = rand.Float32()\n\t}\n\treturn x\n}\n\nfunc BenchmarkDot(b *testing.B) {\n\tfor _, feat := range supportedFeatures {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.dot(v1, v2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkEuclidean(b *testing.B) {\n\tfor _, feat := range supportedFeatures {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.euclidean(v1, v2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkMulConstAddTo(b *testing.B) {\n\tfor _, feat := range supportedFeatures {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tv3 := make([]float32, i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.mulConstAddTo(v1, 2, v2, v3)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkMulConstAdd(b *testing.B) {\n\tfor _, feat := range supportedFeatures {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.mulConstAdd(v1, 2, v2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkMulConst(b *testing.B) {\n\tfor _, feat := range supportedFeatures {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.mulConst(v1, 2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkMulConstTo(b *testing.B) {\n\tfor _, feat := range supportedFeatures {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.mulConstTo(v1, 2, v2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkAddConst(b *testing.B) {\n\tfor _, feat := range supportedFeatures {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.addConst(v1, 2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkSub(b *testing.B) {\n\tfor _, feat := range supportedFeatures {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.sub(v1, v2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkSubTo(b *testing.B) {\n\tfor _, feat := range supportedFeatures {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tv3 := make([]float32, i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.subTo(v1, v2, v3)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkMulTo(b *testing.B) {\n\tfor _, feat := range supportedFeatures {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tv3 := make([]float32, i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.mulTo(v1, v2, v3)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkDivTo(b *testing.B) {\n\tfor _, feat := range supportedFeatures {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tv3 := make([]float32, i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.divTo(v1, v2, v3)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkSqrtTo(b *testing.B) {\n\tfor _, feat := range supportedFeatures {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := make([]float32, i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.sqrtTo(v1, v2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkMM(b *testing.B) {\n\tfor _, transA := range []bool{false, true} {\n\t\tfor _, transB := range []bool{false, true} {\n\t\t\tfor _, feat := range supportedFeatures {\n\t\t\t\tb.Run(fmt.Sprintf(\"(%v,%v,%v)\", transA, transB, feat.String()), func(b *testing.B) {\n\t\t\t\t\tfor n := 16; n <= 128; n *= 2 {\n\t\t\t\t\t\tb.Run(strconv.Itoa(n), func(b *testing.B) {\n\t\t\t\t\t\t\tmatA := initializeFloat32Array(n * n)\n\t\t\t\t\t\t\tmatB := initializeFloat32Array(n * n)\n\t\t\t\t\t\t\tmatC := make([]float32, n*n)\n\t\t\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\t\t\tfeat.mm(transA, transB, n, n, n, matA, n, matB, n, matC, n)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "common/floats/floats_arm64.go",
    "content": "//go:build !noasm\n\n// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage floats\n\nimport (\n\t\"strings\"\n\t\"unsafe\"\n)\n\n//go:generate goat src/floats_neon.c -O3\n\ntype Feature uint64\n\nconst (\n\tAMX Feature = 1 << iota // Apple matrix extension\n\tOPENBLAS\n)\n\nvar feature Feature\n\nfunc (feature Feature) String() string {\n\tvar features = []string{\"ARM64\"}\n\tif feature&AMX > 0 {\n\t\tfeatures = append(features, \"AMX\")\n\t}\n\treturn strings.Join(features, \"+\")\n}\n\nfunc (feature Feature) mulConstAddTo(a []float32, b float32, c, dst []float32) {\n\tvmul_const_add_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b), unsafe.Pointer(&c[0]), unsafe.Pointer(&dst[0]), int64(len(a)))\n}\n\nfunc (feature Feature) mulConstAdd(a []float32, b float32, c []float32) {\n\tvmul_const_add(unsafe.Pointer(&a[0]), unsafe.Pointer(&b), unsafe.Pointer(&c[0]), int64(len(a)))\n}\n\nfunc (feature Feature) mulConstTo(a []float32, b float32, c []float32) {\n\tvmul_const_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b), unsafe.Pointer(&c[0]), int64(len(a)))\n}\n\nfunc (feature Feature) addConst(a []float32, b float32) {\n\tvadd_const(unsafe.Pointer(&a[0]), unsafe.Pointer(&b), int64(len(a)))\n}\n\nfunc (feature Feature) sub(a, b []float32) {\n\tvsub(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), int64(len(a)))\n}\n\nfunc (feature Feature) subTo(a, b, c []float32) {\n\tvsub_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), unsafe.Pointer(&c[0]), int64(len(a)))\n}\n\nfunc (feature Feature) mulTo(a, b, c []float32) {\n\tvmul_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), unsafe.Pointer(&c[0]), int64(len(a)))\n}\n\nfunc (feature Feature) divTo(a, b, c []float32) {\n\tvdiv_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), unsafe.Pointer(&c[0]), int64(len(a)))\n}\n\nfunc (feature Feature) sqrtTo(a, b []float32) {\n\tvsqrt_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), int64(len(a)))\n}\n\nfunc (feature Feature) mulConst(a []float32, b float32) {\n\tvmul_const(unsafe.Pointer(&a[0]), unsafe.Pointer(&b), int64(len(a)))\n}\n\nfunc (feature Feature) dot(a, b []float32) float32 {\n\treturn vdot(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), int64(len(a)))\n}\n\nfunc (feature Feature) euclidean(a, b []float32) float32 {\n\treturn veuclidean(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), int64(len(a)))\n}\n\nfunc (feature Feature) mm(transA, transB bool, m, n, k int, a []float32, lda int, b []float32, ldb int, c []float32, ldc int) {\n\tif feature&AMX == AMX || feature&OPENBLAS == OPENBLAS {\n\t\tmm(transA, transB, m, n, k, a, lda, b, ldb, c, ldc)\n\t} else {\n\t\tvmm(transA, transB, int64(m), int64(n), int64(k), unsafe.Pointer(&a[0]), int64(lda), unsafe.Pointer(&b[0]), int64(ldb), unsafe.Pointer(&c[0]), int64(ldc))\n\t}\n}\n"
  },
  {
    "path": "common/floats/floats_arm64_test.go",
    "content": "//go:build !noasm\n\n// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage floats\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/suite\"\n)\n\nfunc TestASIMD(t *testing.T) {\n\tsuite.Run(t, &SIMDTestSuite{})\n}\n\nfunc TestAMX(t *testing.T) {\n\tif runtime.GOOS != \"darwin\" || runtime.GOARCH != \"arm64\" {\n\t\tt.Skip(\"AMX is only supported on macOS ARM64\")\n\t}\n\tsuite.Run(t, &SIMDTestSuite{Feature: AMX})\n}\n\nfunc initializeFloat32Array(n int) []float32 {\n\tx := make([]float32, n)\n\tfor i := 0; i < n; i++ {\n\t\tx[i] = rand.Float32()\n\t}\n\treturn x\n}\n\nfunc BenchmarkDot(b *testing.B) {\n\tfor _, feat := range []Feature{0} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.dot(v1, v2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkEuclidean(b *testing.B) {\n\tfor _, feat := range []Feature{0} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.euclidean(v1, v2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkMulConstAddTo(b *testing.B) {\n\tfor _, feat := range []Feature{0} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tv3 := make([]float32, i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.mulConstAddTo(v1, 2, v2, v3)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkMulConstAdd(b *testing.B) {\n\tfor _, feat := range []Feature{0} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.mulConstAdd(v1, 2, v2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkMulConstTo(b *testing.B) {\n\tfor _, feat := range []Feature{0} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.mulConstTo(v1, 2, v2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkAddConst(b *testing.B) {\n\tfor _, feat := range []Feature{0} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.addConst(v1, 2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkMulConst(b *testing.B) {\n\tfor _, feat := range []Feature{0} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.mulConst(v1, 2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkSubTo(b *testing.B) {\n\tfor _, feat := range []Feature{0} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tv3 := make([]float32, i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.subTo(v1, v2, v3)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkSub(b *testing.B) {\n\tfor _, feat := range []Feature{0} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.sub(v1, v2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkMulTo(b *testing.B) {\n\tfor _, feat := range []Feature{0} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tv3 := make([]float32, i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.mulTo(v1, v2, v3)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkDivTo(b *testing.B) {\n\tfor _, feat := range []Feature{0} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tv3 := make([]float32, i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.divTo(v1, v2, v3)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkSqrtTo(b *testing.B) {\n\tfor _, feat := range []Feature{0} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := make([]float32, i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.sqrtTo(v1, v2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkMM(b *testing.B) {\n\tvar feats = []Feature{0}\n\tif runtime.GOOS == \"darwin\" && runtime.GOARCH == \"arm64\" {\n\t\tfeats = append(feats, AMX)\n\t}\n\tfor _, transA := range []bool{false, true} {\n\t\tfor _, transB := range []bool{false, true} {\n\t\t\tfor _, feat := range feats {\n\t\t\t\tb.Run(fmt.Sprintf(\"(%v,%v,%v)\", transA, transB, feat.String()), func(b *testing.B) {\n\t\t\t\t\tfor n := 16; n <= 128; n *= 2 {\n\t\t\t\t\t\tb.Run(strconv.Itoa(n), func(b *testing.B) {\n\t\t\t\t\t\t\tmatA := initializeFloat32Array(n * n)\n\t\t\t\t\t\t\tmatB := initializeFloat32Array(n * n)\n\t\t\t\t\t\t\tmatC := make([]float32, n*n)\n\t\t\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\t\t\tfeat.mm(transA, transB, n, n, n, matA, n, matB, n, matC, n)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "common/floats/floats_avx.go",
    "content": "//go:build !noasm && amd64\n// Code generated by GoAT. DO NOT EDIT.\n// versions:\n// \tclang   19.1.7 (++20250114103320+cd708029e0b2-1~exp1~20250114103432.75)\n// \tobjdump 2.38\n// flags: -mavx -O3\n// source: src/floats_avx.c\n\npackage floats\n\nimport \"unsafe\"\n\n//go:noescape\nfunc _mm256_mul_const_add_to(a, b, c, dst unsafe.Pointer, n int64)\n\n//go:noescape\nfunc _mm256_mul_const_add(a, b, c unsafe.Pointer, n int64)\n\n//go:noescape\nfunc _mm256_mul_const_to(a, b, c unsafe.Pointer, n int64)\n\n//go:noescape\nfunc _mm256_mul_const(a, b unsafe.Pointer, n int64)\n\n//go:noescape\nfunc _mm256_add_const(a, b unsafe.Pointer, n int64)\n\n//go:noescape\nfunc _mm256_sub_to(a, b, c unsafe.Pointer, n int64)\n\n//go:noescape\nfunc _mm256_sub(a, b unsafe.Pointer, n int64)\n\n//go:noescape\nfunc _mm256_mul_to(a, b, c unsafe.Pointer, n int64)\n\n//go:noescape\nfunc _mm256_div_to(a, b, c unsafe.Pointer, n int64)\n\n//go:noescape\nfunc _mm256_sqrt_to(a, b unsafe.Pointer, n int64)\n\n//go:noescape\nfunc _mm256_dot(a, b unsafe.Pointer, n int64) (result float32)\n\n//go:noescape\nfunc _mm256_euclidean(a, b unsafe.Pointer, n int64) (result float32)\n\n//go:noescape\nfunc _mm256_mm(transA, transB bool, m, n, k int64, a unsafe.Pointer, lda int64, b unsafe.Pointer, ldb int64, c unsafe.Pointer, ldc int64)\n"
  },
  {
    "path": "common/floats/floats_avx.s",
    "content": "//go:build !noasm && amd64\n// Code generated by GoAT. DO NOT EDIT.\n// versions:\n// \tclang   19.1.7 (++20250114103320+cd708029e0b2-1~exp1~20250114103432.75)\n// \tobjdump 2.38\n// flags: -mavx -O3\n// source: src/floats_avx.c\n\nTEXT ·_mm256_mul_const_add_to(SB), $0-40\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ c+16(FP), DX\n\tMOVQ dst+24(FP), CX\n\tMOVQ n+32(FP), R8\n\tBYTE $0x55                                 // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5                   // movq\t%rsp, %rbp\n\tLONG $0xf8e48348                           // andq\t$-8, %rsp\n\tLONG $0x07488d4d                           // leaq\t7(%r8), %r9\n\tWORD $0x854d; BYTE $0xc0                   // testq\t%r8, %r8\n\tLONG $0xc8490f4d                           // cmovnsq\t%r8, %r9\n\tWORD $0x894c; BYTE $0xc8                   // movq\t%r9, %rax\n\tLONG $0x03f8c148                           // sarq\t$3, %rax\n\tLONG $0xf8e18349                           // andq\t$-8, %r9\n\tWORD $0x294d; BYTE $0xc8                   // subq\t%r9, %r8\n\tWORD $0xc085                               // testl\t%eax, %eax\n\tJLE  LBB0_6\n\tWORD $0xf883; BYTE $0x01                   // cmpl\t$1, %eax\n\tJE   LBB0_4\n\tWORD $0x8941; BYTE $0xc1                   // movl\t%eax, %r9d\n\tLONG $0xfee18141; WORD $0xffff; BYTE $0x7f // andl\t$2147483646, %r9d               # imm = 0x7FFFFFFE\n\nLBB0_3:\n\tLONG $0x187de2c4; BYTE $0x06 // vbroadcastss\t(%rsi), %ymm0\n\tLONG $0x0759fcc5             // vmulps\t(%rdi), %ymm0, %ymm0\n\tLONG $0x0258fcc5             // vaddps\t(%rdx), %ymm0, %ymm0\n\tLONG $0x0111fcc5             // vmovups\t%ymm0, (%rcx)\n\tLONG $0x187de2c4; BYTE $0x06 // vbroadcastss\t(%rsi), %ymm0\n\tLONG $0x4759fcc5; BYTE $0x20 // vmulps\t32(%rdi), %ymm0, %ymm0\n\tLONG $0x4258fcc5; BYTE $0x20 // vaddps\t32(%rdx), %ymm0, %ymm0\n\tLONG $0x4111fcc5; BYTE $0x20 // vmovups\t%ymm0, 32(%rcx)\n\tLONG $0x40c78348             // addq\t$64, %rdi\n\tLONG $0x40c28348             // addq\t$64, %rdx\n\tLONG $0x40c18348             // addq\t$64, %rcx\n\tLONG $0xfec18341             // addl\t$-2, %r9d\n\tJNE  LBB0_3\n\nLBB0_4:\n\tWORD $0x01a8                 // testb\t$1, %al\n\tJE   LBB0_6\n\tLONG $0x187de2c4; BYTE $0x06 // vbroadcastss\t(%rsi), %ymm0\n\tLONG $0x0759fcc5             // vmulps\t(%rdi), %ymm0, %ymm0\n\tLONG $0x0258fcc5             // vaddps\t(%rdx), %ymm0, %ymm0\n\tLONG $0x0111fcc5             // vmovups\t%ymm0, (%rcx)\n\tLONG $0x20c78348             // addq\t$32, %rdi\n\tLONG $0x20c28348             // addq\t$32, %rdx\n\tLONG $0x20c18348             // addq\t$32, %rcx\n\nLBB0_6:\n\tWORD $0x854d; BYTE $0xc0     // testq\t%r8, %r8\n\tJLE  LBB0_14\n\tWORD $0x8944; BYTE $0xc0     // movl\t%r8d, %eax\n\tLONG $0x0710fac5             // vmovss\t(%rdi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x0258fac5             // vaddss\t(%rdx), %xmm0, %xmm0\n\tLONG $0x0111fac5             // vmovss\t%xmm0, (%rcx)\n\tLONG $0x01f88348             // cmpq\t$1, %rax\n\tJE   LBB0_14\n\tLONG $0x4710fac5; BYTE $0x04 // vmovss\t4(%rdi), %xmm0                  # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4258fac5; BYTE $0x04 // vaddss\t4(%rdx), %xmm0, %xmm0\n\tLONG $0x4111fac5; BYTE $0x04 // vmovss\t%xmm0, 4(%rcx)\n\tWORD $0xf883; BYTE $0x02     // cmpl\t$2, %eax\n\tJE   LBB0_14\n\tLONG $0x4710fac5; BYTE $0x08 // vmovss\t8(%rdi), %xmm0                  # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4258fac5; BYTE $0x08 // vaddss\t8(%rdx), %xmm0, %xmm0\n\tLONG $0x4111fac5; BYTE $0x08 // vmovss\t%xmm0, 8(%rcx)\n\tWORD $0xf883; BYTE $0x03     // cmpl\t$3, %eax\n\tJE   LBB0_14\n\tLONG $0x4710fac5; BYTE $0x0c // vmovss\t12(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4258fac5; BYTE $0x0c // vaddss\t12(%rdx), %xmm0, %xmm0\n\tLONG $0x4111fac5; BYTE $0x0c // vmovss\t%xmm0, 12(%rcx)\n\tWORD $0xf883; BYTE $0x04     // cmpl\t$4, %eax\n\tJE   LBB0_14\n\tLONG $0x4710fac5; BYTE $0x10 // vmovss\t16(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4258fac5; BYTE $0x10 // vaddss\t16(%rdx), %xmm0, %xmm0\n\tLONG $0x4111fac5; BYTE $0x10 // vmovss\t%xmm0, 16(%rcx)\n\tWORD $0xf883; BYTE $0x05     // cmpl\t$5, %eax\n\tJE   LBB0_14\n\tLONG $0x4710fac5; BYTE $0x14 // vmovss\t20(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4258fac5; BYTE $0x14 // vaddss\t20(%rdx), %xmm0, %xmm0\n\tLONG $0x4111fac5; BYTE $0x14 // vmovss\t%xmm0, 20(%rcx)\n\tWORD $0xf883; BYTE $0x06     // cmpl\t$6, %eax\n\tJE   LBB0_14\n\tLONG $0x4710fac5; BYTE $0x18 // vmovss\t24(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4258fac5; BYTE $0x18 // vaddss\t24(%rdx), %xmm0, %xmm0\n\tLONG $0x4111fac5; BYTE $0x18 // vmovss\t%xmm0, 24(%rcx)\n\nLBB0_14:\n\tWORD $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tRET\n\nTEXT ·_mm256_mul_const_add(SB), $0-32\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ c+16(FP), DX\n\tMOVQ n+24(FP), CX\n\tBYTE $0x55                                 // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5                   // movq\t%rsp, %rbp\n\tLONG $0xf8e48348                           // andq\t$-8, %rsp\n\tLONG $0x07418d4c                           // leaq\t7(%rcx), %r8\n\tWORD $0x8548; BYTE $0xc9                   // testq\t%rcx, %rcx\n\tLONG $0xc1490f4c                           // cmovnsq\t%rcx, %r8\n\tWORD $0x894c; BYTE $0xc0                   // movq\t%r8, %rax\n\tLONG $0x03f8c148                           // sarq\t$3, %rax\n\tLONG $0xf8e08349                           // andq\t$-8, %r8\n\tWORD $0x294c; BYTE $0xc1                   // subq\t%r8, %rcx\n\tWORD $0xc085                               // testl\t%eax, %eax\n\tJLE  LBB1_6\n\tWORD $0xf883; BYTE $0x01                   // cmpl\t$1, %eax\n\tJE   LBB1_4\n\tWORD $0x8941; BYTE $0xc0                   // movl\t%eax, %r8d\n\tLONG $0xfee08141; WORD $0xffff; BYTE $0x7f // andl\t$2147483646, %r8d               # imm = 0x7FFFFFFE\n\nLBB1_3:\n\tLONG $0x187de2c4; BYTE $0x06 // vbroadcastss\t(%rsi), %ymm0\n\tLONG $0x0759fcc5             // vmulps\t(%rdi), %ymm0, %ymm0\n\tLONG $0x0258fcc5             // vaddps\t(%rdx), %ymm0, %ymm0\n\tLONG $0x0211fcc5             // vmovups\t%ymm0, (%rdx)\n\tLONG $0x187de2c4; BYTE $0x06 // vbroadcastss\t(%rsi), %ymm0\n\tLONG $0x4759fcc5; BYTE $0x20 // vmulps\t32(%rdi), %ymm0, %ymm0\n\tLONG $0x4258fcc5; BYTE $0x20 // vaddps\t32(%rdx), %ymm0, %ymm0\n\tLONG $0x4211fcc5; BYTE $0x20 // vmovups\t%ymm0, 32(%rdx)\n\tLONG $0x40c78348             // addq\t$64, %rdi\n\tLONG $0x40c28348             // addq\t$64, %rdx\n\tLONG $0xfec08341             // addl\t$-2, %r8d\n\tJNE  LBB1_3\n\nLBB1_4:\n\tWORD $0x01a8                 // testb\t$1, %al\n\tJE   LBB1_6\n\tLONG $0x187de2c4; BYTE $0x06 // vbroadcastss\t(%rsi), %ymm0\n\tLONG $0x0759fcc5             // vmulps\t(%rdi), %ymm0, %ymm0\n\tLONG $0x0258fcc5             // vaddps\t(%rdx), %ymm0, %ymm0\n\tLONG $0x0211fcc5             // vmovups\t%ymm0, (%rdx)\n\tLONG $0x20c78348             // addq\t$32, %rdi\n\tLONG $0x20c28348             // addq\t$32, %rdx\n\nLBB1_6:\n\tWORD $0x8548; BYTE $0xc9     // testq\t%rcx, %rcx\n\tJLE  LBB1_14\n\tWORD $0xc889                 // movl\t%ecx, %eax\n\tLONG $0x0710fac5             // vmovss\t(%rdi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x0258fac5             // vaddss\t(%rdx), %xmm0, %xmm0\n\tLONG $0x0211fac5             // vmovss\t%xmm0, (%rdx)\n\tLONG $0x01f88348             // cmpq\t$1, %rax\n\tJE   LBB1_14\n\tLONG $0x4710fac5; BYTE $0x04 // vmovss\t4(%rdi), %xmm0                  # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4258fac5; BYTE $0x04 // vaddss\t4(%rdx), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x04 // vmovss\t%xmm0, 4(%rdx)\n\tWORD $0xf883; BYTE $0x02     // cmpl\t$2, %eax\n\tJE   LBB1_14\n\tLONG $0x4710fac5; BYTE $0x08 // vmovss\t8(%rdi), %xmm0                  # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4258fac5; BYTE $0x08 // vaddss\t8(%rdx), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x08 // vmovss\t%xmm0, 8(%rdx)\n\tWORD $0xf883; BYTE $0x03     // cmpl\t$3, %eax\n\tJE   LBB1_14\n\tLONG $0x4710fac5; BYTE $0x0c // vmovss\t12(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4258fac5; BYTE $0x0c // vaddss\t12(%rdx), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x0c // vmovss\t%xmm0, 12(%rdx)\n\tWORD $0xf883; BYTE $0x04     // cmpl\t$4, %eax\n\tJE   LBB1_14\n\tLONG $0x4710fac5; BYTE $0x10 // vmovss\t16(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4258fac5; BYTE $0x10 // vaddss\t16(%rdx), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x10 // vmovss\t%xmm0, 16(%rdx)\n\tWORD $0xf883; BYTE $0x05     // cmpl\t$5, %eax\n\tJE   LBB1_14\n\tLONG $0x4710fac5; BYTE $0x14 // vmovss\t20(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4258fac5; BYTE $0x14 // vaddss\t20(%rdx), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x14 // vmovss\t%xmm0, 20(%rdx)\n\tWORD $0xf883; BYTE $0x06     // cmpl\t$6, %eax\n\tJE   LBB1_14\n\tLONG $0x4710fac5; BYTE $0x18 // vmovss\t24(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4258fac5; BYTE $0x18 // vaddss\t24(%rdx), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x18 // vmovss\t%xmm0, 24(%rdx)\n\nLBB1_14:\n\tWORD $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tRET\n\nTEXT ·_mm256_mul_const_to(SB), $0-32\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ c+16(FP), DX\n\tMOVQ n+24(FP), CX\n\tBYTE $0x55                                 // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5                   // movq\t%rsp, %rbp\n\tLONG $0xf8e48348                           // andq\t$-8, %rsp\n\tLONG $0x07418d4c                           // leaq\t7(%rcx), %r8\n\tWORD $0x8548; BYTE $0xc9                   // testq\t%rcx, %rcx\n\tLONG $0xc1490f4c                           // cmovnsq\t%rcx, %r8\n\tWORD $0x894c; BYTE $0xc0                   // movq\t%r8, %rax\n\tLONG $0x03f8c148                           // sarq\t$3, %rax\n\tLONG $0xf8e08349                           // andq\t$-8, %r8\n\tWORD $0x294c; BYTE $0xc1                   // subq\t%r8, %rcx\n\tWORD $0xc085                               // testl\t%eax, %eax\n\tJLE  LBB2_6\n\tWORD $0xf883; BYTE $0x01                   // cmpl\t$1, %eax\n\tJE   LBB2_4\n\tWORD $0x8941; BYTE $0xc0                   // movl\t%eax, %r8d\n\tLONG $0xfee08141; WORD $0xffff; BYTE $0x7f // andl\t$2147483646, %r8d               # imm = 0x7FFFFFFE\n\nLBB2_3:\n\tLONG $0x187de2c4; BYTE $0x06 // vbroadcastss\t(%rsi), %ymm0\n\tLONG $0x0759fcc5             // vmulps\t(%rdi), %ymm0, %ymm0\n\tLONG $0x0211fcc5             // vmovups\t%ymm0, (%rdx)\n\tLONG $0x187de2c4; BYTE $0x06 // vbroadcastss\t(%rsi), %ymm0\n\tLONG $0x4759fcc5; BYTE $0x20 // vmulps\t32(%rdi), %ymm0, %ymm0\n\tLONG $0x4211fcc5; BYTE $0x20 // vmovups\t%ymm0, 32(%rdx)\n\tLONG $0x40c78348             // addq\t$64, %rdi\n\tLONG $0x40c28348             // addq\t$64, %rdx\n\tLONG $0xfec08341             // addl\t$-2, %r8d\n\tJNE  LBB2_3\n\nLBB2_4:\n\tWORD $0x01a8                 // testb\t$1, %al\n\tJE   LBB2_6\n\tLONG $0x187de2c4; BYTE $0x06 // vbroadcastss\t(%rsi), %ymm0\n\tLONG $0x0759fcc5             // vmulps\t(%rdi), %ymm0, %ymm0\n\tLONG $0x0211fcc5             // vmovups\t%ymm0, (%rdx)\n\tLONG $0x20c78348             // addq\t$32, %rdi\n\tLONG $0x20c28348             // addq\t$32, %rdx\n\nLBB2_6:\n\tWORD $0x8548; BYTE $0xc9     // testq\t%rcx, %rcx\n\tJLE  LBB2_14\n\tWORD $0xc889                 // movl\t%ecx, %eax\n\tLONG $0x0710fac5             // vmovss\t(%rdi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x0211fac5             // vmovss\t%xmm0, (%rdx)\n\tLONG $0x01f88348             // cmpq\t$1, %rax\n\tJE   LBB2_14\n\tLONG $0x4710fac5; BYTE $0x04 // vmovss\t4(%rdi), %xmm0                  # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x04 // vmovss\t%xmm0, 4(%rdx)\n\tWORD $0xf883; BYTE $0x02     // cmpl\t$2, %eax\n\tJE   LBB2_14\n\tLONG $0x4710fac5; BYTE $0x08 // vmovss\t8(%rdi), %xmm0                  # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x08 // vmovss\t%xmm0, 8(%rdx)\n\tWORD $0xf883; BYTE $0x03     // cmpl\t$3, %eax\n\tJE   LBB2_14\n\tLONG $0x4710fac5; BYTE $0x0c // vmovss\t12(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x0c // vmovss\t%xmm0, 12(%rdx)\n\tWORD $0xf883; BYTE $0x04     // cmpl\t$4, %eax\n\tJE   LBB2_14\n\tLONG $0x4710fac5; BYTE $0x10 // vmovss\t16(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x10 // vmovss\t%xmm0, 16(%rdx)\n\tWORD $0xf883; BYTE $0x05     // cmpl\t$5, %eax\n\tJE   LBB2_14\n\tLONG $0x4710fac5; BYTE $0x14 // vmovss\t20(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x14 // vmovss\t%xmm0, 20(%rdx)\n\tWORD $0xf883; BYTE $0x06     // cmpl\t$6, %eax\n\tJE   LBB2_14\n\tLONG $0x4710fac5; BYTE $0x18 // vmovss\t24(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x18 // vmovss\t%xmm0, 24(%rdx)\n\nLBB2_14:\n\tWORD $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tRET\n\nTEXT ·_mm256_mul_const(SB), $0-24\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ n+16(FP), DX\n\tBYTE $0x55                     // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5       // movq\t%rsp, %rbp\n\tLONG $0xf8e48348               // andq\t$-8, %rsp\n\tLONG $0x074a8d48               // leaq\t7(%rdx), %rcx\n\tWORD $0x8548; BYTE $0xd2       // testq\t%rdx, %rdx\n\tLONG $0xca490f48               // cmovnsq\t%rdx, %rcx\n\tWORD $0x8948; BYTE $0xc8       // movq\t%rcx, %rax\n\tLONG $0x03f8c148               // sarq\t$3, %rax\n\tLONG $0xf8e18348               // andq\t$-8, %rcx\n\tWORD $0x2948; BYTE $0xca       // subq\t%rcx, %rdx\n\tWORD $0xc085                   // testl\t%eax, %eax\n\tJLE  LBB3_6\n\tWORD $0xf883; BYTE $0x01       // cmpl\t$1, %eax\n\tJE   LBB3_4\n\tWORD $0xc189                   // movl\t%eax, %ecx\n\tLONG $0xfffee181; WORD $0x7fff // andl\t$2147483646, %ecx               # imm = 0x7FFFFFFE\n\nLBB3_3:\n\tLONG $0x187de2c4; BYTE $0x06 // vbroadcastss\t(%rsi), %ymm0\n\tLONG $0x0759fcc5             // vmulps\t(%rdi), %ymm0, %ymm0\n\tLONG $0x0711fcc5             // vmovups\t%ymm0, (%rdi)\n\tLONG $0x187de2c4; BYTE $0x06 // vbroadcastss\t(%rsi), %ymm0\n\tLONG $0x4759fcc5; BYTE $0x20 // vmulps\t32(%rdi), %ymm0, %ymm0\n\tLONG $0x4711fcc5; BYTE $0x20 // vmovups\t%ymm0, 32(%rdi)\n\tLONG $0x40c78348             // addq\t$64, %rdi\n\tWORD $0xc183; BYTE $0xfe     // addl\t$-2, %ecx\n\tJNE  LBB3_3\n\nLBB3_4:\n\tWORD $0x01a8                 // testb\t$1, %al\n\tJE   LBB3_6\n\tLONG $0x187de2c4; BYTE $0x06 // vbroadcastss\t(%rsi), %ymm0\n\tLONG $0x0759fcc5             // vmulps\t(%rdi), %ymm0, %ymm0\n\tLONG $0x0711fcc5             // vmovups\t%ymm0, (%rdi)\n\tLONG $0x20c78348             // addq\t$32, %rdi\n\nLBB3_6:\n\tWORD $0x8548; BYTE $0xd2     // testq\t%rdx, %rdx\n\tJLE  LBB3_14\n\tWORD $0xd089                 // movl\t%edx, %eax\n\tLONG $0x0610fac5             // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0759fac5             // vmulss\t(%rdi), %xmm0, %xmm0\n\tLONG $0x0711fac5             // vmovss\t%xmm0, (%rdi)\n\tLONG $0x01f88348             // cmpq\t$1, %rax\n\tJE   LBB3_14\n\tLONG $0x0610fac5             // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4759fac5; BYTE $0x04 // vmulss\t4(%rdi), %xmm0, %xmm0\n\tLONG $0x4711fac5; BYTE $0x04 // vmovss\t%xmm0, 4(%rdi)\n\tWORD $0xf883; BYTE $0x02     // cmpl\t$2, %eax\n\tJE   LBB3_14\n\tLONG $0x0610fac5             // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4759fac5; BYTE $0x08 // vmulss\t8(%rdi), %xmm0, %xmm0\n\tLONG $0x4711fac5; BYTE $0x08 // vmovss\t%xmm0, 8(%rdi)\n\tWORD $0xf883; BYTE $0x03     // cmpl\t$3, %eax\n\tJE   LBB3_14\n\tLONG $0x0610fac5             // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4759fac5; BYTE $0x0c // vmulss\t12(%rdi), %xmm0, %xmm0\n\tLONG $0x4711fac5; BYTE $0x0c // vmovss\t%xmm0, 12(%rdi)\n\tWORD $0xf883; BYTE $0x04     // cmpl\t$4, %eax\n\tJE   LBB3_14\n\tLONG $0x0610fac5             // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4759fac5; BYTE $0x10 // vmulss\t16(%rdi), %xmm0, %xmm0\n\tLONG $0x4711fac5; BYTE $0x10 // vmovss\t%xmm0, 16(%rdi)\n\tWORD $0xf883; BYTE $0x05     // cmpl\t$5, %eax\n\tJE   LBB3_14\n\tLONG $0x0610fac5             // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4759fac5; BYTE $0x14 // vmulss\t20(%rdi), %xmm0, %xmm0\n\tLONG $0x4711fac5; BYTE $0x14 // vmovss\t%xmm0, 20(%rdi)\n\tWORD $0xf883; BYTE $0x06     // cmpl\t$6, %eax\n\tJE   LBB3_14\n\tLONG $0x0610fac5             // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4759fac5; BYTE $0x18 // vmulss\t24(%rdi), %xmm0, %xmm0\n\tLONG $0x4711fac5; BYTE $0x18 // vmovss\t%xmm0, 24(%rdi)\n\nLBB3_14:\n\tWORD $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tRET\n\nTEXT ·_mm256_add_const(SB), $0-24\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ n+16(FP), DX\n\tBYTE $0x55                     // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5       // movq\t%rsp, %rbp\n\tLONG $0xf8e48348               // andq\t$-8, %rsp\n\tLONG $0x074a8d48               // leaq\t7(%rdx), %rcx\n\tWORD $0x8548; BYTE $0xd2       // testq\t%rdx, %rdx\n\tLONG $0xca490f48               // cmovnsq\t%rdx, %rcx\n\tWORD $0x8948; BYTE $0xc8       // movq\t%rcx, %rax\n\tLONG $0x03f8c148               // sarq\t$3, %rax\n\tLONG $0xf8e18348               // andq\t$-8, %rcx\n\tWORD $0x2948; BYTE $0xca       // subq\t%rcx, %rdx\n\tWORD $0xc085                   // testl\t%eax, %eax\n\tJLE  LBB4_6\n\tWORD $0xf883; BYTE $0x01       // cmpl\t$1, %eax\n\tJE   LBB4_4\n\tWORD $0xc189                   // movl\t%eax, %ecx\n\tLONG $0xfffee181; WORD $0x7fff // andl\t$2147483646, %ecx               # imm = 0x7FFFFFFE\n\nLBB4_3:\n\tLONG $0x187de2c4; BYTE $0x06 // vbroadcastss\t(%rsi), %ymm0\n\tLONG $0x0758fcc5             // vaddps\t(%rdi), %ymm0, %ymm0\n\tLONG $0x0711fcc5             // vmovups\t%ymm0, (%rdi)\n\tLONG $0x187de2c4; BYTE $0x06 // vbroadcastss\t(%rsi), %ymm0\n\tLONG $0x4758fcc5; BYTE $0x20 // vaddps\t32(%rdi), %ymm0, %ymm0\n\tLONG $0x4711fcc5; BYTE $0x20 // vmovups\t%ymm0, 32(%rdi)\n\tLONG $0x40c78348             // addq\t$64, %rdi\n\tWORD $0xc183; BYTE $0xfe     // addl\t$-2, %ecx\n\tJNE  LBB4_3\n\nLBB4_4:\n\tWORD $0x01a8                 // testb\t$1, %al\n\tJE   LBB4_6\n\tLONG $0x187de2c4; BYTE $0x06 // vbroadcastss\t(%rsi), %ymm0\n\tLONG $0x0758fcc5             // vaddps\t(%rdi), %ymm0, %ymm0\n\tLONG $0x0711fcc5             // vmovups\t%ymm0, (%rdi)\n\tLONG $0x20c78348             // addq\t$32, %rdi\n\nLBB4_6:\n\tWORD $0x8548; BYTE $0xd2     // testq\t%rdx, %rdx\n\tJLE  LBB4_14\n\tWORD $0xd089                 // movl\t%edx, %eax\n\tLONG $0x0610fac5             // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0758fac5             // vaddss\t(%rdi), %xmm0, %xmm0\n\tLONG $0x0711fac5             // vmovss\t%xmm0, (%rdi)\n\tLONG $0x01f88348             // cmpq\t$1, %rax\n\tJE   LBB4_14\n\tLONG $0x0610fac5             // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4758fac5; BYTE $0x04 // vaddss\t4(%rdi), %xmm0, %xmm0\n\tLONG $0x4711fac5; BYTE $0x04 // vmovss\t%xmm0, 4(%rdi)\n\tWORD $0xf883; BYTE $0x02     // cmpl\t$2, %eax\n\tJE   LBB4_14\n\tLONG $0x0610fac5             // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4758fac5; BYTE $0x08 // vaddss\t8(%rdi), %xmm0, %xmm0\n\tLONG $0x4711fac5; BYTE $0x08 // vmovss\t%xmm0, 8(%rdi)\n\tWORD $0xf883; BYTE $0x03     // cmpl\t$3, %eax\n\tJE   LBB4_14\n\tLONG $0x0610fac5             // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4758fac5; BYTE $0x0c // vaddss\t12(%rdi), %xmm0, %xmm0\n\tLONG $0x4711fac5; BYTE $0x0c // vmovss\t%xmm0, 12(%rdi)\n\tWORD $0xf883; BYTE $0x04     // cmpl\t$4, %eax\n\tJE   LBB4_14\n\tLONG $0x0610fac5             // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4758fac5; BYTE $0x10 // vaddss\t16(%rdi), %xmm0, %xmm0\n\tLONG $0x4711fac5; BYTE $0x10 // vmovss\t%xmm0, 16(%rdi)\n\tWORD $0xf883; BYTE $0x05     // cmpl\t$5, %eax\n\tJE   LBB4_14\n\tLONG $0x0610fac5             // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4758fac5; BYTE $0x14 // vaddss\t20(%rdi), %xmm0, %xmm0\n\tLONG $0x4711fac5; BYTE $0x14 // vmovss\t%xmm0, 20(%rdi)\n\tWORD $0xf883; BYTE $0x06     // cmpl\t$6, %eax\n\tJE   LBB4_14\n\tLONG $0x0610fac5             // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4758fac5; BYTE $0x18 // vaddss\t24(%rdi), %xmm0, %xmm0\n\tLONG $0x4711fac5; BYTE $0x18 // vmovss\t%xmm0, 24(%rdi)\n\nLBB4_14:\n\tWORD $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tRET\n\nTEXT ·_mm256_sub_to(SB), $0-32\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ c+16(FP), DX\n\tMOVQ n+24(FP), CX\n\tBYTE $0x55                                 // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5                   // movq\t%rsp, %rbp\n\tLONG $0xf8e48348                           // andq\t$-8, %rsp\n\tLONG $0x07418d48                           // leaq\t7(%rcx), %rax\n\tWORD $0x8548; BYTE $0xc9                   // testq\t%rcx, %rcx\n\tLONG $0xc1490f48                           // cmovnsq\t%rcx, %rax\n\tWORD $0x8949; BYTE $0xc0                   // movq\t%rax, %r8\n\tLONG $0x03f8c149                           // sarq\t$3, %r8\n\tLONG $0xf8e08348                           // andq\t$-8, %rax\n\tWORD $0x2948; BYTE $0xc1                   // subq\t%rax, %rcx\n\tWORD $0x8545; BYTE $0xc0                   // testl\t%r8d, %r8d\n\tJLE  LBB5_6\n\tWORD $0x8944; BYTE $0xc0                   // movl\t%r8d, %eax\n\tWORD $0xe083; BYTE $0x03                   // andl\t$3, %eax\n\tLONG $0x04f88341                           // cmpl\t$4, %r8d\n\tJB   LBB5_4\n\tLONG $0xfce08141; WORD $0xffff; BYTE $0x7f // andl\t$2147483644, %r8d               # imm = 0x7FFFFFFC\n\nLBB5_3:\n\tLONG $0x0710fcc5             // vmovups\t(%rdi), %ymm0\n\tLONG $0x065cfcc5             // vsubps\t(%rsi), %ymm0, %ymm0\n\tLONG $0x0211fcc5             // vmovups\t%ymm0, (%rdx)\n\tLONG $0x4710fcc5; BYTE $0x20 // vmovups\t32(%rdi), %ymm0\n\tLONG $0x465cfcc5; BYTE $0x20 // vsubps\t32(%rsi), %ymm0, %ymm0\n\tLONG $0x4211fcc5; BYTE $0x20 // vmovups\t%ymm0, 32(%rdx)\n\tLONG $0x4710fcc5; BYTE $0x40 // vmovups\t64(%rdi), %ymm0\n\tLONG $0x465cfcc5; BYTE $0x40 // vsubps\t64(%rsi), %ymm0, %ymm0\n\tLONG $0x4211fcc5; BYTE $0x40 // vmovups\t%ymm0, 64(%rdx)\n\tLONG $0x4710fcc5; BYTE $0x60 // vmovups\t96(%rdi), %ymm0\n\tLONG $0x465cfcc5; BYTE $0x60 // vsubps\t96(%rsi), %ymm0, %ymm0\n\tLONG $0x4211fcc5; BYTE $0x60 // vmovups\t%ymm0, 96(%rdx)\n\tLONG $0x80ef8348             // subq\t$-128, %rdi\n\tLONG $0x80ee8348             // subq\t$-128, %rsi\n\tLONG $0x80ea8348             // subq\t$-128, %rdx\n\tLONG $0xfcc08341             // addl\t$-4, %r8d\n\tJNE  LBB5_3\n\nLBB5_4:\n\tWORD $0xc085 // testl\t%eax, %eax\n\tJE   LBB5_6\n\nLBB5_5:\n\tLONG $0x0710fcc5 // vmovups\t(%rdi), %ymm0\n\tLONG $0x065cfcc5 // vsubps\t(%rsi), %ymm0, %ymm0\n\tLONG $0x0211fcc5 // vmovups\t%ymm0, (%rdx)\n\tLONG $0x20c78348 // addq\t$32, %rdi\n\tLONG $0x20c68348 // addq\t$32, %rsi\n\tLONG $0x20c28348 // addq\t$32, %rdx\n\tWORD $0xc8ff     // decl\t%eax\n\tJNE  LBB5_5\n\nLBB5_6:\n\tWORD $0x8548; BYTE $0xc9     // testq\t%rcx, %rcx\n\tJLE  LBB5_14\n\tWORD $0xc889                 // movl\t%ecx, %eax\n\tLONG $0x0710fac5             // vmovss\t(%rdi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x065cfac5             // vsubss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x0211fac5             // vmovss\t%xmm0, (%rdx)\n\tLONG $0x01f88348             // cmpq\t$1, %rax\n\tJE   LBB5_14\n\tLONG $0x4710fac5; BYTE $0x04 // vmovss\t4(%rdi), %xmm0                  # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x465cfac5; BYTE $0x04 // vsubss\t4(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x04 // vmovss\t%xmm0, 4(%rdx)\n\tWORD $0xf883; BYTE $0x02     // cmpl\t$2, %eax\n\tJE   LBB5_14\n\tLONG $0x4710fac5; BYTE $0x08 // vmovss\t8(%rdi), %xmm0                  # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x465cfac5; BYTE $0x08 // vsubss\t8(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x08 // vmovss\t%xmm0, 8(%rdx)\n\tWORD $0xf883; BYTE $0x03     // cmpl\t$3, %eax\n\tJE   LBB5_14\n\tLONG $0x4710fac5; BYTE $0x0c // vmovss\t12(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x465cfac5; BYTE $0x0c // vsubss\t12(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x0c // vmovss\t%xmm0, 12(%rdx)\n\tWORD $0xf883; BYTE $0x04     // cmpl\t$4, %eax\n\tJE   LBB5_14\n\tLONG $0x4710fac5; BYTE $0x10 // vmovss\t16(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x465cfac5; BYTE $0x10 // vsubss\t16(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x10 // vmovss\t%xmm0, 16(%rdx)\n\tWORD $0xf883; BYTE $0x05     // cmpl\t$5, %eax\n\tJE   LBB5_14\n\tLONG $0x4710fac5; BYTE $0x14 // vmovss\t20(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x465cfac5; BYTE $0x14 // vsubss\t20(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x14 // vmovss\t%xmm0, 20(%rdx)\n\tWORD $0xf883; BYTE $0x06     // cmpl\t$6, %eax\n\tJE   LBB5_14\n\tLONG $0x4710fac5; BYTE $0x18 // vmovss\t24(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x465cfac5; BYTE $0x18 // vsubss\t24(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x18 // vmovss\t%xmm0, 24(%rdx)\n\nLBB5_14:\n\tWORD $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tRET\n\nTEXT ·_mm256_sub(SB), $0-24\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ n+16(FP), DX\n\tBYTE $0x55                     // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5       // movq\t%rsp, %rbp\n\tLONG $0xf8e48348               // andq\t$-8, %rsp\n\tLONG $0x07428d48               // leaq\t7(%rdx), %rax\n\tWORD $0x8548; BYTE $0xd2       // testq\t%rdx, %rdx\n\tLONG $0xc2490f48               // cmovnsq\t%rdx, %rax\n\tWORD $0x8948; BYTE $0xc1       // movq\t%rax, %rcx\n\tLONG $0x03f9c148               // sarq\t$3, %rcx\n\tLONG $0xf8e08348               // andq\t$-8, %rax\n\tWORD $0x2948; BYTE $0xc2       // subq\t%rax, %rdx\n\tWORD $0xc985                   // testl\t%ecx, %ecx\n\tJLE  LBB6_6\n\tWORD $0xc889                   // movl\t%ecx, %eax\n\tWORD $0xe083; BYTE $0x03       // andl\t$3, %eax\n\tWORD $0xf983; BYTE $0x04       // cmpl\t$4, %ecx\n\tJB   LBB6_4\n\tLONG $0xfffce181; WORD $0x7fff // andl\t$2147483644, %ecx               # imm = 0x7FFFFFFC\n\nLBB6_3:\n\tLONG $0x0710fcc5             // vmovups\t(%rdi), %ymm0\n\tLONG $0x4f10fcc5; BYTE $0x20 // vmovups\t32(%rdi), %ymm1\n\tLONG $0x5710fcc5; BYTE $0x40 // vmovups\t64(%rdi), %ymm2\n\tLONG $0x065cfcc5             // vsubps\t(%rsi), %ymm0, %ymm0\n\tLONG $0x5f10fcc5; BYTE $0x60 // vmovups\t96(%rdi), %ymm3\n\tLONG $0x0711fcc5             // vmovups\t%ymm0, (%rdi)\n\tLONG $0x465cf4c5; BYTE $0x20 // vsubps\t32(%rsi), %ymm1, %ymm0\n\tLONG $0x4711fcc5; BYTE $0x20 // vmovups\t%ymm0, 32(%rdi)\n\tLONG $0x465cecc5; BYTE $0x40 // vsubps\t64(%rsi), %ymm2, %ymm0\n\tLONG $0x4711fcc5; BYTE $0x40 // vmovups\t%ymm0, 64(%rdi)\n\tLONG $0x465ce4c5; BYTE $0x60 // vsubps\t96(%rsi), %ymm3, %ymm0\n\tLONG $0x4711fcc5; BYTE $0x60 // vmovups\t%ymm0, 96(%rdi)\n\tLONG $0x80ef8348             // subq\t$-128, %rdi\n\tLONG $0x80ee8348             // subq\t$-128, %rsi\n\tWORD $0xc183; BYTE $0xfc     // addl\t$-4, %ecx\n\tJNE  LBB6_3\n\nLBB6_4:\n\tWORD $0xc085 // testl\t%eax, %eax\n\tJE   LBB6_6\n\nLBB6_5:\n\tLONG $0x0710fcc5 // vmovups\t(%rdi), %ymm0\n\tLONG $0x065cfcc5 // vsubps\t(%rsi), %ymm0, %ymm0\n\tLONG $0x0711fcc5 // vmovups\t%ymm0, (%rdi)\n\tLONG $0x20c78348 // addq\t$32, %rdi\n\tLONG $0x20c68348 // addq\t$32, %rsi\n\tWORD $0xc8ff     // decl\t%eax\n\tJNE  LBB6_5\n\nLBB6_6:\n\tWORD $0x8548; BYTE $0xd2     // testq\t%rdx, %rdx\n\tJLE  LBB6_14\n\tWORD $0xd089                 // movl\t%edx, %eax\n\tLONG $0x0710fac5             // vmovss\t(%rdi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x065cfac5             // vsubss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x0711fac5             // vmovss\t%xmm0, (%rdi)\n\tLONG $0x01f88348             // cmpq\t$1, %rax\n\tJE   LBB6_14\n\tLONG $0x4710fac5; BYTE $0x04 // vmovss\t4(%rdi), %xmm0                  # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x465cfac5; BYTE $0x04 // vsubss\t4(%rsi), %xmm0, %xmm0\n\tLONG $0x4711fac5; BYTE $0x04 // vmovss\t%xmm0, 4(%rdi)\n\tWORD $0xf883; BYTE $0x02     // cmpl\t$2, %eax\n\tJE   LBB6_14\n\tLONG $0x4710fac5; BYTE $0x08 // vmovss\t8(%rdi), %xmm0                  # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x465cfac5; BYTE $0x08 // vsubss\t8(%rsi), %xmm0, %xmm0\n\tLONG $0x4711fac5; BYTE $0x08 // vmovss\t%xmm0, 8(%rdi)\n\tWORD $0xf883; BYTE $0x03     // cmpl\t$3, %eax\n\tJE   LBB6_14\n\tLONG $0x4710fac5; BYTE $0x0c // vmovss\t12(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x465cfac5; BYTE $0x0c // vsubss\t12(%rsi), %xmm0, %xmm0\n\tLONG $0x4711fac5; BYTE $0x0c // vmovss\t%xmm0, 12(%rdi)\n\tWORD $0xf883; BYTE $0x04     // cmpl\t$4, %eax\n\tJE   LBB6_14\n\tLONG $0x4710fac5; BYTE $0x10 // vmovss\t16(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x465cfac5; BYTE $0x10 // vsubss\t16(%rsi), %xmm0, %xmm0\n\tLONG $0x4711fac5; BYTE $0x10 // vmovss\t%xmm0, 16(%rdi)\n\tWORD $0xf883; BYTE $0x05     // cmpl\t$5, %eax\n\tJE   LBB6_14\n\tLONG $0x4710fac5; BYTE $0x14 // vmovss\t20(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x465cfac5; BYTE $0x14 // vsubss\t20(%rsi), %xmm0, %xmm0\n\tLONG $0x4711fac5; BYTE $0x14 // vmovss\t%xmm0, 20(%rdi)\n\tWORD $0xf883; BYTE $0x06     // cmpl\t$6, %eax\n\tJE   LBB6_14\n\tLONG $0x4710fac5; BYTE $0x18 // vmovss\t24(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x465cfac5; BYTE $0x18 // vsubss\t24(%rsi), %xmm0, %xmm0\n\tLONG $0x4711fac5; BYTE $0x18 // vmovss\t%xmm0, 24(%rdi)\n\nLBB6_14:\n\tWORD $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tRET\n\nTEXT ·_mm256_mul_to(SB), $0-32\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ c+16(FP), DX\n\tMOVQ n+24(FP), CX\n\tBYTE $0x55                                 // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5                   // movq\t%rsp, %rbp\n\tLONG $0xf8e48348                           // andq\t$-8, %rsp\n\tLONG $0x07418d48                           // leaq\t7(%rcx), %rax\n\tWORD $0x8548; BYTE $0xc9                   // testq\t%rcx, %rcx\n\tLONG $0xc1490f48                           // cmovnsq\t%rcx, %rax\n\tWORD $0x8949; BYTE $0xc0                   // movq\t%rax, %r8\n\tLONG $0x03f8c149                           // sarq\t$3, %r8\n\tLONG $0xf8e08348                           // andq\t$-8, %rax\n\tWORD $0x2948; BYTE $0xc1                   // subq\t%rax, %rcx\n\tWORD $0x8545; BYTE $0xc0                   // testl\t%r8d, %r8d\n\tJLE  LBB7_6\n\tWORD $0x8944; BYTE $0xc0                   // movl\t%r8d, %eax\n\tWORD $0xe083; BYTE $0x03                   // andl\t$3, %eax\n\tLONG $0x04f88341                           // cmpl\t$4, %r8d\n\tJB   LBB7_4\n\tLONG $0xfce08141; WORD $0xffff; BYTE $0x7f // andl\t$2147483644, %r8d               # imm = 0x7FFFFFFC\n\nLBB7_3:\n\tLONG $0x0710fcc5             // vmovups\t(%rdi), %ymm0\n\tLONG $0x0659fcc5             // vmulps\t(%rsi), %ymm0, %ymm0\n\tLONG $0x0211fcc5             // vmovups\t%ymm0, (%rdx)\n\tLONG $0x4710fcc5; BYTE $0x20 // vmovups\t32(%rdi), %ymm0\n\tLONG $0x4659fcc5; BYTE $0x20 // vmulps\t32(%rsi), %ymm0, %ymm0\n\tLONG $0x4211fcc5; BYTE $0x20 // vmovups\t%ymm0, 32(%rdx)\n\tLONG $0x4710fcc5; BYTE $0x40 // vmovups\t64(%rdi), %ymm0\n\tLONG $0x4659fcc5; BYTE $0x40 // vmulps\t64(%rsi), %ymm0, %ymm0\n\tLONG $0x4211fcc5; BYTE $0x40 // vmovups\t%ymm0, 64(%rdx)\n\tLONG $0x4710fcc5; BYTE $0x60 // vmovups\t96(%rdi), %ymm0\n\tLONG $0x4659fcc5; BYTE $0x60 // vmulps\t96(%rsi), %ymm0, %ymm0\n\tLONG $0x4211fcc5; BYTE $0x60 // vmovups\t%ymm0, 96(%rdx)\n\tLONG $0x80ef8348             // subq\t$-128, %rdi\n\tLONG $0x80ee8348             // subq\t$-128, %rsi\n\tLONG $0x80ea8348             // subq\t$-128, %rdx\n\tLONG $0xfcc08341             // addl\t$-4, %r8d\n\tJNE  LBB7_3\n\nLBB7_4:\n\tWORD $0xc085 // testl\t%eax, %eax\n\tJE   LBB7_6\n\nLBB7_5:\n\tLONG $0x0710fcc5 // vmovups\t(%rdi), %ymm0\n\tLONG $0x0659fcc5 // vmulps\t(%rsi), %ymm0, %ymm0\n\tLONG $0x0211fcc5 // vmovups\t%ymm0, (%rdx)\n\tLONG $0x20c78348 // addq\t$32, %rdi\n\tLONG $0x20c68348 // addq\t$32, %rsi\n\tLONG $0x20c28348 // addq\t$32, %rdx\n\tWORD $0xc8ff     // decl\t%eax\n\tJNE  LBB7_5\n\nLBB7_6:\n\tWORD $0x8548; BYTE $0xc9     // testq\t%rcx, %rcx\n\tJLE  LBB7_14\n\tWORD $0xc889                 // movl\t%ecx, %eax\n\tLONG $0x0710fac5             // vmovss\t(%rdi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x0211fac5             // vmovss\t%xmm0, (%rdx)\n\tLONG $0x01f88348             // cmpq\t$1, %rax\n\tJE   LBB7_14\n\tLONG $0x4710fac5; BYTE $0x04 // vmovss\t4(%rdi), %xmm0                  # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4659fac5; BYTE $0x04 // vmulss\t4(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x04 // vmovss\t%xmm0, 4(%rdx)\n\tWORD $0xf883; BYTE $0x02     // cmpl\t$2, %eax\n\tJE   LBB7_14\n\tLONG $0x4710fac5; BYTE $0x08 // vmovss\t8(%rdi), %xmm0                  # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4659fac5; BYTE $0x08 // vmulss\t8(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x08 // vmovss\t%xmm0, 8(%rdx)\n\tWORD $0xf883; BYTE $0x03     // cmpl\t$3, %eax\n\tJE   LBB7_14\n\tLONG $0x4710fac5; BYTE $0x0c // vmovss\t12(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4659fac5; BYTE $0x0c // vmulss\t12(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x0c // vmovss\t%xmm0, 12(%rdx)\n\tWORD $0xf883; BYTE $0x04     // cmpl\t$4, %eax\n\tJE   LBB7_14\n\tLONG $0x4710fac5; BYTE $0x10 // vmovss\t16(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4659fac5; BYTE $0x10 // vmulss\t16(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x10 // vmovss\t%xmm0, 16(%rdx)\n\tWORD $0xf883; BYTE $0x05     // cmpl\t$5, %eax\n\tJE   LBB7_14\n\tLONG $0x4710fac5; BYTE $0x14 // vmovss\t20(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4659fac5; BYTE $0x14 // vmulss\t20(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x14 // vmovss\t%xmm0, 20(%rdx)\n\tWORD $0xf883; BYTE $0x06     // cmpl\t$6, %eax\n\tJE   LBB7_14\n\tLONG $0x4710fac5; BYTE $0x18 // vmovss\t24(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4659fac5; BYTE $0x18 // vmulss\t24(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x18 // vmovss\t%xmm0, 24(%rdx)\n\nLBB7_14:\n\tWORD $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tRET\n\nTEXT ·_mm256_div_to(SB), $0-32\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ c+16(FP), DX\n\tMOVQ n+24(FP), CX\n\tBYTE $0x55                                 // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5                   // movq\t%rsp, %rbp\n\tLONG $0xf8e48348                           // andq\t$-8, %rsp\n\tLONG $0x07418d48                           // leaq\t7(%rcx), %rax\n\tWORD $0x8548; BYTE $0xc9                   // testq\t%rcx, %rcx\n\tLONG $0xc1490f48                           // cmovnsq\t%rcx, %rax\n\tWORD $0x8949; BYTE $0xc0                   // movq\t%rax, %r8\n\tLONG $0x03f8c149                           // sarq\t$3, %r8\n\tLONG $0xf8e08348                           // andq\t$-8, %rax\n\tWORD $0x2948; BYTE $0xc1                   // subq\t%rax, %rcx\n\tWORD $0x8545; BYTE $0xc0                   // testl\t%r8d, %r8d\n\tJLE  LBB8_6\n\tWORD $0x8944; BYTE $0xc0                   // movl\t%r8d, %eax\n\tWORD $0xe083; BYTE $0x03                   // andl\t$3, %eax\n\tLONG $0x04f88341                           // cmpl\t$4, %r8d\n\tJB   LBB8_4\n\tLONG $0xfce08141; WORD $0xffff; BYTE $0x7f // andl\t$2147483644, %r8d               # imm = 0x7FFFFFFC\n\nLBB8_3:\n\tLONG $0x0710fcc5             // vmovups\t(%rdi), %ymm0\n\tLONG $0x065efcc5             // vdivps\t(%rsi), %ymm0, %ymm0\n\tLONG $0x0211fcc5             // vmovups\t%ymm0, (%rdx)\n\tLONG $0x4710fcc5; BYTE $0x20 // vmovups\t32(%rdi), %ymm0\n\tLONG $0x465efcc5; BYTE $0x20 // vdivps\t32(%rsi), %ymm0, %ymm0\n\tLONG $0x4211fcc5; BYTE $0x20 // vmovups\t%ymm0, 32(%rdx)\n\tLONG $0x4710fcc5; BYTE $0x40 // vmovups\t64(%rdi), %ymm0\n\tLONG $0x465efcc5; BYTE $0x40 // vdivps\t64(%rsi), %ymm0, %ymm0\n\tLONG $0x4211fcc5; BYTE $0x40 // vmovups\t%ymm0, 64(%rdx)\n\tLONG $0x4710fcc5; BYTE $0x60 // vmovups\t96(%rdi), %ymm0\n\tLONG $0x465efcc5; BYTE $0x60 // vdivps\t96(%rsi), %ymm0, %ymm0\n\tLONG $0x4211fcc5; BYTE $0x60 // vmovups\t%ymm0, 96(%rdx)\n\tLONG $0x80ef8348             // subq\t$-128, %rdi\n\tLONG $0x80ee8348             // subq\t$-128, %rsi\n\tLONG $0x80ea8348             // subq\t$-128, %rdx\n\tLONG $0xfcc08341             // addl\t$-4, %r8d\n\tJNE  LBB8_3\n\nLBB8_4:\n\tWORD $0xc085 // testl\t%eax, %eax\n\tJE   LBB8_6\n\nLBB8_5:\n\tLONG $0x0710fcc5 // vmovups\t(%rdi), %ymm0\n\tLONG $0x065efcc5 // vdivps\t(%rsi), %ymm0, %ymm0\n\tLONG $0x0211fcc5 // vmovups\t%ymm0, (%rdx)\n\tLONG $0x20c78348 // addq\t$32, %rdi\n\tLONG $0x20c68348 // addq\t$32, %rsi\n\tLONG $0x20c28348 // addq\t$32, %rdx\n\tWORD $0xc8ff     // decl\t%eax\n\tJNE  LBB8_5\n\nLBB8_6:\n\tWORD $0x8548; BYTE $0xc9     // testq\t%rcx, %rcx\n\tJLE  LBB8_14\n\tWORD $0xc889                 // movl\t%ecx, %eax\n\tLONG $0x0710fac5             // vmovss\t(%rdi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x065efac5             // vdivss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x0211fac5             // vmovss\t%xmm0, (%rdx)\n\tLONG $0x01f88348             // cmpq\t$1, %rax\n\tJE   LBB8_14\n\tLONG $0x4710fac5; BYTE $0x04 // vmovss\t4(%rdi), %xmm0                  # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x465efac5; BYTE $0x04 // vdivss\t4(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x04 // vmovss\t%xmm0, 4(%rdx)\n\tWORD $0xf883; BYTE $0x02     // cmpl\t$2, %eax\n\tJE   LBB8_14\n\tLONG $0x4710fac5; BYTE $0x08 // vmovss\t8(%rdi), %xmm0                  # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x465efac5; BYTE $0x08 // vdivss\t8(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x08 // vmovss\t%xmm0, 8(%rdx)\n\tWORD $0xf883; BYTE $0x03     // cmpl\t$3, %eax\n\tJE   LBB8_14\n\tLONG $0x4710fac5; BYTE $0x0c // vmovss\t12(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x465efac5; BYTE $0x0c // vdivss\t12(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x0c // vmovss\t%xmm0, 12(%rdx)\n\tWORD $0xf883; BYTE $0x04     // cmpl\t$4, %eax\n\tJE   LBB8_14\n\tLONG $0x4710fac5; BYTE $0x10 // vmovss\t16(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x465efac5; BYTE $0x10 // vdivss\t16(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x10 // vmovss\t%xmm0, 16(%rdx)\n\tWORD $0xf883; BYTE $0x05     // cmpl\t$5, %eax\n\tJE   LBB8_14\n\tLONG $0x4710fac5; BYTE $0x14 // vmovss\t20(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x465efac5; BYTE $0x14 // vdivss\t20(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x14 // vmovss\t%xmm0, 20(%rdx)\n\tWORD $0xf883; BYTE $0x06     // cmpl\t$6, %eax\n\tJE   LBB8_14\n\tLONG $0x4710fac5; BYTE $0x18 // vmovss\t24(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x465efac5; BYTE $0x18 // vdivss\t24(%rsi), %xmm0, %xmm0\n\tLONG $0x4211fac5; BYTE $0x18 // vmovss\t%xmm0, 24(%rdx)\n\nLBB8_14:\n\tWORD $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tRET\n\nTEXT ·_mm256_sqrt_to(SB), $0-24\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ n+16(FP), DX\n\tBYTE $0x55                     // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5       // movq\t%rsp, %rbp\n\tLONG $0xf8e48348               // andq\t$-8, %rsp\n\tLONG $0x07428d48               // leaq\t7(%rdx), %rax\n\tWORD $0x8548; BYTE $0xd2       // testq\t%rdx, %rdx\n\tLONG $0xc2490f48               // cmovnsq\t%rdx, %rax\n\tWORD $0x8948; BYTE $0xc1       // movq\t%rax, %rcx\n\tLONG $0x03f9c148               // sarq\t$3, %rcx\n\tLONG $0xf8e08348               // andq\t$-8, %rax\n\tWORD $0x2948; BYTE $0xc2       // subq\t%rax, %rdx\n\tWORD $0xc985                   // testl\t%ecx, %ecx\n\tJLE  LBB9_6\n\tWORD $0xc889                   // movl\t%ecx, %eax\n\tWORD $0xe083; BYTE $0x03       // andl\t$3, %eax\n\tWORD $0xf983; BYTE $0x04       // cmpl\t$4, %ecx\n\tJB   LBB9_4\n\tLONG $0xfffce181; WORD $0x7fff // andl\t$2147483644, %ecx               # imm = 0x7FFFFFFC\n\nLBB9_3:\n\tLONG $0x0751fcc5             // vsqrtps\t(%rdi), %ymm0\n\tLONG $0x0611fcc5             // vmovups\t%ymm0, (%rsi)\n\tLONG $0x4751fcc5; BYTE $0x20 // vsqrtps\t32(%rdi), %ymm0\n\tLONG $0x4611fcc5; BYTE $0x20 // vmovups\t%ymm0, 32(%rsi)\n\tLONG $0x4751fcc5; BYTE $0x40 // vsqrtps\t64(%rdi), %ymm0\n\tLONG $0x4611fcc5; BYTE $0x40 // vmovups\t%ymm0, 64(%rsi)\n\tLONG $0x4751fcc5; BYTE $0x60 // vsqrtps\t96(%rdi), %ymm0\n\tLONG $0x4611fcc5; BYTE $0x60 // vmovups\t%ymm0, 96(%rsi)\n\tLONG $0x80ef8348             // subq\t$-128, %rdi\n\tLONG $0x80ee8348             // subq\t$-128, %rsi\n\tWORD $0xc183; BYTE $0xfc     // addl\t$-4, %ecx\n\tJNE  LBB9_3\n\nLBB9_4:\n\tWORD $0xc085 // testl\t%eax, %eax\n\tJE   LBB9_6\n\nLBB9_5:\n\tLONG $0x0751fcc5 // vsqrtps\t(%rdi), %ymm0\n\tLONG $0x0611fcc5 // vmovups\t%ymm0, (%rsi)\n\tLONG $0x20c78348 // addq\t$32, %rdi\n\tLONG $0x20c68348 // addq\t$32, %rsi\n\tWORD $0xc8ff     // decl\t%eax\n\tJNE  LBB9_5\n\nLBB9_6:\n\tWORD $0x8548; BYTE $0xd2     // testq\t%rdx, %rdx\n\tJLE  LBB9_14\n\tWORD $0xd089                 // movl\t%edx, %eax\n\tLONG $0x0710fac5             // vmovss\t(%rdi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0xc051fac5             // vsqrtss\t%xmm0, %xmm0, %xmm0\n\tLONG $0x0611fac5             // vmovss\t%xmm0, (%rsi)\n\tLONG $0x01f88348             // cmpq\t$1, %rax\n\tJE   LBB9_14\n\tLONG $0x4710fac5; BYTE $0x04 // vmovss\t4(%rdi), %xmm0                  # xmm0 = mem[0],zero,zero,zero\n\tLONG $0xc051fac5             // vsqrtss\t%xmm0, %xmm0, %xmm0\n\tLONG $0x4611fac5; BYTE $0x04 // vmovss\t%xmm0, 4(%rsi)\n\tWORD $0xf883; BYTE $0x02     // cmpl\t$2, %eax\n\tJE   LBB9_14\n\tLONG $0x4710fac5; BYTE $0x08 // vmovss\t8(%rdi), %xmm0                  # xmm0 = mem[0],zero,zero,zero\n\tLONG $0xc051fac5             // vsqrtss\t%xmm0, %xmm0, %xmm0\n\tLONG $0x4611fac5; BYTE $0x08 // vmovss\t%xmm0, 8(%rsi)\n\tWORD $0xf883; BYTE $0x03     // cmpl\t$3, %eax\n\tJE   LBB9_14\n\tLONG $0x4710fac5; BYTE $0x0c // vmovss\t12(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0xc051fac5             // vsqrtss\t%xmm0, %xmm0, %xmm0\n\tLONG $0x4611fac5; BYTE $0x0c // vmovss\t%xmm0, 12(%rsi)\n\tWORD $0xf883; BYTE $0x04     // cmpl\t$4, %eax\n\tJE   LBB9_14\n\tLONG $0x4710fac5; BYTE $0x10 // vmovss\t16(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0xc051fac5             // vsqrtss\t%xmm0, %xmm0, %xmm0\n\tLONG $0x4611fac5; BYTE $0x10 // vmovss\t%xmm0, 16(%rsi)\n\tWORD $0xf883; BYTE $0x05     // cmpl\t$5, %eax\n\tJE   LBB9_14\n\tLONG $0x4710fac5; BYTE $0x14 // vmovss\t20(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0xc051fac5             // vsqrtss\t%xmm0, %xmm0, %xmm0\n\tLONG $0x4611fac5; BYTE $0x14 // vmovss\t%xmm0, 20(%rsi)\n\tWORD $0xf883; BYTE $0x06     // cmpl\t$6, %eax\n\tJE   LBB9_14\n\tLONG $0x4710fac5; BYTE $0x18 // vmovss\t24(%rdi), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0xc051fac5             // vsqrtss\t%xmm0, %xmm0, %xmm0\n\tLONG $0x4611fac5; BYTE $0x18 // vmovss\t%xmm0, 24(%rsi)\n\nLBB9_14:\n\tWORD $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tRET\n\nTEXT ·_mm256_dot(SB), $8-32\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ n+16(FP), DX\n\tBYTE $0x55               // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5 // movq\t%rsp, %rbp\n\tLONG $0xf8e48348         // andq\t$-8, %rsp\n\tLONG $0x07428d48         // leaq\t7(%rdx), %rax\n\tWORD $0x8548; BYTE $0xd2 // testq\t%rdx, %rdx\n\tLONG $0xc2490f48         // cmovnsq\t%rdx, %rax\n\tWORD $0x8949; BYTE $0xc0 // movq\t%rax, %r8\n\tLONG $0x03f8c149         // sarq\t$3, %r8\n\tLONG $0xf8e08348         // andq\t$-8, %rax\n\tWORD $0x2948; BYTE $0xc2 // subq\t%rax, %rdx\n\tWORD $0x8545; BYTE $0xc0 // testl\t%r8d, %r8d\n\tJLE  LBB10_1\n\tLONG $0x0710fcc5         // vmovups\t(%rdi), %ymm0\n\tLONG $0x0659fcc5         // vmulps\t(%rsi), %ymm0, %ymm0\n\tLONG $0x20c78348         // addq\t$32, %rdi\n\tLONG $0x20c68348         // addq\t$32, %rsi\n\tLONG $0x01f88341         // cmpl\t$1, %r8d\n\tJE   LBB10_8\n\tLONG $0xff488d41         // leal\t-1(%r8), %ecx\n\tLONG $0xfec08341         // addl\t$-2, %r8d\n\tWORD $0xc889             // movl\t%ecx, %eax\n\tWORD $0xe083; BYTE $0x03 // andl\t$3, %eax\n\tLONG $0x03f88341         // cmpl\t$3, %r8d\n\tJB   LBB10_6\n\tWORD $0xe183; BYTE $0xfc // andl\t$-4, %ecx\n\nLBB10_5:\n\tLONG $0x0f10fcc5             // vmovups\t(%rdi), %ymm1\n\tLONG $0x5710fcc5; BYTE $0x20 // vmovups\t32(%rdi), %ymm2\n\tLONG $0x5f10fcc5; BYTE $0x40 // vmovups\t64(%rdi), %ymm3\n\tLONG $0x6710fcc5; BYTE $0x60 // vmovups\t96(%rdi), %ymm4\n\tLONG $0x0e59f4c5             // vmulps\t(%rsi), %ymm1, %ymm1\n\tLONG $0xc158fcc5             // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0x4e59ecc5; BYTE $0x20 // vmulps\t32(%rsi), %ymm2, %ymm1\n\tLONG $0x5659e4c5; BYTE $0x40 // vmulps\t64(%rsi), %ymm3, %ymm2\n\tLONG $0xc158fcc5             // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0xc258fcc5             // vaddps\t%ymm2, %ymm0, %ymm0\n\tLONG $0x4e59dcc5; BYTE $0x60 // vmulps\t96(%rsi), %ymm4, %ymm1\n\tLONG $0xc158fcc5             // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0x80ef8348             // subq\t$-128, %rdi\n\tLONG $0x80ee8348             // subq\t$-128, %rsi\n\tWORD $0xc183; BYTE $0xfc     // addl\t$-4, %ecx\n\tJNE  LBB10_5\n\nLBB10_6:\n\tWORD $0xc085 // testl\t%eax, %eax\n\tJE   LBB10_8\n\nLBB10_7:\n\tLONG $0x0f10fcc5 // vmovups\t(%rdi), %ymm1\n\tLONG $0x0e59f4c5 // vmulps\t(%rsi), %ymm1, %ymm1\n\tLONG $0xc158fcc5 // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0x20c78348 // addq\t$32, %rdi\n\tLONG $0x20c68348 // addq\t$32, %rsi\n\tWORD $0xc8ff     // decl\t%eax\n\tJNE  LBB10_7\n\tJMP  LBB10_8\n\nLBB10_1:\n\tLONG $0xc057f8c5 // vxorps\t%xmm0, %xmm0, %xmm0\n\nLBB10_8:\n\tLONG $0x197de3c4; WORD $0x01c1 // vextractf128\t$1, %ymm0, %xmm1\n\tLONG $0xc058f0c5               // vaddps\t%xmm0, %xmm1, %xmm0\n\tLONG $0xc8c6f9c5; BYTE $0x01   // vshufpd\t$1, %xmm0, %xmm0, %xmm1         # xmm1 = xmm0[1,0]\n\tLONG $0xc158f8c5               // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0xc816fac5               // vmovshdup\t%xmm0, %xmm1            # xmm1 = xmm0[1,1,3,3]\n\tLONG $0xc158fac5               // vaddss\t%xmm1, %xmm0, %xmm0\n\tWORD $0x8548; BYTE $0xd2       // testq\t%rdx, %rdx\n\tJLE  LBB10_16\n\tWORD $0xd089                   // movl\t%edx, %eax\n\tLONG $0x0f10fac5               // vmovss\t(%rdi), %xmm1                   # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x0e59f2c5               // vmulss\t(%rsi), %xmm1, %xmm1\n\tLONG $0xc058f2c5               // vaddss\t%xmm0, %xmm1, %xmm0\n\tLONG $0x01f88348               // cmpq\t$1, %rax\n\tJE   LBB10_16\n\tLONG $0x4f10fac5; BYTE $0x04   // vmovss\t4(%rdi), %xmm1                  # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x4e59f2c5; BYTE $0x04   // vmulss\t4(%rsi), %xmm1, %xmm1\n\tLONG $0xc058f2c5               // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xf883; BYTE $0x02       // cmpl\t$2, %eax\n\tJE   LBB10_16\n\tLONG $0x4f10fac5; BYTE $0x08   // vmovss\t8(%rdi), %xmm1                  # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x4e59f2c5; BYTE $0x08   // vmulss\t8(%rsi), %xmm1, %xmm1\n\tLONG $0xc058f2c5               // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xf883; BYTE $0x03       // cmpl\t$3, %eax\n\tJE   LBB10_16\n\tLONG $0x4f10fac5; BYTE $0x0c   // vmovss\t12(%rdi), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x4e59f2c5; BYTE $0x0c   // vmulss\t12(%rsi), %xmm1, %xmm1\n\tLONG $0xc058f2c5               // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xf883; BYTE $0x04       // cmpl\t$4, %eax\n\tJE   LBB10_16\n\tLONG $0x4f10fac5; BYTE $0x10   // vmovss\t16(%rdi), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x4e59f2c5; BYTE $0x10   // vmulss\t16(%rsi), %xmm1, %xmm1\n\tLONG $0xc058f2c5               // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xf883; BYTE $0x05       // cmpl\t$5, %eax\n\tJE   LBB10_16\n\tLONG $0x4f10fac5; BYTE $0x14   // vmovss\t20(%rdi), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x4e59f2c5; BYTE $0x14   // vmulss\t20(%rsi), %xmm1, %xmm1\n\tLONG $0xc058f2c5               // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xf883; BYTE $0x06       // cmpl\t$6, %eax\n\tJE   LBB10_16\n\tLONG $0x4f10fac5; BYTE $0x18   // vmovss\t24(%rdi), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x4e59f2c5; BYTE $0x18   // vmulss\t24(%rsi), %xmm1, %xmm1\n\tLONG $0xc058f2c5               // vaddss\t%xmm0, %xmm1, %xmm0\n\nLBB10_16:\n\tWORD  $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE  $0x5d               // popq\t%rbp\n\tWORD  $0xf8c5; BYTE $0x77 // vzeroupper\n\tMOVSS X0, result+24(FP)\n\tRET\n\nTEXT ·_mm256_euclidean(SB), $8-32\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ n+16(FP), DX\n\tBYTE $0x55               // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5 // movq\t%rsp, %rbp\n\tLONG $0xf8e48348         // andq\t$-8, %rsp\n\tLONG $0x07428d48         // leaq\t7(%rdx), %rax\n\tWORD $0x8548; BYTE $0xd2 // testq\t%rdx, %rdx\n\tLONG $0xc2490f48         // cmovnsq\t%rdx, %rax\n\tWORD $0x8949; BYTE $0xc0 // movq\t%rax, %r8\n\tLONG $0x03f8c149         // sarq\t$3, %r8\n\tLONG $0xf8e08348         // andq\t$-8, %rax\n\tWORD $0x2948; BYTE $0xc2 // subq\t%rax, %rdx\n\tWORD $0x8545; BYTE $0xc0 // testl\t%r8d, %r8d\n\tJLE  LBB11_1\n\tLONG $0x0710fcc5         // vmovups\t(%rdi), %ymm0\n\tLONG $0x065cfcc5         // vsubps\t(%rsi), %ymm0, %ymm0\n\tLONG $0xc059fcc5         // vmulps\t%ymm0, %ymm0, %ymm0\n\tLONG $0x20c78348         // addq\t$32, %rdi\n\tLONG $0x20c68348         // addq\t$32, %rsi\n\tLONG $0x01f88341         // cmpl\t$1, %r8d\n\tJE   LBB11_8\n\tLONG $0xff488d41         // leal\t-1(%r8), %ecx\n\tLONG $0xfec08341         // addl\t$-2, %r8d\n\tWORD $0xc889             // movl\t%ecx, %eax\n\tWORD $0xe083; BYTE $0x03 // andl\t$3, %eax\n\tLONG $0x03f88341         // cmpl\t$3, %r8d\n\tJB   LBB11_6\n\tWORD $0xe183; BYTE $0xfc // andl\t$-4, %ecx\n\nLBB11_5:\n\tLONG $0x0f10fcc5             // vmovups\t(%rdi), %ymm1\n\tLONG $0x5710fcc5; BYTE $0x20 // vmovups\t32(%rdi), %ymm2\n\tLONG $0x5f10fcc5; BYTE $0x40 // vmovups\t64(%rdi), %ymm3\n\tLONG $0x6710fcc5; BYTE $0x60 // vmovups\t96(%rdi), %ymm4\n\tLONG $0x0e5cf4c5             // vsubps\t(%rsi), %ymm1, %ymm1\n\tLONG $0xc959f4c5             // vmulps\t%ymm1, %ymm1, %ymm1\n\tLONG $0xc158fcc5             // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0x4e5cecc5; BYTE $0x20 // vsubps\t32(%rsi), %ymm2, %ymm1\n\tLONG $0xc959f4c5             // vmulps\t%ymm1, %ymm1, %ymm1\n\tLONG $0xc158fcc5             // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0x4e5ce4c5; BYTE $0x40 // vsubps\t64(%rsi), %ymm3, %ymm1\n\tLONG $0xc959f4c5             // vmulps\t%ymm1, %ymm1, %ymm1\n\tLONG $0xc158fcc5             // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0x4e5cdcc5; BYTE $0x60 // vsubps\t96(%rsi), %ymm4, %ymm1\n\tLONG $0xc959f4c5             // vmulps\t%ymm1, %ymm1, %ymm1\n\tLONG $0xc158fcc5             // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0x80ef8348             // subq\t$-128, %rdi\n\tLONG $0x80ee8348             // subq\t$-128, %rsi\n\tWORD $0xc183; BYTE $0xfc     // addl\t$-4, %ecx\n\tJNE  LBB11_5\n\nLBB11_6:\n\tWORD $0xc085 // testl\t%eax, %eax\n\tJE   LBB11_8\n\nLBB11_7:\n\tLONG $0x0f10fcc5 // vmovups\t(%rdi), %ymm1\n\tLONG $0x0e5cf4c5 // vsubps\t(%rsi), %ymm1, %ymm1\n\tLONG $0xc959f4c5 // vmulps\t%ymm1, %ymm1, %ymm1\n\tLONG $0xc158fcc5 // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0x20c78348 // addq\t$32, %rdi\n\tLONG $0x20c68348 // addq\t$32, %rsi\n\tWORD $0xc8ff     // decl\t%eax\n\tJNE  LBB11_7\n\tJMP  LBB11_8\n\nLBB11_1:\n\tLONG $0xc057f8c5 // vxorps\t%xmm0, %xmm0, %xmm0\n\nLBB11_8:\n\tLONG $0x197de3c4; WORD $0x01c1 // vextractf128\t$1, %ymm0, %xmm1\n\tLONG $0xc058f0c5               // vaddps\t%xmm0, %xmm1, %xmm0\n\tLONG $0xc8c6f9c5; BYTE $0x01   // vshufpd\t$1, %xmm0, %xmm0, %xmm1         # xmm1 = xmm0[1,0]\n\tLONG $0xc158f8c5               // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0xc816fac5               // vmovshdup\t%xmm0, %xmm1            # xmm1 = xmm0[1,1,3,3]\n\tLONG $0xc158fac5               // vaddss\t%xmm1, %xmm0, %xmm0\n\tWORD $0x8548; BYTE $0xd2       // testq\t%rdx, %rdx\n\tJLE  LBB11_16\n\tLONG $0x0f10fac5               // vmovss\t(%rdi), %xmm1                   # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x0e5cf2c5               // vsubss\t(%rsi), %xmm1, %xmm1\n\tWORD $0xd089                   // movl\t%edx, %eax\n\tLONG $0xc959f2c5               // vmulss\t%xmm1, %xmm1, %xmm1\n\tLONG $0xc058f2c5               // vaddss\t%xmm0, %xmm1, %xmm0\n\tLONG $0x01f88348               // cmpq\t$1, %rax\n\tJE   LBB11_16\n\tLONG $0x4f10fac5; BYTE $0x04   // vmovss\t4(%rdi), %xmm1                  # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x4e5cf2c5; BYTE $0x04   // vsubss\t4(%rsi), %xmm1, %xmm1\n\tLONG $0xc959f2c5               // vmulss\t%xmm1, %xmm1, %xmm1\n\tLONG $0xc058f2c5               // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xf883; BYTE $0x02       // cmpl\t$2, %eax\n\tJE   LBB11_16\n\tLONG $0x4f10fac5; BYTE $0x08   // vmovss\t8(%rdi), %xmm1                  # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x4e5cf2c5; BYTE $0x08   // vsubss\t8(%rsi), %xmm1, %xmm1\n\tLONG $0xc959f2c5               // vmulss\t%xmm1, %xmm1, %xmm1\n\tLONG $0xc058f2c5               // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xf883; BYTE $0x03       // cmpl\t$3, %eax\n\tJE   LBB11_16\n\tLONG $0x4f10fac5; BYTE $0x0c   // vmovss\t12(%rdi), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x4e5cf2c5; BYTE $0x0c   // vsubss\t12(%rsi), %xmm1, %xmm1\n\tLONG $0xc959f2c5               // vmulss\t%xmm1, %xmm1, %xmm1\n\tLONG $0xc058f2c5               // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xf883; BYTE $0x04       // cmpl\t$4, %eax\n\tJE   LBB11_16\n\tLONG $0x4f10fac5; BYTE $0x10   // vmovss\t16(%rdi), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x4e5cf2c5; BYTE $0x10   // vsubss\t16(%rsi), %xmm1, %xmm1\n\tLONG $0xc959f2c5               // vmulss\t%xmm1, %xmm1, %xmm1\n\tLONG $0xc058f2c5               // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xf883; BYTE $0x05       // cmpl\t$5, %eax\n\tJE   LBB11_16\n\tLONG $0x4f10fac5; BYTE $0x14   // vmovss\t20(%rdi), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x4e5cf2c5; BYTE $0x14   // vsubss\t20(%rsi), %xmm1, %xmm1\n\tLONG $0xc959f2c5               // vmulss\t%xmm1, %xmm1, %xmm1\n\tLONG $0xc058f2c5               // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xf883; BYTE $0x06       // cmpl\t$6, %eax\n\tJE   LBB11_16\n\tLONG $0x4f10fac5; BYTE $0x18   // vmovss\t24(%rdi), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x4e5cf2c5; BYTE $0x18   // vsubss\t24(%rsi), %xmm1, %xmm1\n\tLONG $0xc959f2c5               // vmulss\t%xmm1, %xmm1, %xmm1\n\tLONG $0xc058f2c5               // vaddss\t%xmm0, %xmm1, %xmm0\n\nLBB11_16:\n\tLONG  $0xc051fac5         // vsqrtss\t%xmm0, %xmm0, %xmm0\n\tWORD  $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE  $0x5d               // popq\t%rbp\n\tWORD  $0xf8c5; BYTE $0x77 // vzeroupper\n\tMOVSS X0, result+24(FP)\n\tRET\n\nTEXT ·_mm256_mm(SB), $0-88\n\tMOVQ  transA+0(FP), DI\n\tMOVQ  transB+1(FP), SI\n\tMOVQ  m+8(FP), DX\n\tMOVQ  n+16(FP), CX\n\tMOVQ  k+24(FP), R8\n\tMOVQ  a+32(FP), R9\n\tPUSHQ ldc+72(FP)\n\tPUSHQ c+64(FP)\n\tPUSHQ ldb+56(FP)\n\tPUSHQ b+48(FP)\n\tPUSHQ lda+40(FP)\n\tPUSHQ $0\n\tBYTE  $0x55                                 // pushq\t%rbp\n\tWORD  $0x8948; BYTE $0xe5                   // movq\t%rsp, %rbp\n\tWORD  $0x5741                               // pushq\t%r15\n\tWORD  $0x5641                               // pushq\t%r14\n\tWORD  $0x5541                               // pushq\t%r13\n\tWORD  $0x5441                               // pushq\t%r12\n\tBYTE  $0x53                                 // pushq\t%rbx\n\tLONG  $0xf0e48348                           // andq\t$-16, %rsp\n\tLONG  $0x90ec8148; WORD $0x0000; BYTE $0x00 // subq\t$144, %rsp\n\tLONG  $0x244c894c; BYTE $0x08               // movq\t%r9, 8(%rsp)                    # 8-byte Spill\n\tWORD  $0x8948; BYTE $0xcb                   // movq\t%rcx, %rbx\n\tLONG  $0x24148948                           // movq\t%rdx, (%rsp)                    # 8-byte Spill\n\tLONG  $0x30458b48                           // movq\t48(%rbp), %rax\n\tLONG  $0x24448948; BYTE $0x68               // movq\t%rax, 104(%rsp)                 # 8-byte Spill\n\tLONG  $0x28458b48                           // movq\t40(%rbp), %rax\n\tLONG  $0x24448948; BYTE $0x70               // movq\t%rax, 112(%rsp)                 # 8-byte Spill\n\tLONG  $0x206d8b4c                           // movq\t32(%rbp), %r13\n\tLONG  $0x18458b48                           // movq\t24(%rbp), %rax\n\tLONG  $0x24448948; BYTE $0x60               // movq\t%rax, 96(%rsp)                  # 8-byte Spill\n\tWORD  $0xf889                               // movl\t%edi, %eax\n\tWORD  $0x0840; BYTE $0xf0                   // orb\t%sil, %al\n\tLONG  $0x2444894c; BYTE $0x58               // movq\t%r8, 88(%rsp)                   # 8-byte Spill\n\tJE    LBB12_1\n\tWORD  $0xf089                               // movl\t%esi, %eax\n\tWORD  $0x0134                               // xorb\t$1, %al\n\tWORD  $0x0840; BYTE $0xf8                   // orb\t%dil, %al\n\tJE    LBB12_16\n\tWORD  $0xf989                               // movl\t%edi, %ecx\n\tWORD  $0xf180; BYTE $0x01                   // xorb\t$1, %cl\n\tWORD  $0x0840; BYTE $0xf1                   // orb\t%sil, %cl\n\tWORD  $0x854d; BYTE $0xc0                   // testq\t%r8, %r8\n\tWORD  $0x9f0f; BYTE $0xc2                   // setg\t%dl\n\tWORD  $0x8548; BYTE $0xdb                   // testq\t%rbx, %rbx\n\tWORD  $0x9f0f; BYTE $0xc0                   // setg\t%al\n\tWORD  $0xd020                               // andb\t%dl, %al\n\tWORD  $0xc984                               // testb\t%cl, %cl\n\tJE    LBB12_88\n\tLONG  $0x243c8348; BYTE $0x00               // cmpq\t$0, (%rsp)                      # 8-byte Folded Reload\n\tWORD  $0x9f0f; BYTE $0xc1                   // setg\t%cl\n\tWORD  $0x2040; BYTE $0xf0                   // andb\t%sil, %al\n\tWORD  $0x2040; BYTE $0xf9                   // andb\t%dil, %cl\n\tWORD  $0xc120                               // andb\t%al, %cl\n\tWORD  $0xf980; BYTE $0x01                   // cmpb\t$1, %cl\n\tJNE   LBB12_117\n\tLONG  $0x30458b48                           // movq\t48(%rbp), %rax\n\tQUAD  $0x00000000853c8d48                   // leaq\t(,%rax,4), %rdi\n\tLONG  $0xff408d49                           // leaq\t-1(%r8), %rax\n\tLONG  $0x45af0f48; BYTE $0x10               // imulq\t16(%rbp), %rax\n\tLONG  $0x180c8d49                           // leaq\t(%r8,%rbx), %rcx\n\tLONG  $0x18758b48                           // movq\t24(%rbp), %rsi\n\tLONG  $0x8e0c8d48                           // leaq\t(%rsi,%rcx,4), %rcx\n\tLONG  $0xfcc18348                           // addq\t$-4, %rcx\n\tLONG  $0x244c8948; BYTE $0x28               // movq\t%rcx, 40(%rsp)                  # 8-byte Spill\n\tLONG  $0x284d8b48                           // movq\t40(%rbp), %rcx\n\tLONG  $0x99148d48                           // leaq\t(%rcx,%rbx,4), %rdx\n\tLONG  $0x24548948; BYTE $0x20               // movq\t%rdx, 32(%rsp)                  # 8-byte Spill\n\tLONG  $0x24548b48; BYTE $0x08               // movq\t8(%rsp), %rdx                   # 8-byte Reload\n\tLONG  $0x82048d48                           // leaq\t(%rdx,%rax,4), %rax\n\tLONG  $0x04c08348                           // addq\t$4, %rax\n\tLONG  $0x24448948; BYTE $0x18               // movq\t%rax, 24(%rsp)                  # 8-byte Spill\n\tLONG  $0x20fb8348                           // cmpq\t$32, %rbx\n\tWORD  $0x930f; BYTE $0xc0                   // setae\t%al\n\tLONG  $0x01fd8349                           // cmpq\t$1, %r13\n\tLONG  $0xc0940f41                           // sete\t%r8b\n\tWORD  $0x2041; BYTE $0xc0                   // andb\t%al, %r8b\n\tQUAD  $0xffffffffffe0bc49; WORD $0x7fff     // movabsq\t$9223372036854775776, %r12      # imm = 0x7FFFFFFFFFFFFFE0\n\tWORD  $0x2149; BYTE $0xdc                   // andq\t%rbx, %r12\n\tLONG  $0xff438d48                           // leaq\t-1(%rbx), %rax\n\tLONG  $0x24448948; BYTE $0x50               // movq\t%rax, 80(%rsp)                  # 8-byte Spill\n\tLONG  $0x60468d48                           // leaq\t96(%rsi), %rax\n\tQUAD  $0x0000008024848948                   // movq\t%rax, 128(%rsp)                 # 8-byte Spill\n\tLONG  $0x60718d4c                           // leaq\t96(%rcx), %r14\n\tQUAD  $0x00000000ad048d4a                   // leaq\t(,%r13,4), %rax\n\tLONG  $0x24448948; BYTE $0x40               // movq\t%rax, 64(%rsp)                  # 8-byte Spill\n\tQUAD  $0x00000000ed1c8d4e                   // leaq\t(,%r13,8), %r11\n\tLONG  $0x04518d48                           // leaq\t4(%rcx), %rdx\n\tWORD  $0xf641; BYTE $0xd0                   // notb\t%r8b\n\tLONG  $0x24448844; BYTE $0x78               // movb\t%r8b, 120(%rsp)                 # 1-byte Spill\n\tWORD  $0x3145; BYTE $0xc0                   // xorl\t%r8d, %r8d\n\tLONG  $0x247c8948; BYTE $0x30               // movq\t%rdi, 48(%rsp)                  # 8-byte Spill\n\tJMP   LBB12_104\n\nLBB12_116:\n\tLONG $0x24448b4c; BYTE $0x38 // movq\t56(%rsp), %r8                   # 8-byte Reload\n\tWORD $0xff49; BYTE $0xc0     // incq\t%r8\n\tLONG $0x247c8b48; BYTE $0x30 // movq\t48(%rsp), %rdi                  # 8-byte Reload\n\tWORD $0x0149; BYTE $0xfe     // addq\t%rdi, %r14\n\tWORD $0x0148; BYTE $0xfa     // addq\t%rdi, %rdx\n\tLONG $0x24043b4c             // cmpq\t(%rsp), %r8                     # 8-byte Folded Reload\n\tJE   LBB12_117\n\nLBB12_104:\n\tQUAD $0x000000000000b848; WORD $0x2000 // movabsq\t$2305843009213693952, %rax      # imm = 0x2000000000000000\n\tLONG $0x10458548                       // testq\t%rax, 16(%rbp)\n\tWORD $0x950f; BYTE $0xc0               // setne\t%al\n\tWORD $0x8948; BYTE $0xf9               // movq\t%rdi, %rcx\n\tLONG $0xc8af0f49                       // imulq\t%r8, %rcx\n\tLONG $0x284d8b4c                       // movq\t40(%rbp), %r9\n\tLONG $0x09348d49                       // leaq\t(%r9,%rcx), %rsi\n\tLONG $0x244c0348; BYTE $0x20           // addq\t32(%rsp), %rcx                  # 8-byte Folded Reload\n\tLONG $0x247c8b48; BYTE $0x08           // movq\t8(%rsp), %rdi                   # 8-byte Reload\n\tLONG $0x87148d4e                       // leaq\t(%rdi,%r8,4), %r10\n\tLONG $0x247c8b48; BYTE $0x18           // movq\t24(%rsp), %rdi                  # 8-byte Reload\n\tLONG $0x873c8d4a                       // leaq\t(%rdi,%r8,4), %rdi\n\tLONG $0x2444894c; BYTE $0x38           // movq\t%r8, 56(%rsp)                   # 8-byte Spill\n\tLONG $0x45af0f4c; BYTE $0x30           // imulq\t48(%rbp), %r8\n\tLONG $0x81048d4f                       // leaq\t(%r9,%r8,4), %r8\n\tLONG $0x2444894c; BYTE $0x48           // movq\t%r8, 72(%rsp)                   # 8-byte Spill\n\tWORD $0x3948; BYTE $0xfe               // cmpq\t%rdi, %rsi\n\tLONG $0xc7920f40                       // setb\t%dil\n\tWORD $0x3949; BYTE $0xca               // cmpq\t%rcx, %r10\n\tLONG $0xc0920f41                       // setb\t%r8b\n\tWORD $0x2041; BYTE $0xf8               // andb\t%dil, %r8b\n\tWORD $0x0841; BYTE $0xc0               // orb\t%al, %r8b\n\tLONG $0x24743b48; BYTE $0x28           // cmpq\t40(%rsp), %rsi                  # 8-byte Folded Reload\n\tWORD $0x920f; BYTE $0xc0               // setb\t%al\n\tLONG $0x187d8b48                       // movq\t24(%rbp), %rdi\n\tWORD $0x3948; BYTE $0xf9               // cmpq\t%rdi, %rcx\n\tWORD $0x970f; BYTE $0xc1               // seta\t%cl\n\tWORD $0xc120                           // andb\t%al, %cl\n\tWORD $0x0844; BYTE $0xc1               // orb\t%r8b, %cl\n\tLONG $0x78244c0a                       // orb\t120(%rsp), %cl                  # 1-byte Folded Reload\n\tLONG $0x10244c88                       // movb\t%cl, 16(%rsp)                   # 1-byte Spill\n\tQUAD $0x00000080248c8b4c               // movq\t128(%rsp), %r9                  # 8-byte Reload\n\tWORD $0xf631                           // xorl\t%esi, %esi\n\tJMP  LBB12_105\n\nLBB12_115:\n\tWORD $0xff48; BYTE $0xc6     // incq\t%rsi\n\tLONG $0x04c18349             // addq\t$4, %r9\n\tLONG $0x04c78348             // addq\t$4, %rdi\n\tLONG $0x24743b48; BYTE $0x58 // cmpq\t88(%rsp), %rsi                  # 8-byte Folded Reload\n\tJE   LBB12_116\n\nLBB12_105:\n\tWORD $0x8948; BYTE $0xf0     // movq\t%rsi, %rax\n\tLONG $0x45af0f48; BYTE $0x10 // imulq\t16(%rbp), %rax\n\tLONG $0x102444f6; BYTE $0x01 // testb\t$1, 16(%rsp)                    # 1-byte Folded Reload\n\tJE   LBB12_107\n\tWORD $0x3145; BYTE $0xc0     // xorl\t%r8d, %r8d\n\tJMP  LBB12_110\n\nLBB12_107:\n\tLONG $0x187dc2c4; WORD $0x8204 // vbroadcastss\t(%r10,%rax,4), %ymm0\n\tWORD $0xc931                   // xorl\t%ecx, %ecx\n\nLBB12_108:\n\tLONG $0x597cc1c4; WORD $0x894c; BYTE $0xa0 // vmulps\t-96(%r9,%rcx,4), %ymm0, %ymm1\n\tLONG $0x5874c1c4; WORD $0x8e4c; BYTE $0xa0 // vaddps\t-96(%r14,%rcx,4), %ymm1, %ymm1\n\tLONG $0x597cc1c4; WORD $0x8954; BYTE $0xc0 // vmulps\t-64(%r9,%rcx,4), %ymm0, %ymm2\n\tLONG $0x586cc1c4; WORD $0x8e54; BYTE $0xc0 // vaddps\t-64(%r14,%rcx,4), %ymm2, %ymm2\n\tLONG $0x597cc1c4; WORD $0x895c; BYTE $0xe0 // vmulps\t-32(%r9,%rcx,4), %ymm0, %ymm3\n\tLONG $0x5864c1c4; WORD $0x8e5c; BYTE $0xe0 // vaddps\t-32(%r14,%rcx,4), %ymm3, %ymm3\n\tLONG $0x597cc1c4; WORD $0x8924             // vmulps\t(%r9,%rcx,4), %ymm0, %ymm4\n\tLONG $0x585cc1c4; WORD $0x8e24             // vaddps\t(%r14,%rcx,4), %ymm4, %ymm4\n\tLONG $0x117cc1c4; WORD $0x8e4c; BYTE $0xa0 // vmovups\t%ymm1, -96(%r14,%rcx,4)\n\tLONG $0x117cc1c4; WORD $0x8e54; BYTE $0xc0 // vmovups\t%ymm2, -64(%r14,%rcx,4)\n\tLONG $0x117cc1c4; WORD $0x8e5c; BYTE $0xe0 // vmovups\t%ymm3, -32(%r14,%rcx,4)\n\tLONG $0x117cc1c4; WORD $0x8e24             // vmovups\t%ymm4, (%r14,%rcx,4)\n\tLONG $0x20c18348                           // addq\t$32, %rcx\n\tWORD $0x3949; BYTE $0xcc                   // cmpq\t%rcx, %r12\n\tJNE  LBB12_108\n\tWORD $0x894d; BYTE $0xe0                   // movq\t%r12, %r8\n\tWORD $0x3949; BYTE $0xdc                   // cmpq\t%rbx, %r12\n\tJE   LBB12_115\n\nLBB12_110:\n\tWORD $0x894d; BYTE $0xc7       // movq\t%r8, %r15\n\tWORD $0xc3f6; BYTE $0x01       // testb\t$1, %bl\n\tJE   LBB12_112\n\tLONG $0x184d8b48               // movq\t24(%rbp), %rcx\n\tLONG $0xb10c8d48               // leaq\t(%rcx,%rsi,4), %rcx\n\tLONG $0x107ac1c4; WORD $0x8204 // vmovss\t(%r10,%rax,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tWORD $0x894d; BYTE $0xc7       // movq\t%r8, %r15\n\tLONG $0x7daf0f4c; BYTE $0x20   // imulq\t32(%rbp), %r15\n\tLONG $0x597aa1c4; WORD $0xb904 // vmulss\t(%rcx,%r15,4), %xmm0, %xmm0\n\tLONG $0x244c8b48; BYTE $0x48   // movq\t72(%rsp), %rcx                  # 8-byte Reload\n\tLONG $0x587aa1c4; WORD $0x8104 // vaddss\t(%rcx,%r8,4), %xmm0, %xmm0\n\tLONG $0x117aa1c4; WORD $0x8104 // vmovss\t%xmm0, (%rcx,%r8,4)\n\tWORD $0x894d; BYTE $0xc7       // movq\t%r8, %r15\n\tLONG $0x01cf8349               // orq\t$1, %r15\n\nLBB12_112:\n\tLONG $0x24443b4c; BYTE $0x50 // cmpq\t80(%rsp), %r8                   # 8-byte Folded Reload\n\tJE   LBB12_115\n\tLONG $0x244c8b48; BYTE $0x40 // movq\t64(%rsp), %rcx                  # 8-byte Reload\n\tWORD $0x8949; BYTE $0xcd     // movq\t%rcx, %r13\n\tLONG $0xefaf0f4d             // imulq\t%r15, %r13\n\tLONG $0x01478d4d             // leaq\t1(%r15), %r8\n\tLONG $0xc1af0f4c             // imulq\t%rcx, %r8\n\tWORD $0x8948; BYTE $0xf9     // movq\t%rdi, %rcx\n\nLBB12_114:\n\tLONG $0x107ac1c4; WORD $0x8204             // vmovss\t(%r10,%rax,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x597aa1c4; WORD $0x2904             // vmulss\t(%rcx,%r13), %xmm0, %xmm0\n\tLONG $0x587aa1c4; WORD $0xba44; BYTE $0xfc // vaddss\t-4(%rdx,%r15,4), %xmm0, %xmm0\n\tLONG $0x117aa1c4; WORD $0xba44; BYTE $0xfc // vmovss\t%xmm0, -4(%rdx,%r15,4)\n\tLONG $0x107ac1c4; WORD $0x8204             // vmovss\t(%r10,%rax,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x597aa1c4; WORD $0x0104             // vmulss\t(%rcx,%r8), %xmm0, %xmm0\n\tLONG $0x587aa1c4; WORD $0xba04             // vaddss\t(%rdx,%r15,4), %xmm0, %xmm0\n\tLONG $0x117aa1c4; WORD $0xba04             // vmovss\t%xmm0, (%rdx,%r15,4)\n\tLONG $0x02c78349                           // addq\t$2, %r15\n\tWORD $0x014c; BYTE $0xd9                   // addq\t%r11, %rcx\n\tWORD $0x394c; BYTE $0xfb                   // cmpq\t%r15, %rbx\n\tJNE  LBB12_114\n\tJMP  LBB12_115\n\nLBB12_1:\n\tLONG $0x243c8348; BYTE $0x00           // cmpq\t$0, (%rsp)                      # 8-byte Folded Reload\n\tWORD $0x9e0f; BYTE $0xc0               // setle\t%al\n\tWORD $0x854d; BYTE $0xc0               // testq\t%r8, %r8\n\tWORD $0x9e0f; BYTE $0xc1               // setle\t%cl\n\tWORD $0xc108                           // orb\t%al, %cl\n\tWORD $0x8548; BYTE $0xdb               // testq\t%rbx, %rbx\n\tWORD $0x9e0f; BYTE $0xc0               // setle\t%al\n\tWORD $0xc808                           // orb\t%cl, %al\n\tJNE  LBB12_117\n\tLONG $0x30458b48                       // movq\t48(%rbp), %rax\n\tQUAD $0x00000000853c8d48               // leaq\t(,%rax,4), %rdi\n\tLONG $0x10458b48                       // movq\t16(%rbp), %rax\n\tQUAD $0x0000000085048d48               // leaq\t(,%rax,4), %rax\n\tLONG $0x24448948; BYTE $0x38           // movq\t%rax, 56(%rsp)                  # 8-byte Spill\n\tLONG $0xff408d49                       // leaq\t-1(%r8), %rax\n\tLONG $0xc5af0f49                       // imulq\t%r13, %rax\n\tWORD $0x0148; BYTE $0xd8               // addq\t%rbx, %rax\n\tLONG $0x18758b48                       // movq\t24(%rbp), %rsi\n\tLONG $0x86048d48                       // leaq\t(%rsi,%rax,4), %rax\n\tLONG $0x24448948; BYTE $0x30           // movq\t%rax, 48(%rsp)                  # 8-byte Spill\n\tLONG $0x28458b48                       // movq\t40(%rbp), %rax\n\tLONG $0x980c8d48                       // leaq\t(%rax,%rbx,4), %rcx\n\tLONG $0x244c8948; BYTE $0x28           // movq\t%rcx, 40(%rsp)                  # 8-byte Spill\n\tLONG $0x244c8b48; BYTE $0x08           // movq\t8(%rsp), %rcx                   # 8-byte Reload\n\tLONG $0x810c8d4a                       // leaq\t(%rcx,%r8,4), %rcx\n\tLONG $0x244c8948; BYTE $0x20           // movq\t%rcx, 32(%rsp)                  # 8-byte Spill\n\tQUAD $0xffffffffffe0bc49; WORD $0x7fff // movabsq\t$9223372036854775776, %r12      # imm = 0x7FFFFFFFFFFFFFE0\n\tWORD $0x2149; BYTE $0xdc               // andq\t%rbx, %r12\n\tLONG $0xff4b8d48                       // leaq\t-1(%rbx), %rcx\n\tLONG $0x244c8948; BYTE $0x10           // movq\t%rcx, 16(%rsp)                  # 8-byte Spill\n\tLONG $0x604e8d48                       // leaq\t96(%rsi), %rcx\n\tLONG $0x244c8948; BYTE $0x18           // movq\t%rcx, 24(%rsp)                  # 8-byte Spill\n\tQUAD $0x00000000ad148d4a               // leaq\t(,%r13,4), %rdx\n\tLONG $0x60708d4c                       // leaq\t96(%rax), %r14\n\tLONG $0x044e8d48                       // leaq\t4(%rsi), %rcx\n\tLONG $0x244c8948; BYTE $0x78           // movq\t%rcx, 120(%rsp)                 # 8-byte Spill\n\tLONG $0x04588d4c                       // leaq\t4(%rax), %r11\n\tWORD $0x3145; BYTE $0xff               // xorl\t%r15d, %r15d\n\tLONG $0x247c8948; BYTE $0x40           // movq\t%rdi, 64(%rsp)                  # 8-byte Spill\n\tJMP  LBB12_3\n\nLBB12_14:\n\tLONG $0x247c8b4c; BYTE $0x50 // movq\t80(%rsp), %r15                  # 8-byte Reload\n\tWORD $0xff49; BYTE $0xc7     // incq\t%r15\n\tLONG $0x247c8b48; BYTE $0x40 // movq\t64(%rsp), %rdi                  # 8-byte Reload\n\tWORD $0x0149; BYTE $0xfe     // addq\t%rdi, %r14\n\tWORD $0x0149; BYTE $0xfb     // addq\t%rdi, %r11\n\tLONG $0x243c3b4c             // cmpq\t(%rsp), %r15                    # 8-byte Folded Reload\n\tLONG $0x206d8b4c             // movq\t32(%rbp), %r13\n\tJE   LBB12_117\n\nLBB12_3:\n\tQUAD $0x000000000000b848; WORD $0x2000 // movabsq\t$2305843009213693952, %rax      # imm = 0x2000000000000000\n\tWORD $0x8549; BYTE $0xc5               // testq\t%rax, %r13\n\tLONG $0x2444950f; BYTE $0x48           // setne\t72(%rsp)                        # 1-byte Folded Spill\n\tWORD $0x8948; BYTE $0xf9               // movq\t%rdi, %rcx\n\tLONG $0xcfaf0f49                       // imulq\t%r15, %rcx\n\tLONG $0x286d8b4c                       // movq\t40(%rbp), %r13\n\tLONG $0x29348d4a                       // leaq\t(%rcx,%r13), %rsi\n\tLONG $0x244c0348; BYTE $0x28           // addq\t40(%rsp), %rcx                  # 8-byte Folded Reload\n\tLONG $0x247c8b48; BYTE $0x38           // movq\t56(%rsp), %rdi                  # 8-byte Reload\n\tLONG $0xffaf0f49                       // imulq\t%r15, %rdi\n\tLONG $0x24448b48; BYTE $0x08           // movq\t8(%rsp), %rax                   # 8-byte Reload\n\tLONG $0x380c8d4c                       // leaq\t(%rax,%rdi), %r9\n\tLONG $0x247c0348; BYTE $0x20           // addq\t32(%rsp), %rdi                  # 8-byte Folded Reload\n\tWORD $0x894d; BYTE $0xfa               // movq\t%r15, %r10\n\tLONG $0x55af0f4c; BYTE $0x10           // imulq\t16(%rbp), %r10\n\tLONG $0x247c894c; BYTE $0x50           // movq\t%r15, 80(%rsp)                  # 8-byte Spill\n\tLONG $0x7daf0f4c; BYTE $0x30           // imulq\t48(%rbp), %r15\n\tWORD $0x3948; BYTE $0xfe               // cmpq\t%rdi, %rsi\n\tLONG $0xc7920f40                       // setb\t%dil\n\tWORD $0x3949; BYTE $0xc9               // cmpq\t%rcx, %r9\n\tLONG $0x90148d4e                       // leaq\t(%rax,%r10,4), %r10\n\tLONG $0xbd4c8d4f; BYTE $0x00           // leaq\t(%r13,%r15,4), %r9\n\tLONG $0xc5920f41                       // setb\t%r13b\n\tWORD $0x2041; BYTE $0xfd               // andb\t%dil, %r13b\n\tLONG $0x24743b48; BYTE $0x30           // cmpq\t48(%rsp), %rsi                  # 8-byte Folded Reload\n\tLONG $0xc6920f40                       // setb\t%sil\n\tLONG $0x184d3b48                       // cmpq\t24(%rbp), %rcx\n\tWORD $0x970f; BYTE $0xc1               // seta\t%cl\n\tWORD $0x2040; BYTE $0xf1               // andb\t%sil, %cl\n\tLONG $0x246c0a44; BYTE $0x48           // orb\t72(%rsp), %r13b                 # 1-byte Folded Reload\n\tWORD $0x0841; BYTE $0xcd               // orb\t%cl, %r13b\n\tLONG $0x24448b48; BYTE $0x78           // movq\t120(%rsp), %rax                 # 8-byte Reload\n\tLONG $0x244c8b48; BYTE $0x18           // movq\t24(%rsp), %rcx                  # 8-byte Reload\n\tWORD $0xff31                           // xorl\t%edi, %edi\n\tJMP  LBB12_4\n\nLBB12_13:\n\tWORD $0xff48; BYTE $0xc7 // incq\t%rdi\n\tWORD $0x0148; BYTE $0xd1 // addq\t%rdx, %rcx\n\tWORD $0x0148; BYTE $0xd0 // addq\t%rdx, %rax\n\tWORD $0x394c; BYTE $0xc7 // cmpq\t%r8, %rdi\n\tJE   LBB12_14\n\nLBB12_4:\n\tLONG $0x20fb8348         // cmpq\t$32, %rbx\n\tLONG $0xc6920f40         // setb\t%sil\n\tWORD $0x0844; BYTE $0xee // orb\t%r13b, %sil\n\tLONG $0x01c6f640         // testb\t$1, %sil\n\tJE   LBB12_6\n\tWORD $0xf631             // xorl\t%esi, %esi\n\tJMP  LBB12_9\n\nLBB12_6:\n\tLONG $0x187dc2c4; WORD $0xba04 // vbroadcastss\t(%r10,%rdi,4), %ymm0\n\tWORD $0xf631                   // xorl\t%esi, %esi\n\nLBB12_7:\n\tLONG $0x4c59fcc5; WORD $0xa0b1             // vmulps\t-96(%rcx,%rsi,4), %ymm0, %ymm1\n\tLONG $0x5874c1c4; WORD $0xb64c; BYTE $0xa0 // vaddps\t-96(%r14,%rsi,4), %ymm1, %ymm1\n\tLONG $0x5459fcc5; WORD $0xc0b1             // vmulps\t-64(%rcx,%rsi,4), %ymm0, %ymm2\n\tLONG $0x586cc1c4; WORD $0xb654; BYTE $0xc0 // vaddps\t-64(%r14,%rsi,4), %ymm2, %ymm2\n\tLONG $0x5c59fcc5; WORD $0xe0b1             // vmulps\t-32(%rcx,%rsi,4), %ymm0, %ymm3\n\tLONG $0x5864c1c4; WORD $0xb65c; BYTE $0xe0 // vaddps\t-32(%r14,%rsi,4), %ymm3, %ymm3\n\tLONG $0x2459fcc5; BYTE $0xb1               // vmulps\t(%rcx,%rsi,4), %ymm0, %ymm4\n\tLONG $0x585cc1c4; WORD $0xb624             // vaddps\t(%r14,%rsi,4), %ymm4, %ymm4\n\tLONG $0x117cc1c4; WORD $0xb64c; BYTE $0xa0 // vmovups\t%ymm1, -96(%r14,%rsi,4)\n\tLONG $0x117cc1c4; WORD $0xb654; BYTE $0xc0 // vmovups\t%ymm2, -64(%r14,%rsi,4)\n\tLONG $0x117cc1c4; WORD $0xb65c; BYTE $0xe0 // vmovups\t%ymm3, -32(%r14,%rsi,4)\n\tLONG $0x117cc1c4; WORD $0xb624             // vmovups\t%ymm4, (%r14,%rsi,4)\n\tLONG $0x20c68348                           // addq\t$32, %rsi\n\tWORD $0x3949; BYTE $0xf4                   // cmpq\t%rsi, %r12\n\tJNE  LBB12_7\n\tWORD $0x894c; BYTE $0xe6                   // movq\t%r12, %rsi\n\tWORD $0x3949; BYTE $0xdc                   // cmpq\t%rbx, %r12\n\tJE   LBB12_13\n\nLBB12_9:\n\tWORD $0x8949; BYTE $0xf7       // movq\t%rsi, %r15\n\tWORD $0xc3f6; BYTE $0x01       // testb\t$1, %bl\n\tJE   LBB12_11\n\tWORD $0x8949; BYTE $0xff       // movq\t%rdi, %r15\n\tLONG $0x7daf0f4c; BYTE $0x20   // imulq\t32(%rbp), %r15\n\tLONG $0x18458b4c               // movq\t24(%rbp), %r8\n\tLONG $0xb83c8d4f               // leaq\t(%r8,%r15,4), %r15\n\tLONG $0x24448b4c; BYTE $0x58   // movq\t88(%rsp), %r8                   # 8-byte Reload\n\tLONG $0x107ac1c4; WORD $0xba04 // vmovss\t(%r10,%rdi,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x597ac1c4; WORD $0xb704 // vmulss\t(%r15,%rsi,4), %xmm0, %xmm0\n\tLONG $0x587ac1c4; WORD $0xb104 // vaddss\t(%r9,%rsi,4), %xmm0, %xmm0\n\tLONG $0x117ac1c4; WORD $0xb104 // vmovss\t%xmm0, (%r9,%rsi,4)\n\tWORD $0x8949; BYTE $0xf7       // movq\t%rsi, %r15\n\tLONG $0x01cf8349               // orq\t$1, %r15\n\nLBB12_11:\n\tLONG $0x24743b48; BYTE $0x10 // cmpq\t16(%rsp), %rsi                  # 8-byte Folded Reload\n\tJE   LBB12_13\n\nLBB12_12:\n\tLONG $0x107ac1c4; WORD $0xba04             // vmovss\t(%r10,%rdi,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x597aa1c4; WORD $0xb844; BYTE $0xfc // vmulss\t-4(%rax,%r15,4), %xmm0, %xmm0\n\tLONG $0x587a81c4; WORD $0xbb44; BYTE $0xfc // vaddss\t-4(%r11,%r15,4), %xmm0, %xmm0\n\tLONG $0x117a81c4; WORD $0xbb44; BYTE $0xfc // vmovss\t%xmm0, -4(%r11,%r15,4)\n\tLONG $0x107ac1c4; WORD $0xba04             // vmovss\t(%r10,%rdi,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x597aa1c4; WORD $0xb804             // vmulss\t(%rax,%r15,4), %xmm0, %xmm0\n\tLONG $0x587a81c4; WORD $0xbb04             // vaddss\t(%r11,%r15,4), %xmm0, %xmm0\n\tLONG $0x117a81c4; WORD $0xbb04             // vmovss\t%xmm0, (%r11,%r15,4)\n\tLONG $0x02c78349                           // addq\t$2, %r15\n\tWORD $0x394c; BYTE $0xfb                   // cmpq\t%r15, %rbx\n\tJNE  LBB12_12\n\tJMP  LBB12_13\n\nLBB12_16:\n\tLONG $0x243c8348; BYTE $0x00 // cmpq\t$0, (%rsp)                      # 8-byte Folded Reload\n\tJLE  LBB12_117\n\tLONG $0x07408d49             // leaq\t7(%r8), %rax\n\tWORD $0x854d; BYTE $0xc0     // testq\t%r8, %r8\n\tLONG $0xc0490f49             // cmovnsq\t%r8, %rax\n\tWORD $0x8548; BYTE $0xdb     // testq\t%rbx, %rbx\n\tJLE  LBB12_117\n\tWORD $0x8948; BYTE $0xc1     // movq\t%rax, %rcx\n\tLONG $0xf8e18348             // andq\t$-8, %rcx\n\tWORD $0x2949; BYTE $0xc8     // subq\t%rcx, %r8\n\tLONG $0x03f8c148             // sarq\t$3, %rax\n\tWORD $0xf883; BYTE $0x02     // cmpl\t$2, %eax\n\tJL   LBB12_47\n\tWORD $0x854d; BYTE $0xc0     // testq\t%r8, %r8\n\tJLE  LBB12_20\n\tWORD $0x8944; BYTE $0xc6     // movl\t%r8d, %esi\n\tWORD $0x788d; BYTE $0xff     // leal\t-1(%rax), %edi\n\tLONG $0xfe408d44             // leal\t-2(%rax), %r8d\n\tWORD $0x8941; BYTE $0xfb     // movl\t%edi, %r11d\n\tLONG $0x03e38341             // andl\t$3, %r11d\n\tWORD $0xe783; BYTE $0xfc     // andl\t$-4, %edi\n\tWORD $0x3145; BYTE $0xc9     // xorl\t%r9d, %r9d\n\tJMP  LBB12_25\n\nLBB12_39:\n\tWORD $0xff49; BYTE $0xc1 // incq\t%r9\n\tLONG $0x240c3b4c         // cmpq\t(%rsp), %r9                     # 8-byte Folded Reload\n\tJE   LBB12_117\n\nLBB12_25:\n\tWORD $0x894c; BYTE $0xc8     // movq\t%r9, %rax\n\tLONG $0x45af0f48; BYTE $0x10 // imulq\t16(%rbp), %rax\n\tLONG $0x244c8b48; BYTE $0x08 // movq\t8(%rsp), %rcx                   # 8-byte Reload\n\tLONG $0x813c8d4c             // leaq\t(%rcx,%rax,4), %r15\n\tLONG $0x81148d4c             // leaq\t(%rcx,%rax,4), %r10\n\tLONG $0x20c28349             // addq\t$32, %r10\n\tWORD $0x894c; BYTE $0xc8     // movq\t%r9, %rax\n\tLONG $0x45af0f48; BYTE $0x30 // imulq\t48(%rbp), %rax\n\tLONG $0x284d8b48             // movq\t40(%rbp), %rcx\n\tLONG $0x81348d4c             // leaq\t(%rcx,%rax,4), %r14\n\tWORD $0xd231                 // xorl\t%edx, %edx\n\tJMP  LBB12_26\n\nLBB12_38:\n\tLONG $0x117ac1c4; WORD $0x9604 // vmovss\t%xmm0, (%r14,%rdx,4)\n\tWORD $0xff48; BYTE $0xc2       // incq\t%rdx\n\tWORD $0x3948; BYTE $0xda       // cmpq\t%rbx, %rdx\n\tLONG $0x206d8b4c               // movq\t32(%rbp), %r13\n\tJE   LBB12_39\n\nLBB12_26:\n\tWORD $0x8948; BYTE $0xd0     // movq\t%rdx, %rax\n\tLONG $0xc5af0f49             // imulq\t%r13, %rax\n\tLONG $0x107cc1c4; BYTE $0x07 // vmovups\t(%r15), %ymm0\n\tLONG $0x184d8b48             // movq\t24(%rbp), %rcx\n\tLONG $0x0459fcc5; BYTE $0x81 // vmulps\t(%rcx,%rax,4), %ymm0, %ymm0\n\tLONG $0x812c8d4c             // leaq\t(%rcx,%rax,4), %r13\n\tLONG $0x20c58349             // addq\t$32, %r13\n\tWORD $0xf889                 // movl\t%edi, %eax\n\tWORD $0x894d; BYTE $0xd4     // movq\t%r10, %r12\n\tLONG $0x03f88341             // cmpl\t$3, %r8d\n\tJB   LBB12_28\n\nLBB12_27:\n\tLONG $0x107cc1c4; WORD $0x240c             // vmovups\t(%r12), %ymm1\n\tLONG $0x107cc1c4; WORD $0x2454; BYTE $0x20 // vmovups\t32(%r12), %ymm2\n\tLONG $0x107cc1c4; WORD $0x245c; BYTE $0x40 // vmovups\t64(%r12), %ymm3\n\tLONG $0x107cc1c4; WORD $0x2464; BYTE $0x60 // vmovups\t96(%r12), %ymm4\n\tLONG $0x5974c1c4; WORD $0x004d             // vmulps\t(%r13), %ymm1, %ymm1\n\tLONG $0xc158fcc5                           // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0x596cc1c4; WORD $0x204d             // vmulps\t32(%r13), %ymm2, %ymm1\n\tLONG $0x5964c1c4; WORD $0x4055             // vmulps\t64(%r13), %ymm3, %ymm2\n\tLONG $0xc158fcc5                           // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0xc258fcc5                           // vaddps\t%ymm2, %ymm0, %ymm0\n\tLONG $0x595cc1c4; WORD $0x604d             // vmulps\t96(%r13), %ymm4, %ymm1\n\tLONG $0xc158fcc5                           // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0x80ec8349                           // subq\t$-128, %r12\n\tLONG $0x80ed8349                           // subq\t$-128, %r13\n\tWORD $0xc083; BYTE $0xfc                   // addl\t$-4, %eax\n\tJNE  LBB12_27\n\nLBB12_28:\n\tWORD $0x8545; BYTE $0xdb // testl\t%r11d, %r11d\n\tJE   LBB12_31\n\tWORD $0x8944; BYTE $0xd8 // movl\t%r11d, %eax\n\nLBB12_30:\n\tLONG $0x107cc1c4; WORD $0x240c // vmovups\t(%r12), %ymm1\n\tLONG $0x5974c1c4; WORD $0x004d // vmulps\t(%r13), %ymm1, %ymm1\n\tLONG $0xc158fcc5               // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0x20c48349               // addq\t$32, %r12\n\tLONG $0x20c58349               // addq\t$32, %r13\n\tWORD $0xc8ff                   // decl\t%eax\n\tJNE  LBB12_30\n\nLBB12_31:\n\tLONG $0x197de3c4; WORD $0x01c1             // vextractf128\t$1, %ymm0, %xmm1\n\tLONG $0xc058f0c5                           // vaddps\t%xmm0, %xmm1, %xmm0\n\tLONG $0xc8c6f9c5; BYTE $0x01               // vshufpd\t$1, %xmm0, %xmm0, %xmm1         # xmm1 = xmm0[1,0]\n\tLONG $0xc158f8c5                           // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0xc816fac5                           // vmovshdup\t%xmm0, %xmm1            # xmm1 = xmm0[1,1,3,3]\n\tLONG $0xc158fac5                           // vaddss\t%xmm1, %xmm0, %xmm0\n\tLONG $0x107ac1c4; WORD $0x240c             // vmovss\t(%r12), %xmm1                   # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x5972c1c4; WORD $0x004d             // vmulss\t(%r13), %xmm1, %xmm1\n\tLONG $0xc058f2c5                           // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xfe83; BYTE $0x01                   // cmpl\t$1, %esi\n\tJE   LBB12_38\n\tLONG $0x107ac1c4; WORD $0x244c; BYTE $0x04 // vmovss\t4(%r12), %xmm1                  # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x5972c1c4; WORD $0x044d             // vmulss\t4(%r13), %xmm1, %xmm1\n\tLONG $0xc058f2c5                           // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xfe83; BYTE $0x02                   // cmpl\t$2, %esi\n\tJE   LBB12_38\n\tLONG $0x107ac1c4; WORD $0x244c; BYTE $0x08 // vmovss\t8(%r12), %xmm1                  # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x5972c1c4; WORD $0x084d             // vmulss\t8(%r13), %xmm1, %xmm1\n\tLONG $0xc058f2c5                           // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xfe83; BYTE $0x03                   // cmpl\t$3, %esi\n\tJE   LBB12_38\n\tLONG $0x107ac1c4; WORD $0x244c; BYTE $0x0c // vmovss\t12(%r12), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x5972c1c4; WORD $0x0c4d             // vmulss\t12(%r13), %xmm1, %xmm1\n\tLONG $0xc058f2c5                           // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xfe83; BYTE $0x04                   // cmpl\t$4, %esi\n\tJE   LBB12_38\n\tLONG $0x107ac1c4; WORD $0x244c; BYTE $0x10 // vmovss\t16(%r12), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x5972c1c4; WORD $0x104d             // vmulss\t16(%r13), %xmm1, %xmm1\n\tLONG $0xc058f2c5                           // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xfe83; BYTE $0x05                   // cmpl\t$5, %esi\n\tJE   LBB12_38\n\tLONG $0x107ac1c4; WORD $0x244c; BYTE $0x14 // vmovss\t20(%r12), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x5972c1c4; WORD $0x144d             // vmulss\t20(%r13), %xmm1, %xmm1\n\tLONG $0xc058f2c5                           // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xfe83; BYTE $0x06                   // cmpl\t$6, %esi\n\tJE   LBB12_38\n\tLONG $0x107ac1c4; WORD $0x244c; BYTE $0x18 // vmovss\t24(%r12), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x5972c1c4; WORD $0x184d             // vmulss\t24(%r13), %xmm1, %xmm1\n\tLONG $0xc058f2c5                           // vaddss\t%xmm0, %xmm1, %xmm0\n\tJMP  LBB12_38\n\nLBB12_88:\n\tLONG $0x243c8348; BYTE $0x00           // cmpq\t$0, (%rsp)                      # 8-byte Folded Reload\n\tWORD $0x9e0f; BYTE $0xc1               // setle\t%cl\n\tWORD $0x0134                           // xorb\t$1, %al\n\tWORD $0xc808                           // orb\t%cl, %al\n\tJNE  LBB12_117\n\tLONG $0x30458b48                       // movq\t48(%rbp), %rax\n\tQUAD $0x00000000853c8d48               // leaq\t(,%rax,4), %rdi\n\tLONG $0xff408d49                       // leaq\t-1(%r8), %rax\n\tWORD $0x8948; BYTE $0xc1               // movq\t%rax, %rcx\n\tLONG $0x4daf0f48; BYTE $0x10           // imulq\t16(%rbp), %rcx\n\tLONG $0xc5af0f49                       // imulq\t%r13, %rax\n\tWORD $0x0148; BYTE $0xd8               // addq\t%rbx, %rax\n\tLONG $0x18758b48                       // movq\t24(%rbp), %rsi\n\tLONG $0x86048d48                       // leaq\t(%rsi,%rax,4), %rax\n\tLONG $0x24448948; BYTE $0x38           // movq\t%rax, 56(%rsp)                  # 8-byte Spill\n\tLONG $0x28458b48                       // movq\t40(%rbp), %rax\n\tLONG $0x98148d48                       // leaq\t(%rax,%rbx,4), %rdx\n\tLONG $0x24548948; BYTE $0x30           // movq\t%rdx, 48(%rsp)                  # 8-byte Spill\n\tLONG $0x24548b48; BYTE $0x08           // movq\t8(%rsp), %rdx                   # 8-byte Reload\n\tLONG $0x8a0c8d48                       // leaq\t(%rdx,%rcx,4), %rcx\n\tLONG $0x04c18348                       // addq\t$4, %rcx\n\tLONG $0x244c8948; BYTE $0x28           // movq\t%rcx, 40(%rsp)                  # 8-byte Spill\n\tQUAD $0xffffffffffe0bf49; WORD $0x7fff // movabsq\t$9223372036854775776, %r15      # imm = 0x7FFFFFFFFFFFFFE0\n\tWORD $0x2149; BYTE $0xdf               // andq\t%rbx, %r15\n\tLONG $0xff4b8d48                       // leaq\t-1(%rbx), %rcx\n\tLONG $0x244c8948; BYTE $0x10           // movq\t%rcx, 16(%rsp)                  # 8-byte Spill\n\tLONG $0x604e8d48                       // leaq\t96(%rsi), %rcx\n\tLONG $0x244c8948; BYTE $0x20           // movq\t%rcx, 32(%rsp)                  # 8-byte Spill\n\tQUAD $0x00000000ad148d4a               // leaq\t(,%r13,4), %rdx\n\tLONG $0x60588d4c                       // leaq\t96(%rax), %r11\n\tLONG $0x044e8d48                       // leaq\t4(%rsi), %rcx\n\tLONG $0x244c8948; BYTE $0x18           // movq\t%rcx, 24(%rsp)                  # 8-byte Spill\n\tLONG $0x04708d48                       // leaq\t4(%rax), %rsi\n\tWORD $0x3145; BYTE $0xf6               // xorl\t%r14d, %r14d\n\tLONG $0x247c8948; BYTE $0x40           // movq\t%rdi, 64(%rsp)                  # 8-byte Spill\n\tJMP  LBB12_90\n\nLBB12_101:\n\tLONG $0x24748b4c; BYTE $0x48 // movq\t72(%rsp), %r14                  # 8-byte Reload\n\tWORD $0xff49; BYTE $0xc6     // incq\t%r14\n\tLONG $0x247c8b48; BYTE $0x40 // movq\t64(%rsp), %rdi                  # 8-byte Reload\n\tWORD $0x0149; BYTE $0xfb     // addq\t%rdi, %r11\n\tWORD $0x0148; BYTE $0xfe     // addq\t%rdi, %rsi\n\tLONG $0x24343b4c             // cmpq\t(%rsp), %r14                    # 8-byte Folded Reload\n\tLONG $0x206d8b4c             // movq\t32(%rbp), %r13\n\tJE   LBB12_117\n\nLBB12_90:\n\tQUAD $0x000000000000b948; WORD $0x2000 // movabsq\t$2305843009213693952, %rcx      # imm = 0x2000000000000000\n\tWORD $0x8549; BYTE $0xcd               // testq\t%rcx, %r13\n\tWORD $0x950f; BYTE $0xc0               // setne\t%al\n\tLONG $0x104d8548                       // testq\t%rcx, 16(%rbp)\n\tWORD $0x950f; BYTE $0xc1               // setne\t%cl\n\tLONG $0xfeaf0f49                       // imulq\t%r14, %rdi\n\tLONG $0x286d8b4c                       // movq\t40(%rbp), %r13\n\tLONG $0x2f048d4e                       // leaq\t(%rdi,%r13), %r8\n\tLONG $0x247c0348; BYTE $0x30           // addq\t48(%rsp), %rdi                  # 8-byte Folded Reload\n\tLONG $0x244c8b4c; BYTE $0x08           // movq\t8(%rsp), %r9                    # 8-byte Reload\n\tLONG $0xb1148d4f                       // leaq\t(%r9,%r14,4), %r10\n\tLONG $0x244c8b4c; BYTE $0x28           // movq\t40(%rsp), %r9                   # 8-byte Reload\n\tLONG $0xb10c8d4f                       // leaq\t(%r9,%r14,4), %r9\n\tLONG $0x2474894c; BYTE $0x48           // movq\t%r14, 72(%rsp)                  # 8-byte Spill\n\tLONG $0x75af0f4c; BYTE $0x30           // imulq\t48(%rbp), %r14\n\tWORD $0x394d; BYTE $0xc8               // cmpq\t%r9, %r8\n\tLONG $0xc1920f41                       // setb\t%r9b\n\tWORD $0x3949; BYTE $0xfa               // cmpq\t%rdi, %r10\n\tLONG $0xc4920f41                       // setb\t%r12b\n\tWORD $0x2045; BYTE $0xcc               // andb\t%r9b, %r12b\n\tQUAD $0x00000000b50c8d4e               // leaq\t(,%r14,4), %r9\n\tWORD $0x014d; BYTE $0xe9               // addq\t%r13, %r9\n\tLONG $0x244c894c; BYTE $0x50           // movq\t%r9, 80(%rsp)                   # 8-byte Spill\n\tWORD $0x0841; BYTE $0xcc               // orb\t%cl, %r12b\n\tLONG $0x24443b4c; BYTE $0x38           // cmpq\t56(%rsp), %r8                   # 8-byte Folded Reload\n\tWORD $0x920f; BYTE $0xc1               // setb\t%cl\n\tLONG $0x187d3b48                       // cmpq\t24(%rbp), %rdi\n\tLONG $0xc1970f41                       // seta\t%r9b\n\tWORD $0x2041; BYTE $0xc9               // andb\t%cl, %r9b\n\tWORD $0x0841; BYTE $0xc1               // orb\t%al, %r9b\n\tWORD $0x0845; BYTE $0xe1               // orb\t%r12b, %r9b\n\tLONG $0x24448b48; BYTE $0x18           // movq\t24(%rsp), %rax                  # 8-byte Reload\n\tLONG $0x244c8b48; BYTE $0x20           // movq\t32(%rsp), %rcx                  # 8-byte Reload\n\tWORD $0x3145; BYTE $0xf6               // xorl\t%r14d, %r14d\n\tJMP  LBB12_91\n\nLBB12_100:\n\tWORD $0xff49; BYTE $0xc6     // incq\t%r14\n\tWORD $0x0148; BYTE $0xd1     // addq\t%rdx, %rcx\n\tWORD $0x0148; BYTE $0xd0     // addq\t%rdx, %rax\n\tLONG $0x24743b4c; BYTE $0x58 // cmpq\t88(%rsp), %r14                  # 8-byte Folded Reload\n\tJE   LBB12_101\n\nLBB12_91:\n\tLONG $0x20fb8348             // cmpq\t$32, %rbx\n\tLONG $0xc0920f41             // setb\t%r8b\n\tWORD $0x894c; BYTE $0xf7     // movq\t%r14, %rdi\n\tLONG $0x7daf0f48; BYTE $0x10 // imulq\t16(%rbp), %rdi\n\tWORD $0x0845; BYTE $0xc8     // orb\t%r9b, %r8b\n\tLONG $0x01c0f641             // testb\t$1, %r8b\n\tJE   LBB12_93\n\tWORD $0x3145; BYTE $0xc0     // xorl\t%r8d, %r8d\n\tJMP  LBB12_96\n\nLBB12_93:\n\tLONG $0x187dc2c4; WORD $0xba04 // vbroadcastss\t(%r10,%rdi,4), %ymm0\n\tWORD $0x3145; BYTE $0xc0       // xorl\t%r8d, %r8d\n\nLBB12_94:\n\tLONG $0x597ca1c4; WORD $0x814c; BYTE $0xa0 // vmulps\t-96(%rcx,%r8,4), %ymm0, %ymm1\n\tLONG $0x587481c4; WORD $0x834c; BYTE $0xa0 // vaddps\t-96(%r11,%r8,4), %ymm1, %ymm1\n\tLONG $0x597ca1c4; WORD $0x8154; BYTE $0xc0 // vmulps\t-64(%rcx,%r8,4), %ymm0, %ymm2\n\tLONG $0x586c81c4; WORD $0x8354; BYTE $0xc0 // vaddps\t-64(%r11,%r8,4), %ymm2, %ymm2\n\tLONG $0x597ca1c4; WORD $0x815c; BYTE $0xe0 // vmulps\t-32(%rcx,%r8,4), %ymm0, %ymm3\n\tLONG $0x586481c4; WORD $0x835c; BYTE $0xe0 // vaddps\t-32(%r11,%r8,4), %ymm3, %ymm3\n\tLONG $0x597ca1c4; WORD $0x8124             // vmulps\t(%rcx,%r8,4), %ymm0, %ymm4\n\tLONG $0x585c81c4; WORD $0x8324             // vaddps\t(%r11,%r8,4), %ymm4, %ymm4\n\tLONG $0x117c81c4; WORD $0x834c; BYTE $0xa0 // vmovups\t%ymm1, -96(%r11,%r8,4)\n\tLONG $0x117c81c4; WORD $0x8354; BYTE $0xc0 // vmovups\t%ymm2, -64(%r11,%r8,4)\n\tLONG $0x117c81c4; WORD $0x835c; BYTE $0xe0 // vmovups\t%ymm3, -32(%r11,%r8,4)\n\tLONG $0x117c81c4; WORD $0x8324             // vmovups\t%ymm4, (%r11,%r8,4)\n\tLONG $0x20c08349                           // addq\t$32, %r8\n\tWORD $0x394d; BYTE $0xc7                   // cmpq\t%r8, %r15\n\tJNE  LBB12_94\n\tWORD $0x894d; BYTE $0xf8                   // movq\t%r15, %r8\n\tWORD $0x3949; BYTE $0xdf                   // cmpq\t%rbx, %r15\n\tJE   LBB12_100\n\nLBB12_96:\n\tWORD $0x894d; BYTE $0xc4       // movq\t%r8, %r12\n\tWORD $0xc3f6; BYTE $0x01       // testb\t$1, %bl\n\tJE   LBB12_98\n\tWORD $0x894d; BYTE $0xf4       // movq\t%r14, %r12\n\tLONG $0x65af0f4c; BYTE $0x20   // imulq\t32(%rbp), %r12\n\tLONG $0x186d8b4c               // movq\t24(%rbp), %r13\n\tQUAD $0x00000000a5248d4e       // leaq\t(,%r12,4), %r12\n\tWORD $0x014d; BYTE $0xec       // addq\t%r13, %r12\n\tLONG $0x107ac1c4; WORD $0xba04 // vmovss\t(%r10,%rdi,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x597a81c4; WORD $0x8404 // vmulss\t(%r12,%r8,4), %xmm0, %xmm0\n\tLONG $0x24648b4c; BYTE $0x50   // movq\t80(%rsp), %r12                  # 8-byte Reload\n\tLONG $0x587a81c4; WORD $0x8404 // vaddss\t(%r12,%r8,4), %xmm0, %xmm0\n\tLONG $0x117a81c4; WORD $0x8404 // vmovss\t%xmm0, (%r12,%r8,4)\n\tWORD $0x894d; BYTE $0xc4       // movq\t%r8, %r12\n\tLONG $0x01cc8349               // orq\t$1, %r12\n\nLBB12_98:\n\tLONG $0x24443b4c; BYTE $0x10 // cmpq\t16(%rsp), %r8                   # 8-byte Folded Reload\n\tJE   LBB12_100\n\nLBB12_99:\n\tLONG $0x107ac1c4; WORD $0xba04             // vmovss\t(%r10,%rdi,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x597aa1c4; WORD $0xa044; BYTE $0xfc // vmulss\t-4(%rax,%r12,4), %xmm0, %xmm0\n\tLONG $0x587aa1c4; WORD $0xa644; BYTE $0xfc // vaddss\t-4(%rsi,%r12,4), %xmm0, %xmm0\n\tLONG $0x117aa1c4; WORD $0xa644; BYTE $0xfc // vmovss\t%xmm0, -4(%rsi,%r12,4)\n\tLONG $0x107ac1c4; WORD $0xba04             // vmovss\t(%r10,%rdi,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x597aa1c4; WORD $0xa004             // vmulss\t(%rax,%r12,4), %xmm0, %xmm0\n\tLONG $0x587aa1c4; WORD $0xa604             // vaddss\t(%rsi,%r12,4), %xmm0, %xmm0\n\tLONG $0x117aa1c4; WORD $0xa604             // vmovss\t%xmm0, (%rsi,%r12,4)\n\tLONG $0x02c48349                           // addq\t$2, %r12\n\tWORD $0x394c; BYTE $0xe3                   // cmpq\t%r12, %rbx\n\tJNE  LBB12_99\n\tJMP  LBB12_100\n\nLBB12_47:\n\tWORD $0x854d; BYTE $0xc0       // testq\t%r8, %r8\n\tLONG $0x28758b4c               // movq\t40(%rbp), %r14\n\tJLE  LBB12_71\n\tWORD $0x8944; BYTE $0xc6       // movl\t%r8d, %esi\n\tWORD $0xc085                   // testl\t%eax, %eax\n\tJLE  LBB12_49\n\tLONG $0x24448348; WORD $0x3860 // addq\t$56, 96(%rsp)                   # 8-byte Folded Spill\n\tLONG $0x02e5c149               // shlq\t$2, %r13\n\tLONG $0x2464c148; WORD $0x0268 // shlq\t$2, 104(%rsp)                   # 8-byte Folded Spill\n\tWORD $0xd231                   // xorl\t%edx, %edx\n\tLONG $0x244c8b4c; BYTE $0x08   // movq\t8(%rsp), %r9                    # 8-byte Reload\n\tJMP  LBB12_61\n\nLBB12_70:\n\tWORD $0xff48; BYTE $0xc2     // incq\t%rdx\n\tLONG $0x2444034c; BYTE $0x68 // addq\t104(%rsp), %r8                  # 8-byte Folded Reload\n\tLONG $0x2444894c; BYTE $0x70 // movq\t%r8, 112(%rsp)                  # 8-byte Spill\n\tLONG $0x24143b48             // cmpq\t(%rsp), %rdx                    # 8-byte Folded Reload\n\tJE   LBB12_117\n\nLBB12_61:\n\tWORD $0x8948; BYTE $0xd0     // movq\t%rdx, %rax\n\tLONG $0x45af0f48; BYTE $0x10 // imulq\t16(%rbp), %rax\n\tLONG $0x244c8b48; BYTE $0x60 // movq\t96(%rsp), %rcx                  # 8-byte Reload\n\tWORD $0xff31                 // xorl\t%edi, %edi\n\tLONG $0x24448b4c; BYTE $0x70 // movq\t112(%rsp), %r8                  # 8-byte Reload\n\tJMP  LBB12_62\n\nLBB12_69:\n\tLONG $0x117ac1c4; WORD $0xb804 // vmovss\t%xmm0, (%r8,%rdi,4)\n\tWORD $0xff48; BYTE $0xc7       // incq\t%rdi\n\tWORD $0x014c; BYTE $0xe9       // addq\t%r13, %rcx\n\tWORD $0x3948; BYTE $0xfb       // cmpq\t%rdi, %rbx\n\tJE   LBB12_70\n\nLBB12_62:\n\tLONG $0x107cc1c4; WORD $0x8104             // vmovups\t(%r9,%rax,4), %ymm0\n\tLONG $0x4159fcc5; BYTE $0xc8               // vmulps\t-56(%rcx), %ymm0, %ymm0\n\tLONG $0x197de3c4; WORD $0x01c1             // vextractf128\t$1, %ymm0, %xmm1\n\tLONG $0xc058f0c5                           // vaddps\t%xmm0, %xmm1, %xmm0\n\tLONG $0xc8c6f9c5; BYTE $0x01               // vshufpd\t$1, %xmm0, %xmm0, %xmm1         # xmm1 = xmm0[1,0]\n\tLONG $0xc158f8c5                           // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0xc816fac5                           // vmovshdup\t%xmm0, %xmm1            # xmm1 = xmm0[1,1,3,3]\n\tLONG $0xc158fac5                           // vaddss\t%xmm1, %xmm0, %xmm0\n\tLONG $0x107ac1c4; WORD $0x814c; BYTE $0x20 // vmovss\t32(%r9,%rax,4), %xmm1           # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x4959f2c5; BYTE $0xe8               // vmulss\t-24(%rcx), %xmm1, %xmm1\n\tLONG $0xc058f2c5                           // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xfe83; BYTE $0x01                   // cmpl\t$1, %esi\n\tJE   LBB12_69\n\tLONG $0x107ac1c4; WORD $0x814c; BYTE $0x24 // vmovss\t36(%r9,%rax,4), %xmm1           # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x4959f2c5; BYTE $0xec               // vmulss\t-20(%rcx), %xmm1, %xmm1\n\tLONG $0xc058f2c5                           // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xfe83; BYTE $0x02                   // cmpl\t$2, %esi\n\tJE   LBB12_69\n\tLONG $0x107ac1c4; WORD $0x814c; BYTE $0x28 // vmovss\t40(%r9,%rax,4), %xmm1           # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x4959f2c5; BYTE $0xf0               // vmulss\t-16(%rcx), %xmm1, %xmm1\n\tLONG $0xc058f2c5                           // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xfe83; BYTE $0x03                   // cmpl\t$3, %esi\n\tJE   LBB12_69\n\tLONG $0x107ac1c4; WORD $0x814c; BYTE $0x2c // vmovss\t44(%r9,%rax,4), %xmm1           # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x4959f2c5; BYTE $0xf4               // vmulss\t-12(%rcx), %xmm1, %xmm1\n\tLONG $0xc058f2c5                           // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xfe83; BYTE $0x04                   // cmpl\t$4, %esi\n\tJE   LBB12_69\n\tLONG $0x107ac1c4; WORD $0x814c; BYTE $0x30 // vmovss\t48(%r9,%rax,4), %xmm1           # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x4959f2c5; BYTE $0xf8               // vmulss\t-8(%rcx), %xmm1, %xmm1\n\tLONG $0xc058f2c5                           // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xfe83; BYTE $0x05                   // cmpl\t$5, %esi\n\tJE   LBB12_69\n\tLONG $0x107ac1c4; WORD $0x814c; BYTE $0x34 // vmovss\t52(%r9,%rax,4), %xmm1           # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x4959f2c5; BYTE $0xfc               // vmulss\t-4(%rcx), %xmm1, %xmm1\n\tLONG $0xc058f2c5                           // vaddss\t%xmm0, %xmm1, %xmm0\n\tWORD $0xfe83; BYTE $0x06                   // cmpl\t$6, %esi\n\tJE   LBB12_69\n\tLONG $0x107ac1c4; WORD $0x814c; BYTE $0x38 // vmovss\t56(%r9,%rax,4), %xmm1           # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x0959f2c5                           // vmulss\t(%rcx), %xmm1, %xmm1\n\tLONG $0xc058f2c5                           // vaddss\t%xmm0, %xmm1, %xmm0\n\tJMP  LBB12_69\n\nLBB12_20:\n\tWORD $0x508d; BYTE $0xff // leal\t-1(%rax), %edx\n\tWORD $0x708d; BYTE $0xfe // leal\t-2(%rax), %esi\n\tWORD $0xd789             // movl\t%edx, %edi\n\tWORD $0xe783; BYTE $0xfc // andl\t$-4, %edi\n\tWORD $0xc8fe             // decb\t%al\n\tLONG $0xc0b60f44         // movzbl\t%al, %r8d\n\tLONG $0x03e08341         // andl\t$3, %r8d\n\tLONG $0x05e0c141         // shll\t$5, %r8d\n\tWORD $0x3145; BYTE $0xd2 // xorl\t%r10d, %r10d\n\tJMP  LBB12_21\n\nLBB12_46:\n\tWORD $0xff49; BYTE $0xc2 // incq\t%r10\n\tLONG $0x24143b4c         // cmpq\t(%rsp), %r10                    # 8-byte Folded Reload\n\tJE   LBB12_117\n\nLBB12_21:\n\tWORD $0x894c; BYTE $0xd0     // movq\t%r10, %rax\n\tLONG $0x45af0f48; BYTE $0x10 // imulq\t16(%rbp), %rax\n\tLONG $0x244c8b48; BYTE $0x08 // movq\t8(%rsp), %rcx                   # 8-byte Reload\n\tLONG $0x81048d48             // leaq\t(%rcx,%rax,4), %rax\n\tWORD $0x894c; BYTE $0xd1     // movq\t%r10, %rcx\n\tLONG $0x4daf0f48; BYTE $0x30 // imulq\t48(%rbp), %rcx\n\tLONG $0x284d8b4c             // movq\t40(%rbp), %r9\n\tLONG $0x890c8d49             // leaq\t(%r9,%rcx,4), %rcx\n\tWORD $0x3145; BYTE $0xc9     // xorl\t%r9d, %r9d\n\tJMP  LBB12_22\n\nLBB12_45:\n\tLONG $0x197de3c4; WORD $0x01c1 // vextractf128\t$1, %ymm0, %xmm1\n\tLONG $0xc058f0c5               // vaddps\t%xmm0, %xmm1, %xmm0\n\tLONG $0xc8c6f9c5; BYTE $0x01   // vshufpd\t$1, %xmm0, %xmm0, %xmm1         # xmm1 = xmm0[1,0]\n\tLONG $0xc158f8c5               // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0xc816fac5               // vmovshdup\t%xmm0, %xmm1            # xmm1 = xmm0[1,1,3,3]\n\tLONG $0xc158fac5               // vaddss\t%xmm1, %xmm0, %xmm0\n\tLONG $0x117aa1c4; WORD $0x8904 // vmovss\t%xmm0, (%rcx,%r9,4)\n\tWORD $0xff49; BYTE $0xc1       // incq\t%r9\n\tWORD $0x3949; BYTE $0xd9       // cmpq\t%rbx, %r9\n\tJE   LBB12_46\n\nLBB12_22:\n\tWORD $0x894d; BYTE $0xce       // movq\t%r9, %r14\n\tLONG $0xf5af0f4d               // imulq\t%r13, %r14\n\tLONG $0x187d8b4c               // movq\t24(%rbp), %r15\n\tLONG $0xb71c8d4f               // leaq\t(%r15,%r14,4), %r11\n\tLONG $0x0010fcc5               // vmovups\t(%rax), %ymm0\n\tLONG $0x597c81c4; WORD $0xb704 // vmulps\t(%r15,%r14,4), %ymm0, %ymm0\n\tWORD $0xfe83; BYTE $0x03       // cmpl\t$3, %esi\n\tJAE  LBB12_40\n\tWORD $0x8949; BYTE $0xc6       // movq\t%rax, %r14\n\tJMP  LBB12_42\n\nLBB12_40:\n\tWORD $0x8941; BYTE $0xff // movl\t%edi, %r15d\n\tWORD $0x8949; BYTE $0xc6 // movq\t%rax, %r14\n\nLBB12_41:\n\tLONG $0x107cc1c4; WORD $0x204e       // vmovups\t32(%r14), %ymm1\n\tLONG $0x107cc1c4; WORD $0x4056       // vmovups\t64(%r14), %ymm2\n\tLONG $0x107cc1c4; WORD $0x605e       // vmovups\t96(%r14), %ymm3\n\tQUAD $0x000080a6107cc1c4; BYTE $0x00 // vmovups\t128(%r14), %ymm4\n\tLONG $0x5974c1c4; WORD $0x204b       // vmulps\t32(%r11), %ymm1, %ymm1\n\tLONG $0xc158fcc5                     // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0x596cc1c4; WORD $0x404b       // vmulps\t64(%r11), %ymm2, %ymm1\n\tLONG $0x5964c1c4; WORD $0x6053       // vmulps\t96(%r11), %ymm3, %ymm2\n\tLONG $0xc158fcc5                     // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0xc258fcc5                     // vaddps\t%ymm2, %ymm0, %ymm0\n\tQUAD $0x0000808b595cc1c4; BYTE $0x00 // vmulps\t128(%r11), %ymm4, %ymm1\n\tLONG $0x80ee8349                     // subq\t$-128, %r14\n\tLONG $0x80eb8349                     // subq\t$-128, %r11\n\tLONG $0xc158fcc5                     // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0xfcc78341                     // addl\t$-4, %r15d\n\tJNE  LBB12_41\n\nLBB12_42:\n\tWORD $0xc2f6; BYTE $0x03 // testb\t$3, %dl\n\tJE   LBB12_45\n\tWORD $0x3145; BYTE $0xff // xorl\t%r15d, %r15d\n\nLBB12_44:\n\tLONG $0x107c81c4; WORD $0x3e4c; BYTE $0x20 // vmovups\t32(%r14,%r15), %ymm1\n\tLONG $0x597481c4; WORD $0x3b4c; BYTE $0x20 // vmulps\t32(%r11,%r15), %ymm1, %ymm1\n\tLONG $0xc158fcc5                           // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0x20c78349                           // addq\t$32, %r15\n\tWORD $0x3945; BYTE $0xf8                   // cmpl\t%r15d, %r8d\n\tJNE  LBB12_44\n\tJMP  LBB12_45\n\nLBB12_71:\n\tWORD $0xc085                           // testl\t%eax, %eax\n\tJLE  LBB12_75\n\tQUAD $0xffffffffffe0b848; WORD $0x7fff // movabsq\t$9223372036854775776, %rax      # imm = 0x7FFFFFFFFFFFFFE0\n\tLONG $0x1ec88348                       // orq\t$30, %rax\n\tWORD $0x2148; BYTE $0xd8               // andq\t%rbx, %rax\n\tQUAD $0x00000000ed0c8d4a               // leaq\t(,%r13,8), %rcx\n\tLONG $0x04568d49                       // leaq\t4(%r14), %rdx\n\tLONG $0x30758b48                       // movq\t48(%rbp), %rsi\n\tQUAD $0x00000000b5348d48               // leaq\t(,%rsi,4), %rsi\n\tWORD $0xff31                           // xorl\t%edi, %edi\n\tJMP  LBB12_73\n\nLBB12_81:\n\tWORD $0xff48; BYTE $0xc7 // incq\t%rdi\n\tWORD $0x0148; BYTE $0xf2 // addq\t%rsi, %rdx\n\tLONG $0x243c3b48         // cmpq\t(%rsp), %rdi                    # 8-byte Folded Reload\n\tJE   LBB12_117\n\nLBB12_73:\n\tWORD $0x8949; BYTE $0xf9     // movq\t%rdi, %r9\n\tLONG $0x4daf0f4c; BYTE $0x10 // imulq\t16(%rbp), %r9\n\tLONG $0x01fb8348             // cmpq\t$1, %rbx\n\tJNE  LBB12_77\n\tWORD $0x3145; BYTE $0xc0     // xorl\t%r8d, %r8d\n\tLONG $0x24748b4c; BYTE $0x08 // movq\t8(%rsp), %r14                   # 8-byte Reload\n\tJMP  LBB12_79\n\nLBB12_77:\n\tLONG $0x18558b4c             // movq\t24(%rbp), %r10\n\tWORD $0x3145; BYTE $0xc0     // xorl\t%r8d, %r8d\n\tLONG $0x24748b4c; BYTE $0x08 // movq\t8(%rsp), %r14                   # 8-byte Reload\n\nLBB12_78:\n\tLONG $0x107c81c4; WORD $0x8e04             // vmovups\t(%r14,%r9,4), %ymm0\n\tLONG $0x597cc1c4; BYTE $0x02               // vmulps\t(%r10), %ymm0, %ymm0\n\tLONG $0x197de3c4; WORD $0x01c1             // vextractf128\t$1, %ymm0, %xmm1\n\tLONG $0xc058f0c5                           // vaddps\t%xmm0, %xmm1, %xmm0\n\tLONG $0xc8c6f9c5; BYTE $0x01               // vshufpd\t$1, %xmm0, %xmm0, %xmm1         # xmm1 = xmm0[1,0]\n\tLONG $0xc158f8c5                           // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0xc816fac5                           // vmovshdup\t%xmm0, %xmm1            # xmm1 = xmm0[1,1,3,3]\n\tLONG $0xc158fac5                           // vaddss\t%xmm1, %xmm0, %xmm0\n\tLONG $0x117aa1c4; WORD $0x8244; BYTE $0xfc // vmovss\t%xmm0, -4(%rdx,%r8,4)\n\tLONG $0x107c81c4; WORD $0x8e04             // vmovups\t(%r14,%r9,4), %ymm0\n\tLONG $0x597c81c4; WORD $0xaa04             // vmulps\t(%r10,%r13,4), %ymm0, %ymm0\n\tLONG $0x197de3c4; WORD $0x01c1             // vextractf128\t$1, %ymm0, %xmm1\n\tLONG $0xc058f0c5                           // vaddps\t%xmm0, %xmm1, %xmm0\n\tLONG $0xc8c6f9c5; BYTE $0x01               // vshufpd\t$1, %xmm0, %xmm0, %xmm1         # xmm1 = xmm0[1,0]\n\tLONG $0xc158f8c5                           // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0xc816fac5                           // vmovshdup\t%xmm0, %xmm1            # xmm1 = xmm0[1,1,3,3]\n\tLONG $0xc158fac5                           // vaddss\t%xmm1, %xmm0, %xmm0\n\tLONG $0x117aa1c4; WORD $0x8204             // vmovss\t%xmm0, (%rdx,%r8,4)\n\tLONG $0x02c08349                           // addq\t$2, %r8\n\tWORD $0x0149; BYTE $0xca                   // addq\t%rcx, %r10\n\tWORD $0x394c; BYTE $0xc0                   // cmpq\t%r8, %rax\n\tJNE  LBB12_78\n\nLBB12_79:\n\tWORD $0xc3f6; BYTE $0x01       // testb\t$1, %bl\n\tJE   LBB12_81\n\tWORD $0x8949; BYTE $0xfa       // movq\t%rdi, %r10\n\tLONG $0x55af0f4c; BYTE $0x30   // imulq\t48(%rbp), %r10\n\tWORD $0x894d; BYTE $0xc3       // movq\t%r8, %r11\n\tLONG $0xddaf0f4d               // imulq\t%r13, %r11\n\tLONG $0x107c81c4; WORD $0x8e04 // vmovups\t(%r14,%r9,4), %ymm0\n\tLONG $0x184d8b4c               // movq\t24(%rbp), %r9\n\tLONG $0x597c81c4; WORD $0x9904 // vmulps\t(%r9,%r11,4), %ymm0, %ymm0\n\tLONG $0x284d8b4c               // movq\t40(%rbp), %r9\n\tLONG $0x910c8d4f               // leaq\t(%r9,%r10,4), %r9\n\tLONG $0x197de3c4; WORD $0x01c1 // vextractf128\t$1, %ymm0, %xmm1\n\tLONG $0xc058f0c5               // vaddps\t%xmm0, %xmm1, %xmm0\n\tLONG $0xc8c6f9c5; BYTE $0x01   // vshufpd\t$1, %xmm0, %xmm0, %xmm1         # xmm1 = xmm0[1,0]\n\tLONG $0xc158f8c5               // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0xc816fac5               // vmovshdup\t%xmm0, %xmm1            # xmm1 = xmm0[1,1,3,3]\n\tLONG $0xc158fac5               // vaddss\t%xmm1, %xmm0, %xmm0\n\tLONG $0x117a81c4; WORD $0x8104 // vmovss\t%xmm0, (%r9,%r8,4)\n\tJMP  LBB12_81\n\nLBB12_49:\n\tLONG $0x24448348; WORD $0x1860 // addq\t$24, 96(%rsp)                   # 8-byte Folded Spill\n\tLONG $0x02e5c149               // shlq\t$2, %r13\n\tLONG $0x2464c148; WORD $0x0268 // shlq\t$2, 104(%rsp)                   # 8-byte Folded Spill\n\tWORD $0xc031                   // xorl\t%eax, %eax\n\tLONG $0xc057f8c5               // vxorps\t%xmm0, %xmm0, %xmm0\n\tLONG $0x244c8b4c; BYTE $0x08   // movq\t8(%rsp), %r9                    # 8-byte Reload\n\tJMP  LBB12_50\n\nLBB12_59:\n\tWORD $0xff48; BYTE $0xc0     // incq\t%rax\n\tLONG $0x2444034c; BYTE $0x68 // addq\t104(%rsp), %r8                  # 8-byte Folded Reload\n\tLONG $0x2444894c; BYTE $0x70 // movq\t%r8, 112(%rsp)                  # 8-byte Spill\n\tLONG $0x24043b48             // cmpq\t(%rsp), %rax                    # 8-byte Folded Reload\n\tJE   LBB12_117\n\nLBB12_50:\n\tWORD $0x8948; BYTE $0xc1     // movq\t%rax, %rcx\n\tLONG $0x4daf0f48; BYTE $0x10 // imulq\t16(%rbp), %rcx\n\tLONG $0x24548b48; BYTE $0x60 // movq\t96(%rsp), %rdx                  # 8-byte Reload\n\tWORD $0xff31                 // xorl\t%edi, %edi\n\tLONG $0x24448b4c; BYTE $0x70 // movq\t112(%rsp), %r8                  # 8-byte Reload\n\tJMP  LBB12_51\n\nLBB12_58:\n\tLONG $0x117ac1c4; WORD $0xb80c // vmovss\t%xmm1, (%r8,%rdi,4)\n\tWORD $0xff48; BYTE $0xc7       // incq\t%rdi\n\tWORD $0x014c; BYTE $0xea       // addq\t%r13, %rdx\n\tWORD $0x3948; BYTE $0xfb       // cmpq\t%rdi, %rbx\n\tJE   LBB12_59\n\nLBB12_51:\n\tLONG $0x107ac1c4; WORD $0x890c             // vmovss\t(%r9,%rcx,4), %xmm1             # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x4a59f2c5; BYTE $0xe8               // vmulss\t-24(%rdx), %xmm1, %xmm1\n\tLONG $0xc858f2c5                           // vaddss\t%xmm0, %xmm1, %xmm1\n\tWORD $0xfe83; BYTE $0x01                   // cmpl\t$1, %esi\n\tJE   LBB12_58\n\tLONG $0x107ac1c4; WORD $0x8954; BYTE $0x04 // vmovss\t4(%r9,%rcx,4), %xmm2            # xmm2 = mem[0],zero,zero,zero\n\tLONG $0x5259eac5; BYTE $0xec               // vmulss\t-20(%rdx), %xmm2, %xmm2\n\tLONG $0xc958eac5                           // vaddss\t%xmm1, %xmm2, %xmm1\n\tWORD $0xfe83; BYTE $0x02                   // cmpl\t$2, %esi\n\tJE   LBB12_58\n\tLONG $0x107ac1c4; WORD $0x8954; BYTE $0x08 // vmovss\t8(%r9,%rcx,4), %xmm2            # xmm2 = mem[0],zero,zero,zero\n\tLONG $0x5259eac5; BYTE $0xf0               // vmulss\t-16(%rdx), %xmm2, %xmm2\n\tLONG $0xc958eac5                           // vaddss\t%xmm1, %xmm2, %xmm1\n\tWORD $0xfe83; BYTE $0x03                   // cmpl\t$3, %esi\n\tJE   LBB12_58\n\tLONG $0x107ac1c4; WORD $0x8954; BYTE $0x0c // vmovss\t12(%r9,%rcx,4), %xmm2           # xmm2 = mem[0],zero,zero,zero\n\tLONG $0x5259eac5; BYTE $0xf4               // vmulss\t-12(%rdx), %xmm2, %xmm2\n\tLONG $0xc958eac5                           // vaddss\t%xmm1, %xmm2, %xmm1\n\tWORD $0xfe83; BYTE $0x04                   // cmpl\t$4, %esi\n\tJE   LBB12_58\n\tLONG $0x107ac1c4; WORD $0x8954; BYTE $0x10 // vmovss\t16(%r9,%rcx,4), %xmm2           # xmm2 = mem[0],zero,zero,zero\n\tLONG $0x5259eac5; BYTE $0xf8               // vmulss\t-8(%rdx), %xmm2, %xmm2\n\tLONG $0xc958eac5                           // vaddss\t%xmm1, %xmm2, %xmm1\n\tWORD $0xfe83; BYTE $0x05                   // cmpl\t$5, %esi\n\tJE   LBB12_58\n\tLONG $0x107ac1c4; WORD $0x8954; BYTE $0x14 // vmovss\t20(%r9,%rcx,4), %xmm2           # xmm2 = mem[0],zero,zero,zero\n\tLONG $0x5259eac5; BYTE $0xfc               // vmulss\t-4(%rdx), %xmm2, %xmm2\n\tLONG $0xc958eac5                           // vaddss\t%xmm1, %xmm2, %xmm1\n\tWORD $0xfe83; BYTE $0x06                   // cmpl\t$6, %esi\n\tJE   LBB12_58\n\tLONG $0x107ac1c4; WORD $0x8954; BYTE $0x18 // vmovss\t24(%r9,%rcx,4), %xmm2           # xmm2 = mem[0],zero,zero,zero\n\tLONG $0x1259eac5                           // vmulss\t(%rdx), %xmm2, %xmm2\n\tLONG $0xc958eac5                           // vaddss\t%xmm1, %xmm2, %xmm1\n\tJMP  LBB12_58\n\nLBB12_75:\n\tLONG $0x304d8b48             // movq\t48(%rbp), %rcx\n\tQUAD $0x000000008d248d4c     // leaq\t(,%rcx,4), %r12\n\tLONG $0x02e3c148             // shlq\t$2, %rbx\n\tLONG $0x24048b48             // movq\t(%rsp), %rax                    # 8-byte Reload\n\tWORD $0xc289                 // movl\t%eax, %edx\n\tWORD $0xe283; BYTE $0x07     // andl\t$7, %edx\n\tLONG $0x24548948; BYTE $0x10 // movq\t%rdx, 16(%rsp)                  # 8-byte Spill\n\tLONG $0x08f88348             // cmpq\t$8, %rax\n\tJAE  LBB12_82\n\tWORD $0x3145; BYTE $0xed     // xorl\t%r13d, %r13d\n\tJMP  LBB12_84\n\nLBB12_82:\n\tQUAD $0xffffffffffe0b848; WORD $0x7fff // movabsq\t$9223372036854775776, %rax      # imm = 0x7FFFFFFFFFFFFFE0\n\tLONG $0x18c88348                       // orq\t$24, %rax\n\tLONG $0x24042148                       // andq\t%rax, (%rsp)                    # 8-byte Folded Spill\n\tLONG $0x05e1c148                       // shlq\t$5, %rcx\n\tLONG $0x244c8948; BYTE $0x58           // movq\t%rcx, 88(%rsp)                  # 8-byte Spill\n\tWORD $0x3145; BYTE $0xed               // xorl\t%r13d, %r13d\n\nLBB12_83:\n\tWORD $0x894c; BYTE $0xf7     // movq\t%r14, %rdi\n\tWORD $0xf631                 // xorl\t%esi, %esi\n\tWORD $0x8948; BYTE $0xda     // movq\t%rbx, %rdx\n\tLONG $0x000000e8; BYTE $0x00 // callq\tmemset@PLT\n\tLONG $0x263c8d4f             // leaq\t(%r14,%r12), %r15\n\tWORD $0x894c; BYTE $0xff     // movq\t%r15, %rdi\n\tWORD $0xf631                 // xorl\t%esi, %esi\n\tWORD $0x8948; BYTE $0xda     // movq\t%rbx, %rdx\n\tLONG $0x000000e8; BYTE $0x00 // callq\tmemset@PLT\n\tWORD $0x014d; BYTE $0xe7     // addq\t%r12, %r15\n\tWORD $0x894c; BYTE $0xff     // movq\t%r15, %rdi\n\tWORD $0xf631                 // xorl\t%esi, %esi\n\tWORD $0x8948; BYTE $0xda     // movq\t%rbx, %rdx\n\tLONG $0x000000e8; BYTE $0x00 // callq\tmemset@PLT\n\tWORD $0x014d; BYTE $0xe7     // addq\t%r12, %r15\n\tWORD $0x894c; BYTE $0xff     // movq\t%r15, %rdi\n\tWORD $0xf631                 // xorl\t%esi, %esi\n\tWORD $0x8948; BYTE $0xda     // movq\t%rbx, %rdx\n\tLONG $0x000000e8; BYTE $0x00 // callq\tmemset@PLT\n\tWORD $0x014d; BYTE $0xe7     // addq\t%r12, %r15\n\tWORD $0x894c; BYTE $0xff     // movq\t%r15, %rdi\n\tWORD $0xf631                 // xorl\t%esi, %esi\n\tWORD $0x8948; BYTE $0xda     // movq\t%rbx, %rdx\n\tLONG $0x000000e8; BYTE $0x00 // callq\tmemset@PLT\n\tWORD $0x014d; BYTE $0xe7     // addq\t%r12, %r15\n\tWORD $0x894c; BYTE $0xff     // movq\t%r15, %rdi\n\tWORD $0xf631                 // xorl\t%esi, %esi\n\tWORD $0x8948; BYTE $0xda     // movq\t%rbx, %rdx\n\tLONG $0x000000e8; BYTE $0x00 // callq\tmemset@PLT\n\tWORD $0x014d; BYTE $0xe7     // addq\t%r12, %r15\n\tWORD $0x894c; BYTE $0xff     // movq\t%r15, %rdi\n\tWORD $0xf631                 // xorl\t%esi, %esi\n\tWORD $0x8948; BYTE $0xda     // movq\t%rbx, %rdx\n\tLONG $0x000000e8; BYTE $0x00 // callq\tmemset@PLT\n\tWORD $0x014d; BYTE $0xe7     // addq\t%r12, %r15\n\tWORD $0x894c; BYTE $0xff     // movq\t%r15, %rdi\n\tWORD $0xf631                 // xorl\t%esi, %esi\n\tWORD $0x8948; BYTE $0xda     // movq\t%rbx, %rdx\n\tLONG $0x000000e8; BYTE $0x00 // callq\tmemset@PLT\n\tLONG $0x24048b48             // movq\t(%rsp), %rax                    # 8-byte Reload\n\tLONG $0x08c58349             // addq\t$8, %r13\n\tLONG $0x2474034c; BYTE $0x58 // addq\t88(%rsp), %r14                  # 8-byte Folded Reload\n\tWORD $0x394c; BYTE $0xe8     // cmpq\t%r13, %rax\n\tJNE  LBB12_83\n\nLBB12_84:\n\tLONG $0x247c8b4c; BYTE $0x10 // movq\t16(%rsp), %r15                  # 8-byte Reload\n\tWORD $0x854d; BYTE $0xff     // testq\t%r15, %r15\n\tJE   LBB12_117\n\tLONG $0x6daf0f4c; BYTE $0x30 // imulq\t48(%rbp), %r13\n\tLONG $0x28458b48             // movq\t40(%rbp), %rax\n\tLONG $0xa8348d4e             // leaq\t(%rax,%r13,4), %r14\n\nLBB12_86:\n\tWORD $0x894c; BYTE $0xf7     // movq\t%r14, %rdi\n\tWORD $0xf631                 // xorl\t%esi, %esi\n\tWORD $0x8948; BYTE $0xda     // movq\t%rbx, %rdx\n\tLONG $0x000000e8; BYTE $0x00 // callq\tmemset@PLT\n\tWORD $0x014d; BYTE $0xe6     // addq\t%r12, %r14\n\tWORD $0xff49; BYTE $0xcf     // decq\t%r15\n\tJNE  LBB12_86\n\nLBB12_117:\n\tLONG $0xd8658d48         // leaq\t-40(%rbp), %rsp\n\tBYTE $0x5b               // popq\t%rbx\n\tWORD $0x5c41             // popq\t%r12\n\tWORD $0x5d41             // popq\t%r13\n\tWORD $0x5e41             // popq\t%r14\n\tWORD $0x5f41             // popq\t%r15\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tPOPQ DI\n\tPOPQ DI\n\tPOPQ DI\n\tPOPQ DI\n\tPOPQ DI\n\tPOPQ DI\n\tRET\n"
  },
  {
    "path": "common/floats/floats_avx512.go",
    "content": "//go:build !noasm && amd64\n// Code generated by GoAT. DO NOT EDIT.\n// versions:\n// \tclang   19.1.7 (++20250114103320+cd708029e0b2-1~exp1~20250114103432.75)\n// \tobjdump 2.38\n// flags: -mavx -mfma -mavx512f -O3\n// source: src/floats_avx512.c\n\npackage floats\n\nimport \"unsafe\"\n\n//go:noescape\nfunc _mm512_mul_const_add_to(a, b, c, dst unsafe.Pointer, n int64)\n\n//go:noescape\nfunc _mm512_mul_const_add(a, b, c unsafe.Pointer, n int64)\n\n//go:noescape\nfunc _mm512_mul_const_to(a, b, c unsafe.Pointer, n int64)\n\n//go:noescape\nfunc _mm512_mul_const(a, b unsafe.Pointer, n int64)\n\n//go:noescape\nfunc _mm512_add_const(a, b unsafe.Pointer, n int64)\n\n//go:noescape\nfunc _mm512_sub_to(a, b, c unsafe.Pointer, n int64)\n\n//go:noescape\nfunc _mm512_sub(a, b unsafe.Pointer, n int64)\n\n//go:noescape\nfunc _mm512_mul_to(a, b, c unsafe.Pointer, n int64)\n\n//go:noescape\nfunc _mm512_div_to(a, b, c unsafe.Pointer, n int64)\n\n//go:noescape\nfunc _mm512_sqrt_to(a, b unsafe.Pointer, n int64)\n\n//go:noescape\nfunc _mm512_dot(a, b unsafe.Pointer, n int64) (result float32)\n\n//go:noescape\nfunc _mm512_euclidean(a, b unsafe.Pointer, n int64) (result float32)\n\n//go:noescape\nfunc _mm512_mm(transA, transB bool, m, n, k int64, a unsafe.Pointer, lda int64, b unsafe.Pointer, ldb int64, c unsafe.Pointer, ldc int64)\n"
  },
  {
    "path": "common/floats/floats_avx512.s",
    "content": "//go:build !noasm && amd64\n// Code generated by GoAT. DO NOT EDIT.\n// versions:\n// \tclang   19.1.7 (++20250114103320+cd708029e0b2-1~exp1~20250114103432.75)\n// \tobjdump 2.38\n// flags: -mavx -mfma -mavx512f -O3\n// source: src/floats_avx512.c\n\nTEXT ·_mm512_mul_const_add_to(SB), $0-40\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ c+16(FP), DX\n\tMOVQ dst+24(FP), CX\n\tMOVQ n+32(FP), R8\n\tBYTE $0x55                                 // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5                   // movq\t%rsp, %rbp\n\tLONG $0xf8e48348                           // andq\t$-8, %rsp\n\tLONG $0x0f488d4d                           // leaq\t15(%r8), %r9\n\tWORD $0x854d; BYTE $0xc0                   // testq\t%r8, %r8\n\tLONG $0xc8490f4d                           // cmovnsq\t%r8, %r9\n\tWORD $0x894c; BYTE $0xc8                   // movq\t%r9, %rax\n\tLONG $0x04f8c148                           // sarq\t$4, %rax\n\tLONG $0xf0e18349                           // andq\t$-16, %r9\n\tWORD $0x294d; BYTE $0xc8                   // subq\t%r9, %r8\n\tWORD $0xc085                               // testl\t%eax, %eax\n\tJLE  LBB0_6\n\tWORD $0xf883; BYTE $0x01                   // cmpl\t$1, %eax\n\tJE   LBB0_4\n\tWORD $0x8941; BYTE $0xc1                   // movl\t%eax, %r9d\n\tLONG $0xfee18141; WORD $0xffff; BYTE $0x7f // andl\t$2147483646, %r9d               # imm = 0x7FFFFFFE\n\nLBB0_3:\n\tLONG $0x487cf162; WORD $0x0710             // vmovups\t(%rdi), %zmm0\n\tLONG $0x487df262; WORD $0x0e18             // vbroadcastss\t(%rsi), %zmm1\n\tLONG $0x487df262; WORD $0x0aa8             // vfmadd213ps\t(%rdx), %zmm0, %zmm1    # zmm1 = (zmm0 * zmm1) + mem\n\tLONG $0x487cf162; WORD $0x0911             // vmovups\t%zmm1, (%rcx)\n\tLONG $0x487df262; WORD $0x0618             // vbroadcastss\t(%rsi), %zmm0\n\tLONG $0x487cf162; WORD $0x4f10; BYTE $0x01 // vmovups\t64(%rdi), %zmm1\n\tLONG $0x4875f262; WORD $0x42a8; BYTE $0x01 // vfmadd213ps\t64(%rdx), %zmm1, %zmm0  # zmm0 = (zmm1 * zmm0) + mem\n\tLONG $0x487cf162; WORD $0x4111; BYTE $0x01 // vmovups\t%zmm0, 64(%rcx)\n\tLONG $0x80ef8348                           // subq\t$-128, %rdi\n\tLONG $0x80ea8348                           // subq\t$-128, %rdx\n\tLONG $0x80e98348                           // subq\t$-128, %rcx\n\tLONG $0xfec18341                           // addl\t$-2, %r9d\n\tJNE  LBB0_3\n\nLBB0_4:\n\tWORD $0x01a8                   // testb\t$1, %al\n\tJE   LBB0_6\n\tLONG $0x487cf162; WORD $0x0710 // vmovups\t(%rdi), %zmm0\n\tLONG $0x487df262; WORD $0x0e18 // vbroadcastss\t(%rsi), %zmm1\n\tLONG $0x487df262; WORD $0x0aa8 // vfmadd213ps\t(%rdx), %zmm0, %zmm1    # zmm1 = (zmm0 * zmm1) + mem\n\tLONG $0x487cf162; WORD $0x0911 // vmovups\t%zmm1, (%rcx)\n\tLONG $0x40c78348               // addq\t$64, %rdi\n\tLONG $0x40c28348               // addq\t$64, %rdx\n\tLONG $0x40c18348               // addq\t$64, %rcx\n\nLBB0_6:\n\tLONG $0x07f88349             // cmpq\t$7, %r8\n\tJLE  LBB0_8\n\tLONG $0x187de2c4; BYTE $0x06 // vbroadcastss\t(%rsi), %ymm0\n\tLONG $0x0759fcc5             // vmulps\t(%rdi), %ymm0, %ymm0\n\tLONG $0x0258fcc5             // vaddps\t(%rdx), %ymm0, %ymm0\n\tLONG $0x0111fcc5             // vmovups\t%ymm0, (%rcx)\n\tLONG $0x20c78348             // addq\t$32, %rdi\n\tLONG $0x20c28348             // addq\t$32, %rdx\n\tLONG $0x20c18348             // addq\t$32, %rcx\n\tLONG $0xf8c08341             // addl\t$-8, %r8d\n\nLBB0_8:\n\tWORD $0x8545; BYTE $0xc0 // testl\t%r8d, %r8d\n\tJLE  LBB0_13\n\tWORD $0x8944; BYTE $0xc0 // movl\t%r8d, %eax\n\tLONG $0x01f88341         // cmpl\t$1, %r8d\n\tJNE  LBB0_14\n\tWORD $0x3145; BYTE $0xc0 // xorl\t%r8d, %r8d\n\tJMP  LBB0_11\n\nLBB0_14:\n\tWORD $0x8941; BYTE $0xc1                   // movl\t%eax, %r9d\n\tLONG $0xfee18141; WORD $0xffff; BYTE $0x7f // andl\t$2147483646, %r9d               # imm = 0x7FFFFFFE\n\tWORD $0x3145; BYTE $0xc0                   // xorl\t%r8d, %r8d\n\nLBB0_15:\n\tLONG $0x107aa1c4; WORD $0x8704             // vmovss\t(%rdi,%r8,4), %xmm0             # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0e10fac5                           // vmovss\t(%rsi), %xmm1                   # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xa979a2c4; WORD $0x820c             // vfmadd213ss\t(%rdx,%r8,4), %xmm0, %xmm1 # xmm1 = (xmm0 * xmm1) + mem\n\tLONG $0x117aa1c4; WORD $0x810c             // vmovss\t%xmm1, (%rcx,%r8,4)\n\tLONG $0x107aa1c4; WORD $0x8744; BYTE $0x04 // vmovss\t4(%rdi,%r8,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0e10fac5                           // vmovss\t(%rsi), %xmm1                   # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xa979a2c4; WORD $0x824c; BYTE $0x04 // vfmadd213ss\t4(%rdx,%r8,4), %xmm0, %xmm1 # xmm1 = (xmm0 * xmm1) + mem\n\tLONG $0x117aa1c4; WORD $0x814c; BYTE $0x04 // vmovss\t%xmm1, 4(%rcx,%r8,4)\n\tLONG $0x02c08349                           // addq\t$2, %r8\n\tWORD $0x394d; BYTE $0xc1                   // cmpq\t%r8, %r9\n\tJNE  LBB0_15\n\nLBB0_11:\n\tWORD $0x01a8                   // testb\t$1, %al\n\tJE   LBB0_13\n\tLONG $0x107aa1c4; WORD $0x8704 // vmovss\t(%rdi,%r8,4), %xmm0             # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0e10fac5               // vmovss\t(%rsi), %xmm1                   # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xa979a2c4; WORD $0x820c // vfmadd213ss\t(%rdx,%r8,4), %xmm0, %xmm1 # xmm1 = (xmm0 * xmm1) + mem\n\tLONG $0x117aa1c4; WORD $0x810c // vmovss\t%xmm1, (%rcx,%r8,4)\n\nLBB0_13:\n\tWORD $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tRET\n\nTEXT ·_mm512_mul_const_add(SB), $0-32\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ c+16(FP), DX\n\tMOVQ n+24(FP), CX\n\tBYTE $0x55                                 // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5                   // movq\t%rsp, %rbp\n\tLONG $0xf8e48348                           // andq\t$-8, %rsp\n\tLONG $0x0f418d4c                           // leaq\t15(%rcx), %r8\n\tWORD $0x8548; BYTE $0xc9                   // testq\t%rcx, %rcx\n\tLONG $0xc1490f4c                           // cmovnsq\t%rcx, %r8\n\tWORD $0x894c; BYTE $0xc0                   // movq\t%r8, %rax\n\tLONG $0x04f8c148                           // sarq\t$4, %rax\n\tLONG $0xf0e08349                           // andq\t$-16, %r8\n\tWORD $0x294c; BYTE $0xc1                   // subq\t%r8, %rcx\n\tWORD $0xc085                               // testl\t%eax, %eax\n\tJLE  LBB1_6\n\tWORD $0xf883; BYTE $0x01                   // cmpl\t$1, %eax\n\tJE   LBB1_4\n\tWORD $0x8941; BYTE $0xc0                   // movl\t%eax, %r8d\n\tLONG $0xfee08141; WORD $0xffff; BYTE $0x7f // andl\t$2147483646, %r8d               # imm = 0x7FFFFFFE\n\nLBB1_3:\n\tLONG $0x487cf162; WORD $0x0710             // vmovups\t(%rdi), %zmm0\n\tLONG $0x487df262; WORD $0x0e18             // vbroadcastss\t(%rsi), %zmm1\n\tLONG $0x487df262; WORD $0x0aa8             // vfmadd213ps\t(%rdx), %zmm0, %zmm1    # zmm1 = (zmm0 * zmm1) + mem\n\tLONG $0x487cf162; WORD $0x0a11             // vmovups\t%zmm1, (%rdx)\n\tLONG $0x487cf162; WORD $0x4710; BYTE $0x01 // vmovups\t64(%rdi), %zmm0\n\tLONG $0x487df262; WORD $0x0e18             // vbroadcastss\t(%rsi), %zmm1\n\tLONG $0x487df262; WORD $0x4aa8; BYTE $0x01 // vfmadd213ps\t64(%rdx), %zmm0, %zmm1  # zmm1 = (zmm0 * zmm1) + mem\n\tLONG $0x487cf162; WORD $0x4a11; BYTE $0x01 // vmovups\t%zmm1, 64(%rdx)\n\tLONG $0x80ef8348                           // subq\t$-128, %rdi\n\tLONG $0x80ea8348                           // subq\t$-128, %rdx\n\tLONG $0xfec08341                           // addl\t$-2, %r8d\n\tJNE  LBB1_3\n\nLBB1_4:\n\tWORD $0x01a8                   // testb\t$1, %al\n\tJE   LBB1_6\n\tLONG $0x487cf162; WORD $0x0710 // vmovups\t(%rdi), %zmm0\n\tLONG $0x487df262; WORD $0x0e18 // vbroadcastss\t(%rsi), %zmm1\n\tLONG $0x487df262; WORD $0x0aa8 // vfmadd213ps\t(%rdx), %zmm0, %zmm1    # zmm1 = (zmm0 * zmm1) + mem\n\tLONG $0x487cf162; WORD $0x0a11 // vmovups\t%zmm1, (%rdx)\n\tLONG $0x40c78348               // addq\t$64, %rdi\n\tLONG $0x40c28348               // addq\t$64, %rdx\n\nLBB1_6:\n\tLONG $0x07f98348             // cmpq\t$7, %rcx\n\tJLE  LBB1_8\n\tLONG $0x187de2c4; BYTE $0x06 // vbroadcastss\t(%rsi), %ymm0\n\tLONG $0x0759fcc5             // vmulps\t(%rdi), %ymm0, %ymm0\n\tLONG $0x0258fcc5             // vaddps\t(%rdx), %ymm0, %ymm0\n\tLONG $0x0211fcc5             // vmovups\t%ymm0, (%rdx)\n\tLONG $0x20c78348             // addq\t$32, %rdi\n\tLONG $0x20c28348             // addq\t$32, %rdx\n\tWORD $0xc183; BYTE $0xf8     // addl\t$-8, %ecx\n\nLBB1_8:\n\tWORD $0xc985             // testl\t%ecx, %ecx\n\tJLE  LBB1_13\n\tWORD $0xc889             // movl\t%ecx, %eax\n\tWORD $0xf983; BYTE $0x01 // cmpl\t$1, %ecx\n\tJNE  LBB1_14\n\tWORD $0xc931             // xorl\t%ecx, %ecx\n\tJMP  LBB1_11\n\nLBB1_14:\n\tWORD $0x8941; BYTE $0xc0                   // movl\t%eax, %r8d\n\tLONG $0xfee08141; WORD $0xffff; BYTE $0x7f // andl\t$2147483646, %r8d               # imm = 0x7FFFFFFE\n\tWORD $0xc931                               // xorl\t%ecx, %ecx\n\nLBB1_15:\n\tLONG $0x0410fac5; BYTE $0x8f               // vmovss\t(%rdi,%rcx,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0e10fac5                           // vmovss\t(%rsi), %xmm1                   # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xa979e2c4; WORD $0x8a0c             // vfmadd213ss\t(%rdx,%rcx,4), %xmm0, %xmm1 # xmm1 = (xmm0 * xmm1) + mem\n\tLONG $0x0c11fac5; BYTE $0x8a               // vmovss\t%xmm1, (%rdx,%rcx,4)\n\tLONG $0x4410fac5; WORD $0x048f             // vmovss\t4(%rdi,%rcx,4), %xmm0           # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0e10fac5                           // vmovss\t(%rsi), %xmm1                   # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xa979e2c4; WORD $0x8a4c; BYTE $0x04 // vfmadd213ss\t4(%rdx,%rcx,4), %xmm0, %xmm1 # xmm1 = (xmm0 * xmm1) + mem\n\tLONG $0x4c11fac5; WORD $0x048a             // vmovss\t%xmm1, 4(%rdx,%rcx,4)\n\tLONG $0x02c18348                           // addq\t$2, %rcx\n\tWORD $0x3949; BYTE $0xc8                   // cmpq\t%rcx, %r8\n\tJNE  LBB1_15\n\nLBB1_11:\n\tWORD $0x01a8                   // testb\t$1, %al\n\tJE   LBB1_13\n\tLONG $0x0410fac5; BYTE $0x8f   // vmovss\t(%rdi,%rcx,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0e10fac5               // vmovss\t(%rsi), %xmm1                   # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xa979e2c4; WORD $0x8a0c // vfmadd213ss\t(%rdx,%rcx,4), %xmm0, %xmm1 # xmm1 = (xmm0 * xmm1) + mem\n\tLONG $0x0c11fac5; BYTE $0x8a   // vmovss\t%xmm1, (%rdx,%rcx,4)\n\nLBB1_13:\n\tWORD $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tRET\n\nTEXT ·_mm512_mul_const_to(SB), $0-32\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ c+16(FP), DX\n\tMOVQ n+24(FP), CX\n\tBYTE $0x55                                 // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5                   // movq\t%rsp, %rbp\n\tLONG $0xf8e48348                           // andq\t$-8, %rsp\n\tLONG $0x0f418d48                           // leaq\t15(%rcx), %rax\n\tWORD $0x8548; BYTE $0xc9                   // testq\t%rcx, %rcx\n\tLONG $0xc1490f48                           // cmovnsq\t%rcx, %rax\n\tWORD $0x8949; BYTE $0xc0                   // movq\t%rax, %r8\n\tLONG $0x04f8c149                           // sarq\t$4, %r8\n\tLONG $0xf0e08348                           // andq\t$-16, %rax\n\tWORD $0x2948; BYTE $0xc1                   // subq\t%rax, %rcx\n\tWORD $0x8545; BYTE $0xc0                   // testl\t%r8d, %r8d\n\tJLE  LBB2_6\n\tWORD $0x8944; BYTE $0xc0                   // movl\t%r8d, %eax\n\tWORD $0xe083; BYTE $0x03                   // andl\t$3, %eax\n\tLONG $0x04f88341                           // cmpl\t$4, %r8d\n\tJB   LBB2_4\n\tLONG $0xfce08141; WORD $0xffff; BYTE $0x7f // andl\t$2147483644, %r8d               # imm = 0x7FFFFFFC\n\nLBB2_3:\n\tLONG $0x487cf162; WORD $0x0710             // vmovups\t(%rdi), %zmm0\n\tLONG $0x587cf162; WORD $0x0659             // vmulps\t(%rsi){1to16}, %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x0211             // vmovups\t%zmm0, (%rdx)\n\tLONG $0x487cf162; WORD $0x4710; BYTE $0x01 // vmovups\t64(%rdi), %zmm0\n\tLONG $0x587cf162; WORD $0x0659             // vmulps\t(%rsi){1to16}, %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x4211; BYTE $0x01 // vmovups\t%zmm0, 64(%rdx)\n\tLONG $0x487cf162; WORD $0x4710; BYTE $0x02 // vmovups\t128(%rdi), %zmm0\n\tLONG $0x587cf162; WORD $0x0659             // vmulps\t(%rsi){1to16}, %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x4211; BYTE $0x02 // vmovups\t%zmm0, 128(%rdx)\n\tLONG $0x487cf162; WORD $0x4710; BYTE $0x03 // vmovups\t192(%rdi), %zmm0\n\tLONG $0x587cf162; WORD $0x0659             // vmulps\t(%rsi){1to16}, %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x4211; BYTE $0x03 // vmovups\t%zmm0, 192(%rdx)\n\tLONG $0x00c78148; WORD $0x0001; BYTE $0x00 // addq\t$256, %rdi                      # imm = 0x100\n\tLONG $0x00c28148; WORD $0x0001; BYTE $0x00 // addq\t$256, %rdx                      # imm = 0x100\n\tLONG $0xfcc08341                           // addl\t$-4, %r8d\n\tJNE  LBB2_3\n\nLBB2_4:\n\tWORD $0xc085 // testl\t%eax, %eax\n\tJE   LBB2_6\n\nLBB2_5:\n\tLONG $0x487cf162; WORD $0x0710 // vmovups\t(%rdi), %zmm0\n\tLONG $0x587cf162; WORD $0x0659 // vmulps\t(%rsi){1to16}, %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x0211 // vmovups\t%zmm0, (%rdx)\n\tLONG $0x40c78348               // addq\t$64, %rdi\n\tLONG $0x40c28348               // addq\t$64, %rdx\n\tWORD $0xc8ff                   // decl\t%eax\n\tJNE  LBB2_5\n\nLBB2_6:\n\tLONG $0x07f98348             // cmpq\t$7, %rcx\n\tJLE  LBB2_8\n\tLONG $0x187de2c4; BYTE $0x06 // vbroadcastss\t(%rsi), %ymm0\n\tLONG $0x0759fcc5             // vmulps\t(%rdi), %ymm0, %ymm0\n\tLONG $0x0211fcc5             // vmovups\t%ymm0, (%rdx)\n\tLONG $0x20c78348             // addq\t$32, %rdi\n\tLONG $0x20c28348             // addq\t$32, %rdx\n\tWORD $0xc183; BYTE $0xf8     // addl\t$-8, %ecx\n\nLBB2_8:\n\tWORD $0xc985             // testl\t%ecx, %ecx\n\tJLE  LBB2_14\n\tWORD $0x8941; BYTE $0xc8 // movl\t%ecx, %r8d\n\tWORD $0x8944; BYTE $0xc0 // movl\t%r8d, %eax\n\tWORD $0xe083; BYTE $0x03 // andl\t$3, %eax\n\tWORD $0xf983; BYTE $0x04 // cmpl\t$4, %ecx\n\tJAE  LBB2_15\n\tWORD $0xc931             // xorl\t%ecx, %ecx\n\tJMP  LBB2_11\n\nLBB2_15:\n\tLONG $0xfce08141; WORD $0xffff; BYTE $0x7f // andl\t$2147483644, %r8d               # imm = 0x7FFFFFFC\n\tWORD $0xc931                               // xorl\t%ecx, %ecx\n\nLBB2_16:\n\tLONG $0x0410fac5; BYTE $0x8f   // vmovss\t(%rdi,%rcx,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5               // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x0411fac5; BYTE $0x8a   // vmovss\t%xmm0, (%rdx,%rcx,4)\n\tLONG $0x4410fac5; WORD $0x048f // vmovss\t4(%rdi,%rcx,4), %xmm0           # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5               // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x048a // vmovss\t%xmm0, 4(%rdx,%rcx,4)\n\tLONG $0x4410fac5; WORD $0x088f // vmovss\t8(%rdi,%rcx,4), %xmm0           # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5               // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x088a // vmovss\t%xmm0, 8(%rdx,%rcx,4)\n\tLONG $0x4410fac5; WORD $0x0c8f // vmovss\t12(%rdi,%rcx,4), %xmm0          # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5               // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x0c8a // vmovss\t%xmm0, 12(%rdx,%rcx,4)\n\tLONG $0x04c18348               // addq\t$4, %rcx\n\tWORD $0x3949; BYTE $0xc8       // cmpq\t%rcx, %r8\n\tJNE  LBB2_16\n\nLBB2_11:\n\tWORD $0x8548; BYTE $0xc0 // testq\t%rax, %rax\n\tJE   LBB2_14\n\tLONG $0x8a148d48         // leaq\t(%rdx,%rcx,4), %rdx\n\tLONG $0x8f0c8d48         // leaq\t(%rdi,%rcx,4), %rcx\n\tWORD $0xff31             // xorl\t%edi, %edi\n\nLBB2_13:\n\tLONG $0x0410fac5; BYTE $0xb9 // vmovss\t(%rcx,%rdi,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0659fac5             // vmulss\t(%rsi), %xmm0, %xmm0\n\tLONG $0x0411fac5; BYTE $0xba // vmovss\t%xmm0, (%rdx,%rdi,4)\n\tWORD $0xff48; BYTE $0xc7     // incq\t%rdi\n\tWORD $0x3948; BYTE $0xf8     // cmpq\t%rdi, %rax\n\tJNE  LBB2_13\n\nLBB2_14:\n\tWORD $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tRET\n\nTEXT ·_mm512_mul_const(SB), $0-24\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ n+16(FP), DX\n\tBYTE $0x55                     // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5       // movq\t%rsp, %rbp\n\tLONG $0xf8e48348               // andq\t$-8, %rsp\n\tLONG $0x0f428d48               // leaq\t15(%rdx), %rax\n\tWORD $0x8548; BYTE $0xd2       // testq\t%rdx, %rdx\n\tLONG $0xc2490f48               // cmovnsq\t%rdx, %rax\n\tWORD $0x8948; BYTE $0xc1       // movq\t%rax, %rcx\n\tLONG $0x04f9c148               // sarq\t$4, %rcx\n\tLONG $0xf0e08348               // andq\t$-16, %rax\n\tWORD $0x2948; BYTE $0xc2       // subq\t%rax, %rdx\n\tWORD $0xc985                   // testl\t%ecx, %ecx\n\tJLE  LBB3_6\n\tWORD $0xc889                   // movl\t%ecx, %eax\n\tWORD $0xe083; BYTE $0x03       // andl\t$3, %eax\n\tWORD $0xf983; BYTE $0x04       // cmpl\t$4, %ecx\n\tJB   LBB3_4\n\tLONG $0xfffce181; WORD $0x7fff // andl\t$2147483644, %ecx               # imm = 0x7FFFFFFC\n\nLBB3_3:\n\tLONG $0x487cf162; WORD $0x0710             // vmovups\t(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x4f10; BYTE $0x01 // vmovups\t64(%rdi), %zmm1\n\tLONG $0x487cf162; WORD $0x5710; BYTE $0x02 // vmovups\t128(%rdi), %zmm2\n\tLONG $0x587cf162; WORD $0x0659             // vmulps\t(%rsi){1to16}, %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x5f10; BYTE $0x03 // vmovups\t192(%rdi), %zmm3\n\tLONG $0x487cf162; WORD $0x0711             // vmovups\t%zmm0, (%rdi)\n\tLONG $0x5874f162; WORD $0x0659             // vmulps\t(%rsi){1to16}, %zmm1, %zmm0\n\tLONG $0x487cf162; WORD $0x4711; BYTE $0x01 // vmovups\t%zmm0, 64(%rdi)\n\tLONG $0x586cf162; WORD $0x0659             // vmulps\t(%rsi){1to16}, %zmm2, %zmm0\n\tLONG $0x487cf162; WORD $0x4711; BYTE $0x02 // vmovups\t%zmm0, 128(%rdi)\n\tLONG $0x5864f162; WORD $0x0659             // vmulps\t(%rsi){1to16}, %zmm3, %zmm0\n\tLONG $0x487cf162; WORD $0x4711; BYTE $0x03 // vmovups\t%zmm0, 192(%rdi)\n\tLONG $0x00c78148; WORD $0x0001; BYTE $0x00 // addq\t$256, %rdi                      # imm = 0x100\n\tWORD $0xc183; BYTE $0xfc                   // addl\t$-4, %ecx\n\tJNE  LBB3_3\n\nLBB3_4:\n\tWORD $0xc085 // testl\t%eax, %eax\n\tJE   LBB3_6\n\nLBB3_5:\n\tLONG $0x487cf162; WORD $0x0710 // vmovups\t(%rdi), %zmm0\n\tLONG $0x587cf162; WORD $0x0659 // vmulps\t(%rsi){1to16}, %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x0711 // vmovups\t%zmm0, (%rdi)\n\tLONG $0x40c78348               // addq\t$64, %rdi\n\tWORD $0xc8ff                   // decl\t%eax\n\tJNE  LBB3_5\n\nLBB3_6:\n\tLONG $0x07fa8348             // cmpq\t$7, %rdx\n\tJLE  LBB3_8\n\tLONG $0x187de2c4; BYTE $0x06 // vbroadcastss\t(%rsi), %ymm0\n\tLONG $0x0759fcc5             // vmulps\t(%rdi), %ymm0, %ymm0\n\tLONG $0x0711fcc5             // vmovups\t%ymm0, (%rdi)\n\tLONG $0x20c78348             // addq\t$32, %rdi\n\tWORD $0xc283; BYTE $0xf8     // addl\t$-8, %edx\n\nLBB3_8:\n\tWORD $0xd285             // testl\t%edx, %edx\n\tJLE  LBB3_14\n\tWORD $0xd189             // movl\t%edx, %ecx\n\tWORD $0xc889             // movl\t%ecx, %eax\n\tWORD $0xe083; BYTE $0x03 // andl\t$3, %eax\n\tWORD $0xfa83; BYTE $0x04 // cmpl\t$4, %edx\n\tJAE  LBB3_15\n\tWORD $0xd231             // xorl\t%edx, %edx\n\tJMP  LBB3_11\n\nLBB3_15:\n\tLONG $0xfffce181; WORD $0x7fff // andl\t$2147483644, %ecx               # imm = 0x7FFFFFFC\n\tWORD $0xd231                   // xorl\t%edx, %edx\n\nLBB3_16:\n\tLONG $0x0610fac5               // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0459fac5; BYTE $0x97   // vmulss\t(%rdi,%rdx,4), %xmm0, %xmm0\n\tLONG $0x0411fac5; BYTE $0x97   // vmovss\t%xmm0, (%rdi,%rdx,4)\n\tLONG $0x0610fac5               // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4459fac5; WORD $0x0497 // vmulss\t4(%rdi,%rdx,4), %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x0497 // vmovss\t%xmm0, 4(%rdi,%rdx,4)\n\tLONG $0x0610fac5               // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4459fac5; WORD $0x0897 // vmulss\t8(%rdi,%rdx,4), %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x0897 // vmovss\t%xmm0, 8(%rdi,%rdx,4)\n\tLONG $0x0610fac5               // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4459fac5; WORD $0x0c97 // vmulss\t12(%rdi,%rdx,4), %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x0c97 // vmovss\t%xmm0, 12(%rdi,%rdx,4)\n\tLONG $0x04c28348               // addq\t$4, %rdx\n\tWORD $0x3948; BYTE $0xd1       // cmpq\t%rdx, %rcx\n\tJNE  LBB3_16\n\nLBB3_11:\n\tWORD $0x8548; BYTE $0xc0 // testq\t%rax, %rax\n\tJE   LBB3_14\n\tLONG $0x970c8d48         // leaq\t(%rdi,%rdx,4), %rcx\n\tWORD $0xd231             // xorl\t%edx, %edx\n\nLBB3_13:\n\tLONG $0x0610fac5             // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0459fac5; BYTE $0x91 // vmulss\t(%rcx,%rdx,4), %xmm0, %xmm0\n\tLONG $0x0411fac5; BYTE $0x91 // vmovss\t%xmm0, (%rcx,%rdx,4)\n\tWORD $0xff48; BYTE $0xc2     // incq\t%rdx\n\tWORD $0x3948; BYTE $0xd0     // cmpq\t%rdx, %rax\n\tJNE  LBB3_13\n\nLBB3_14:\n\tWORD $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tRET\n\nTEXT ·_mm512_add_const(SB), $0-24\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ n+16(FP), DX\n\tBYTE $0x55                     // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5       // movq\t%rsp, %rbp\n\tLONG $0xf8e48348               // andq\t$-8, %rsp\n\tLONG $0x0f428d48               // leaq\t15(%rdx), %rax\n\tWORD $0x8548; BYTE $0xd2       // testq\t%rdx, %rdx\n\tLONG $0xc2490f48               // cmovnsq\t%rdx, %rax\n\tWORD $0x8948; BYTE $0xc1       // movq\t%rax, %rcx\n\tLONG $0x04f9c148               // sarq\t$4, %rcx\n\tLONG $0xf0e08348               // andq\t$-16, %rax\n\tWORD $0x2948; BYTE $0xc2       // subq\t%rax, %rdx\n\tWORD $0xc985                   // testl\t%ecx, %ecx\n\tJLE  LBB4_6\n\tWORD $0xc889                   // movl\t%ecx, %eax\n\tWORD $0xe083; BYTE $0x03       // andl\t$3, %eax\n\tWORD $0xf983; BYTE $0x04       // cmpl\t$4, %ecx\n\tJB   LBB4_4\n\tLONG $0xfffce181; WORD $0x7fff // andl\t$2147483644, %ecx               # imm = 0x7FFFFFFC\n\nLBB4_3:\n\tLONG $0x487cf162; WORD $0x0710             // vmovups\t(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x4f10; BYTE $0x01 // vmovups\t64(%rdi), %zmm1\n\tLONG $0x487cf162; WORD $0x5710; BYTE $0x02 // vmovups\t128(%rdi), %zmm2\n\tLONG $0x587cf162; WORD $0x0658             // vaddps\t(%rsi){1to16}, %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x5f10; BYTE $0x03 // vmovups\t192(%rdi), %zmm3\n\tLONG $0x487cf162; WORD $0x0711             // vmovups\t%zmm0, (%rdi)\n\tLONG $0x5874f162; WORD $0x0658             // vaddps\t(%rsi){1to16}, %zmm1, %zmm0\n\tLONG $0x487cf162; WORD $0x4711; BYTE $0x01 // vmovups\t%zmm0, 64(%rdi)\n\tLONG $0x586cf162; WORD $0x0658             // vaddps\t(%rsi){1to16}, %zmm2, %zmm0\n\tLONG $0x487cf162; WORD $0x4711; BYTE $0x02 // vmovups\t%zmm0, 128(%rdi)\n\tLONG $0x5864f162; WORD $0x0658             // vaddps\t(%rsi){1to16}, %zmm3, %zmm0\n\tLONG $0x487cf162; WORD $0x4711; BYTE $0x03 // vmovups\t%zmm0, 192(%rdi)\n\tLONG $0x00c78148; WORD $0x0001; BYTE $0x00 // addq\t$256, %rdi                      # imm = 0x100\n\tWORD $0xc183; BYTE $0xfc                   // addl\t$-4, %ecx\n\tJNE  LBB4_3\n\nLBB4_4:\n\tWORD $0xc085 // testl\t%eax, %eax\n\tJE   LBB4_6\n\nLBB4_5:\n\tLONG $0x487cf162; WORD $0x0710 // vmovups\t(%rdi), %zmm0\n\tLONG $0x587cf162; WORD $0x0658 // vaddps\t(%rsi){1to16}, %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x0711 // vmovups\t%zmm0, (%rdi)\n\tLONG $0x40c78348               // addq\t$64, %rdi\n\tWORD $0xc8ff                   // decl\t%eax\n\tJNE  LBB4_5\n\nLBB4_6:\n\tLONG $0x07fa8348             // cmpq\t$7, %rdx\n\tJLE  LBB4_8\n\tLONG $0x187de2c4; BYTE $0x06 // vbroadcastss\t(%rsi), %ymm0\n\tLONG $0x0758fcc5             // vaddps\t(%rdi), %ymm0, %ymm0\n\tLONG $0x0711fcc5             // vmovups\t%ymm0, (%rdi)\n\tLONG $0x20c78348             // addq\t$32, %rdi\n\tWORD $0xc283; BYTE $0xf8     // addl\t$-8, %edx\n\nLBB4_8:\n\tWORD $0xd285             // testl\t%edx, %edx\n\tJLE  LBB4_14\n\tWORD $0xd189             // movl\t%edx, %ecx\n\tWORD $0xc889             // movl\t%ecx, %eax\n\tWORD $0xe083; BYTE $0x03 // andl\t$3, %eax\n\tWORD $0xfa83; BYTE $0x04 // cmpl\t$4, %edx\n\tJAE  LBB4_15\n\tWORD $0xd231             // xorl\t%edx, %edx\n\tJMP  LBB4_11\n\nLBB4_15:\n\tLONG $0xfffce181; WORD $0x7fff // andl\t$2147483644, %ecx               # imm = 0x7FFFFFFC\n\tWORD $0xd231                   // xorl\t%edx, %edx\n\nLBB4_16:\n\tLONG $0x0610fac5               // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0458fac5; BYTE $0x97   // vaddss\t(%rdi,%rdx,4), %xmm0, %xmm0\n\tLONG $0x0411fac5; BYTE $0x97   // vmovss\t%xmm0, (%rdi,%rdx,4)\n\tLONG $0x0610fac5               // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4458fac5; WORD $0x0497 // vaddss\t4(%rdi,%rdx,4), %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x0497 // vmovss\t%xmm0, 4(%rdi,%rdx,4)\n\tLONG $0x0610fac5               // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4458fac5; WORD $0x0897 // vaddss\t8(%rdi,%rdx,4), %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x0897 // vmovss\t%xmm0, 8(%rdi,%rdx,4)\n\tLONG $0x0610fac5               // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4458fac5; WORD $0x0c97 // vaddss\t12(%rdi,%rdx,4), %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x0c97 // vmovss\t%xmm0, 12(%rdi,%rdx,4)\n\tLONG $0x04c28348               // addq\t$4, %rdx\n\tWORD $0x3948; BYTE $0xd1       // cmpq\t%rdx, %rcx\n\tJNE  LBB4_16\n\nLBB4_11:\n\tWORD $0x8548; BYTE $0xc0 // testq\t%rax, %rax\n\tJE   LBB4_14\n\tLONG $0x970c8d48         // leaq\t(%rdi,%rdx,4), %rcx\n\tWORD $0xd231             // xorl\t%edx, %edx\n\nLBB4_13:\n\tLONG $0x0610fac5             // vmovss\t(%rsi), %xmm0                   # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0458fac5; BYTE $0x91 // vaddss\t(%rcx,%rdx,4), %xmm0, %xmm0\n\tLONG $0x0411fac5; BYTE $0x91 // vmovss\t%xmm0, (%rcx,%rdx,4)\n\tWORD $0xff48; BYTE $0xc2     // incq\t%rdx\n\tWORD $0x3948; BYTE $0xd0     // cmpq\t%rdx, %rax\n\tJNE  LBB4_13\n\nLBB4_14:\n\tWORD $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tRET\n\nTEXT ·_mm512_sub_to(SB), $0-32\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ c+16(FP), DX\n\tMOVQ n+24(FP), CX\n\tBYTE $0x55                                 // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5                   // movq\t%rsp, %rbp\n\tLONG $0xf8e48348                           // andq\t$-8, %rsp\n\tLONG $0x0f418d48                           // leaq\t15(%rcx), %rax\n\tWORD $0x8548; BYTE $0xc9                   // testq\t%rcx, %rcx\n\tLONG $0xc1490f48                           // cmovnsq\t%rcx, %rax\n\tWORD $0x8949; BYTE $0xc0                   // movq\t%rax, %r8\n\tLONG $0x04f8c149                           // sarq\t$4, %r8\n\tLONG $0xf0e08348                           // andq\t$-16, %rax\n\tWORD $0x2948; BYTE $0xc1                   // subq\t%rax, %rcx\n\tWORD $0x8545; BYTE $0xc0                   // testl\t%r8d, %r8d\n\tJLE  LBB5_6\n\tWORD $0x8944; BYTE $0xc0                   // movl\t%r8d, %eax\n\tWORD $0xe083; BYTE $0x03                   // andl\t$3, %eax\n\tLONG $0x04f88341                           // cmpl\t$4, %r8d\n\tJB   LBB5_4\n\tLONG $0xfce08141; WORD $0xffff; BYTE $0x7f // andl\t$2147483644, %r8d               # imm = 0x7FFFFFFC\n\nLBB5_3:\n\tLONG $0x487cf162; WORD $0x0710             // vmovups\t(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x065c             // vsubps\t(%rsi), %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x0211             // vmovups\t%zmm0, (%rdx)\n\tLONG $0x487cf162; WORD $0x4710; BYTE $0x01 // vmovups\t64(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x465c; BYTE $0x01 // vsubps\t64(%rsi), %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x4211; BYTE $0x01 // vmovups\t%zmm0, 64(%rdx)\n\tLONG $0x487cf162; WORD $0x4710; BYTE $0x02 // vmovups\t128(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x465c; BYTE $0x02 // vsubps\t128(%rsi), %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x4211; BYTE $0x02 // vmovups\t%zmm0, 128(%rdx)\n\tLONG $0x487cf162; WORD $0x4710; BYTE $0x03 // vmovups\t192(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x465c; BYTE $0x03 // vsubps\t192(%rsi), %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x4211; BYTE $0x03 // vmovups\t%zmm0, 192(%rdx)\n\tLONG $0x00c78148; WORD $0x0001; BYTE $0x00 // addq\t$256, %rdi                      # imm = 0x100\n\tLONG $0x00c68148; WORD $0x0001; BYTE $0x00 // addq\t$256, %rsi                      # imm = 0x100\n\tLONG $0x00c28148; WORD $0x0001; BYTE $0x00 // addq\t$256, %rdx                      # imm = 0x100\n\tLONG $0xfcc08341                           // addl\t$-4, %r8d\n\tJNE  LBB5_3\n\nLBB5_4:\n\tWORD $0xc085 // testl\t%eax, %eax\n\tJE   LBB5_6\n\nLBB5_5:\n\tLONG $0x487cf162; WORD $0x0710 // vmovups\t(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x065c // vsubps\t(%rsi), %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x0211 // vmovups\t%zmm0, (%rdx)\n\tLONG $0x40c78348               // addq\t$64, %rdi\n\tLONG $0x40c68348               // addq\t$64, %rsi\n\tLONG $0x40c28348               // addq\t$64, %rdx\n\tWORD $0xc8ff                   // decl\t%eax\n\tJNE  LBB5_5\n\nLBB5_6:\n\tLONG $0x07f98348         // cmpq\t$7, %rcx\n\tJLE  LBB5_8\n\tLONG $0x0710fcc5         // vmovups\t(%rdi), %ymm0\n\tLONG $0x065cfcc5         // vsubps\t(%rsi), %ymm0, %ymm0\n\tLONG $0x0211fcc5         // vmovups\t%ymm0, (%rdx)\n\tLONG $0x20c78348         // addq\t$32, %rdi\n\tLONG $0x20c68348         // addq\t$32, %rsi\n\tLONG $0x20c28348         // addq\t$32, %rdx\n\tWORD $0xc183; BYTE $0xf8 // addl\t$-8, %ecx\n\nLBB5_8:\n\tWORD $0xc985             // testl\t%ecx, %ecx\n\tJLE  LBB5_14\n\tWORD $0x8941; BYTE $0xc8 // movl\t%ecx, %r8d\n\tWORD $0x8944; BYTE $0xc0 // movl\t%r8d, %eax\n\tWORD $0xe083; BYTE $0x03 // andl\t$3, %eax\n\tWORD $0xf983; BYTE $0x04 // cmpl\t$4, %ecx\n\tJAE  LBB5_15\n\tWORD $0xc931             // xorl\t%ecx, %ecx\n\tJMP  LBB5_11\n\nLBB5_15:\n\tLONG $0xfce08141; WORD $0xffff; BYTE $0x7f // andl\t$2147483644, %r8d               # imm = 0x7FFFFFFC\n\tWORD $0xc931                               // xorl\t%ecx, %ecx\n\nLBB5_16:\n\tLONG $0x0410fac5; BYTE $0x8f   // vmovss\t(%rdi,%rcx,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x045cfac5; BYTE $0x8e   // vsubss\t(%rsi,%rcx,4), %xmm0, %xmm0\n\tLONG $0x0411fac5; BYTE $0x8a   // vmovss\t%xmm0, (%rdx,%rcx,4)\n\tLONG $0x4410fac5; WORD $0x048f // vmovss\t4(%rdi,%rcx,4), %xmm0           # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x445cfac5; WORD $0x048e // vsubss\t4(%rsi,%rcx,4), %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x048a // vmovss\t%xmm0, 4(%rdx,%rcx,4)\n\tLONG $0x4410fac5; WORD $0x088f // vmovss\t8(%rdi,%rcx,4), %xmm0           # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x445cfac5; WORD $0x088e // vsubss\t8(%rsi,%rcx,4), %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x088a // vmovss\t%xmm0, 8(%rdx,%rcx,4)\n\tLONG $0x4410fac5; WORD $0x0c8f // vmovss\t12(%rdi,%rcx,4), %xmm0          # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x445cfac5; WORD $0x0c8e // vsubss\t12(%rsi,%rcx,4), %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x0c8a // vmovss\t%xmm0, 12(%rdx,%rcx,4)\n\tLONG $0x04c18348               // addq\t$4, %rcx\n\tWORD $0x3949; BYTE $0xc8       // cmpq\t%rcx, %r8\n\tJNE  LBB5_16\n\nLBB5_11:\n\tWORD $0x8548; BYTE $0xc0 // testq\t%rax, %rax\n\tJE   LBB5_14\n\tLONG $0x8a148d48         // leaq\t(%rdx,%rcx,4), %rdx\n\tLONG $0x8e348d48         // leaq\t(%rsi,%rcx,4), %rsi\n\tLONG $0x8f0c8d48         // leaq\t(%rdi,%rcx,4), %rcx\n\tWORD $0xff31             // xorl\t%edi, %edi\n\nLBB5_13:\n\tLONG $0x0410fac5; BYTE $0xb9 // vmovss\t(%rcx,%rdi,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x045cfac5; BYTE $0xbe // vsubss\t(%rsi,%rdi,4), %xmm0, %xmm0\n\tLONG $0x0411fac5; BYTE $0xba // vmovss\t%xmm0, (%rdx,%rdi,4)\n\tWORD $0xff48; BYTE $0xc7     // incq\t%rdi\n\tWORD $0x3948; BYTE $0xf8     // cmpq\t%rdi, %rax\n\tJNE  LBB5_13\n\nLBB5_14:\n\tWORD $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tRET\n\nTEXT ·_mm512_sub(SB), $0-24\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ n+16(FP), DX\n\tBYTE $0x55                     // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5       // movq\t%rsp, %rbp\n\tLONG $0xf8e48348               // andq\t$-8, %rsp\n\tLONG $0x0f428d48               // leaq\t15(%rdx), %rax\n\tWORD $0x8548; BYTE $0xd2       // testq\t%rdx, %rdx\n\tLONG $0xc2490f48               // cmovnsq\t%rdx, %rax\n\tWORD $0x8948; BYTE $0xc1       // movq\t%rax, %rcx\n\tLONG $0x04f9c148               // sarq\t$4, %rcx\n\tLONG $0xf0e08348               // andq\t$-16, %rax\n\tWORD $0x2948; BYTE $0xc2       // subq\t%rax, %rdx\n\tWORD $0xc985                   // testl\t%ecx, %ecx\n\tJLE  LBB6_6\n\tWORD $0xc889                   // movl\t%ecx, %eax\n\tWORD $0xe083; BYTE $0x03       // andl\t$3, %eax\n\tWORD $0xf983; BYTE $0x04       // cmpl\t$4, %ecx\n\tJB   LBB6_4\n\tLONG $0xfffce181; WORD $0x7fff // andl\t$2147483644, %ecx               # imm = 0x7FFFFFFC\n\nLBB6_3:\n\tLONG $0x487cf162; WORD $0x0710             // vmovups\t(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x4f10; BYTE $0x01 // vmovups\t64(%rdi), %zmm1\n\tLONG $0x487cf162; WORD $0x5710; BYTE $0x02 // vmovups\t128(%rdi), %zmm2\n\tLONG $0x487cf162; WORD $0x065c             // vsubps\t(%rsi), %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x5f10; BYTE $0x03 // vmovups\t192(%rdi), %zmm3\n\tLONG $0x487cf162; WORD $0x0711             // vmovups\t%zmm0, (%rdi)\n\tLONG $0x4874f162; WORD $0x465c; BYTE $0x01 // vsubps\t64(%rsi), %zmm1, %zmm0\n\tLONG $0x487cf162; WORD $0x4711; BYTE $0x01 // vmovups\t%zmm0, 64(%rdi)\n\tLONG $0x486cf162; WORD $0x465c; BYTE $0x02 // vsubps\t128(%rsi), %zmm2, %zmm0\n\tLONG $0x487cf162; WORD $0x4711; BYTE $0x02 // vmovups\t%zmm0, 128(%rdi)\n\tLONG $0x4864f162; WORD $0x465c; BYTE $0x03 // vsubps\t192(%rsi), %zmm3, %zmm0\n\tLONG $0x487cf162; WORD $0x4711; BYTE $0x03 // vmovups\t%zmm0, 192(%rdi)\n\tLONG $0x00c78148; WORD $0x0001; BYTE $0x00 // addq\t$256, %rdi                      # imm = 0x100\n\tLONG $0x00c68148; WORD $0x0001; BYTE $0x00 // addq\t$256, %rsi                      # imm = 0x100\n\tWORD $0xc183; BYTE $0xfc                   // addl\t$-4, %ecx\n\tJNE  LBB6_3\n\nLBB6_4:\n\tWORD $0xc085 // testl\t%eax, %eax\n\tJE   LBB6_6\n\nLBB6_5:\n\tLONG $0x487cf162; WORD $0x0710 // vmovups\t(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x065c // vsubps\t(%rsi), %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x0711 // vmovups\t%zmm0, (%rdi)\n\tLONG $0x40c78348               // addq\t$64, %rdi\n\tLONG $0x40c68348               // addq\t$64, %rsi\n\tWORD $0xc8ff                   // decl\t%eax\n\tJNE  LBB6_5\n\nLBB6_6:\n\tWORD $0x8548; BYTE $0xd2 // testq\t%rdx, %rdx\n\tJLE  LBB6_12\n\tWORD $0xd089             // movl\t%edx, %eax\n\tLONG $0xff488d48         // leaq\t-1(%rax), %rcx\n\tWORD $0xe283; BYTE $0x03 // andl\t$3, %edx\n\tLONG $0x03f98348         // cmpq\t$3, %rcx\n\tJAE  LBB6_13\n\tWORD $0xc931             // xorl\t%ecx, %ecx\n\tJMP  LBB6_9\n\nLBB6_13:\n\tWORD $0x2948; BYTE $0xd0 // subq\t%rdx, %rax\n\tWORD $0xc931             // xorl\t%ecx, %ecx\n\nLBB6_14:\n\tLONG $0x0410fac5; BYTE $0x8f   // vmovss\t(%rdi,%rcx,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x045cfac5; BYTE $0x8e   // vsubss\t(%rsi,%rcx,4), %xmm0, %xmm0\n\tLONG $0x4c10fac5; WORD $0x048f // vmovss\t4(%rdi,%rcx,4), %xmm1           # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x0411fac5; BYTE $0x8f   // vmovss\t%xmm0, (%rdi,%rcx,4)\n\tLONG $0x445cf2c5; WORD $0x048e // vsubss\t4(%rsi,%rcx,4), %xmm1, %xmm0\n\tLONG $0x4411fac5; WORD $0x048f // vmovss\t%xmm0, 4(%rdi,%rcx,4)\n\tLONG $0x4410fac5; WORD $0x088f // vmovss\t8(%rdi,%rcx,4), %xmm0           # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x445cfac5; WORD $0x088e // vsubss\t8(%rsi,%rcx,4), %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x088f // vmovss\t%xmm0, 8(%rdi,%rcx,4)\n\tLONG $0x4410fac5; WORD $0x0c8f // vmovss\t12(%rdi,%rcx,4), %xmm0          # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x445cfac5; WORD $0x0c8e // vsubss\t12(%rsi,%rcx,4), %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x0c8f // vmovss\t%xmm0, 12(%rdi,%rcx,4)\n\tLONG $0x04c18348               // addq\t$4, %rcx\n\tWORD $0x3948; BYTE $0xc8       // cmpq\t%rcx, %rax\n\tJNE  LBB6_14\n\nLBB6_9:\n\tWORD $0x8548; BYTE $0xd2 // testq\t%rdx, %rdx\n\tJE   LBB6_12\n\tLONG $0x8f048d48         // leaq\t(%rdi,%rcx,4), %rax\n\tLONG $0x8e0c8d48         // leaq\t(%rsi,%rcx,4), %rcx\n\tWORD $0xf631             // xorl\t%esi, %esi\n\nLBB6_11:\n\tLONG $0x0410fac5; BYTE $0xb0 // vmovss\t(%rax,%rsi,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x045cfac5; BYTE $0xb1 // vsubss\t(%rcx,%rsi,4), %xmm0, %xmm0\n\tLONG $0x0411fac5; BYTE $0xb0 // vmovss\t%xmm0, (%rax,%rsi,4)\n\tWORD $0xff48; BYTE $0xc6     // incq\t%rsi\n\tWORD $0x3948; BYTE $0xf2     // cmpq\t%rsi, %rdx\n\tJNE  LBB6_11\n\nLBB6_12:\n\tWORD $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tRET\n\nTEXT ·_mm512_mul_to(SB), $0-32\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ c+16(FP), DX\n\tMOVQ n+24(FP), CX\n\tBYTE $0x55                                 // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5                   // movq\t%rsp, %rbp\n\tLONG $0xf8e48348                           // andq\t$-8, %rsp\n\tLONG $0x0f418d48                           // leaq\t15(%rcx), %rax\n\tWORD $0x8548; BYTE $0xc9                   // testq\t%rcx, %rcx\n\tLONG $0xc1490f48                           // cmovnsq\t%rcx, %rax\n\tWORD $0x8949; BYTE $0xc0                   // movq\t%rax, %r8\n\tLONG $0x04f8c149                           // sarq\t$4, %r8\n\tLONG $0xf0e08348                           // andq\t$-16, %rax\n\tWORD $0x2948; BYTE $0xc1                   // subq\t%rax, %rcx\n\tWORD $0x8545; BYTE $0xc0                   // testl\t%r8d, %r8d\n\tJLE  LBB7_6\n\tWORD $0x8944; BYTE $0xc0                   // movl\t%r8d, %eax\n\tWORD $0xe083; BYTE $0x03                   // andl\t$3, %eax\n\tLONG $0x04f88341                           // cmpl\t$4, %r8d\n\tJB   LBB7_4\n\tLONG $0xfce08141; WORD $0xffff; BYTE $0x7f // andl\t$2147483644, %r8d               # imm = 0x7FFFFFFC\n\nLBB7_3:\n\tLONG $0x487cf162; WORD $0x0710             // vmovups\t(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x0659             // vmulps\t(%rsi), %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x0211             // vmovups\t%zmm0, (%rdx)\n\tLONG $0x487cf162; WORD $0x4710; BYTE $0x01 // vmovups\t64(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x4659; BYTE $0x01 // vmulps\t64(%rsi), %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x4211; BYTE $0x01 // vmovups\t%zmm0, 64(%rdx)\n\tLONG $0x487cf162; WORD $0x4710; BYTE $0x02 // vmovups\t128(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x4659; BYTE $0x02 // vmulps\t128(%rsi), %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x4211; BYTE $0x02 // vmovups\t%zmm0, 128(%rdx)\n\tLONG $0x487cf162; WORD $0x4710; BYTE $0x03 // vmovups\t192(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x4659; BYTE $0x03 // vmulps\t192(%rsi), %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x4211; BYTE $0x03 // vmovups\t%zmm0, 192(%rdx)\n\tLONG $0x00c78148; WORD $0x0001; BYTE $0x00 // addq\t$256, %rdi                      # imm = 0x100\n\tLONG $0x00c68148; WORD $0x0001; BYTE $0x00 // addq\t$256, %rsi                      # imm = 0x100\n\tLONG $0x00c28148; WORD $0x0001; BYTE $0x00 // addq\t$256, %rdx                      # imm = 0x100\n\tLONG $0xfcc08341                           // addl\t$-4, %r8d\n\tJNE  LBB7_3\n\nLBB7_4:\n\tWORD $0xc085 // testl\t%eax, %eax\n\tJE   LBB7_6\n\nLBB7_5:\n\tLONG $0x487cf162; WORD $0x0710 // vmovups\t(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x0659 // vmulps\t(%rsi), %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x0211 // vmovups\t%zmm0, (%rdx)\n\tLONG $0x40c78348               // addq\t$64, %rdi\n\tLONG $0x40c68348               // addq\t$64, %rsi\n\tLONG $0x40c28348               // addq\t$64, %rdx\n\tWORD $0xc8ff                   // decl\t%eax\n\tJNE  LBB7_5\n\nLBB7_6:\n\tLONG $0x07f98348         // cmpq\t$7, %rcx\n\tJLE  LBB7_8\n\tLONG $0x0710fcc5         // vmovups\t(%rdi), %ymm0\n\tLONG $0x0659fcc5         // vmulps\t(%rsi), %ymm0, %ymm0\n\tLONG $0x0211fcc5         // vmovups\t%ymm0, (%rdx)\n\tLONG $0x20c78348         // addq\t$32, %rdi\n\tLONG $0x20c68348         // addq\t$32, %rsi\n\tLONG $0x20c28348         // addq\t$32, %rdx\n\tWORD $0xc183; BYTE $0xf8 // addl\t$-8, %ecx\n\nLBB7_8:\n\tWORD $0xc985             // testl\t%ecx, %ecx\n\tJLE  LBB7_14\n\tWORD $0x8941; BYTE $0xc8 // movl\t%ecx, %r8d\n\tWORD $0x8944; BYTE $0xc0 // movl\t%r8d, %eax\n\tWORD $0xe083; BYTE $0x03 // andl\t$3, %eax\n\tWORD $0xf983; BYTE $0x04 // cmpl\t$4, %ecx\n\tJAE  LBB7_15\n\tWORD $0xc931             // xorl\t%ecx, %ecx\n\tJMP  LBB7_11\n\nLBB7_15:\n\tLONG $0xfce08141; WORD $0xffff; BYTE $0x7f // andl\t$2147483644, %r8d               # imm = 0x7FFFFFFC\n\tWORD $0xc931                               // xorl\t%ecx, %ecx\n\nLBB7_16:\n\tLONG $0x0410fac5; BYTE $0x8f   // vmovss\t(%rdi,%rcx,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0459fac5; BYTE $0x8e   // vmulss\t(%rsi,%rcx,4), %xmm0, %xmm0\n\tLONG $0x0411fac5; BYTE $0x8a   // vmovss\t%xmm0, (%rdx,%rcx,4)\n\tLONG $0x4410fac5; WORD $0x048f // vmovss\t4(%rdi,%rcx,4), %xmm0           # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4459fac5; WORD $0x048e // vmulss\t4(%rsi,%rcx,4), %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x048a // vmovss\t%xmm0, 4(%rdx,%rcx,4)\n\tLONG $0x4410fac5; WORD $0x088f // vmovss\t8(%rdi,%rcx,4), %xmm0           # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4459fac5; WORD $0x088e // vmulss\t8(%rsi,%rcx,4), %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x088a // vmovss\t%xmm0, 8(%rdx,%rcx,4)\n\tLONG $0x4410fac5; WORD $0x0c8f // vmovss\t12(%rdi,%rcx,4), %xmm0          # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x4459fac5; WORD $0x0c8e // vmulss\t12(%rsi,%rcx,4), %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x0c8a // vmovss\t%xmm0, 12(%rdx,%rcx,4)\n\tLONG $0x04c18348               // addq\t$4, %rcx\n\tWORD $0x3949; BYTE $0xc8       // cmpq\t%rcx, %r8\n\tJNE  LBB7_16\n\nLBB7_11:\n\tWORD $0x8548; BYTE $0xc0 // testq\t%rax, %rax\n\tJE   LBB7_14\n\tLONG $0x8a148d48         // leaq\t(%rdx,%rcx,4), %rdx\n\tLONG $0x8e348d48         // leaq\t(%rsi,%rcx,4), %rsi\n\tLONG $0x8f0c8d48         // leaq\t(%rdi,%rcx,4), %rcx\n\tWORD $0xff31             // xorl\t%edi, %edi\n\nLBB7_13:\n\tLONG $0x0410fac5; BYTE $0xb9 // vmovss\t(%rcx,%rdi,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x0459fac5; BYTE $0xbe // vmulss\t(%rsi,%rdi,4), %xmm0, %xmm0\n\tLONG $0x0411fac5; BYTE $0xba // vmovss\t%xmm0, (%rdx,%rdi,4)\n\tWORD $0xff48; BYTE $0xc7     // incq\t%rdi\n\tWORD $0x3948; BYTE $0xf8     // cmpq\t%rdi, %rax\n\tJNE  LBB7_13\n\nLBB7_14:\n\tWORD $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tRET\n\nTEXT ·_mm512_div_to(SB), $0-32\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ c+16(FP), DX\n\tMOVQ n+24(FP), CX\n\tBYTE $0x55                                 // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5                   // movq\t%rsp, %rbp\n\tLONG $0xf8e48348                           // andq\t$-8, %rsp\n\tLONG $0x0f418d48                           // leaq\t15(%rcx), %rax\n\tWORD $0x8548; BYTE $0xc9                   // testq\t%rcx, %rcx\n\tLONG $0xc1490f48                           // cmovnsq\t%rcx, %rax\n\tWORD $0x8949; BYTE $0xc0                   // movq\t%rax, %r8\n\tLONG $0x04f8c149                           // sarq\t$4, %r8\n\tLONG $0xf0e08348                           // andq\t$-16, %rax\n\tWORD $0x2948; BYTE $0xc1                   // subq\t%rax, %rcx\n\tWORD $0x8545; BYTE $0xc0                   // testl\t%r8d, %r8d\n\tJLE  LBB8_6\n\tWORD $0x8944; BYTE $0xc0                   // movl\t%r8d, %eax\n\tWORD $0xe083; BYTE $0x03                   // andl\t$3, %eax\n\tLONG $0x04f88341                           // cmpl\t$4, %r8d\n\tJB   LBB8_4\n\tLONG $0xfce08141; WORD $0xffff; BYTE $0x7f // andl\t$2147483644, %r8d               # imm = 0x7FFFFFFC\n\nLBB8_3:\n\tLONG $0x487cf162; WORD $0x0710             // vmovups\t(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x065e             // vdivps\t(%rsi), %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x0211             // vmovups\t%zmm0, (%rdx)\n\tLONG $0x487cf162; WORD $0x4710; BYTE $0x01 // vmovups\t64(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x465e; BYTE $0x01 // vdivps\t64(%rsi), %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x4211; BYTE $0x01 // vmovups\t%zmm0, 64(%rdx)\n\tLONG $0x487cf162; WORD $0x4710; BYTE $0x02 // vmovups\t128(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x465e; BYTE $0x02 // vdivps\t128(%rsi), %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x4211; BYTE $0x02 // vmovups\t%zmm0, 128(%rdx)\n\tLONG $0x487cf162; WORD $0x4710; BYTE $0x03 // vmovups\t192(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x465e; BYTE $0x03 // vdivps\t192(%rsi), %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x4211; BYTE $0x03 // vmovups\t%zmm0, 192(%rdx)\n\tLONG $0x00c78148; WORD $0x0001; BYTE $0x00 // addq\t$256, %rdi                      # imm = 0x100\n\tLONG $0x00c68148; WORD $0x0001; BYTE $0x00 // addq\t$256, %rsi                      # imm = 0x100\n\tLONG $0x00c28148; WORD $0x0001; BYTE $0x00 // addq\t$256, %rdx                      # imm = 0x100\n\tLONG $0xfcc08341                           // addl\t$-4, %r8d\n\tJNE  LBB8_3\n\nLBB8_4:\n\tWORD $0xc085 // testl\t%eax, %eax\n\tJE   LBB8_6\n\nLBB8_5:\n\tLONG $0x487cf162; WORD $0x0710 // vmovups\t(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x065e // vdivps\t(%rsi), %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0x0211 // vmovups\t%zmm0, (%rdx)\n\tLONG $0x40c78348               // addq\t$64, %rdi\n\tLONG $0x40c68348               // addq\t$64, %rsi\n\tLONG $0x40c28348               // addq\t$64, %rdx\n\tWORD $0xc8ff                   // decl\t%eax\n\tJNE  LBB8_5\n\nLBB8_6:\n\tLONG $0x07f98348         // cmpq\t$7, %rcx\n\tJLE  LBB8_8\n\tLONG $0x0710fcc5         // vmovups\t(%rdi), %ymm0\n\tLONG $0x065efcc5         // vdivps\t(%rsi), %ymm0, %ymm0\n\tLONG $0x0211fcc5         // vmovups\t%ymm0, (%rdx)\n\tLONG $0x20c78348         // addq\t$32, %rdi\n\tLONG $0x20c68348         // addq\t$32, %rsi\n\tLONG $0x20c28348         // addq\t$32, %rdx\n\tWORD $0xc183; BYTE $0xf8 // addl\t$-8, %ecx\n\nLBB8_8:\n\tWORD $0xc985             // testl\t%ecx, %ecx\n\tJLE  LBB8_14\n\tWORD $0x8941; BYTE $0xc8 // movl\t%ecx, %r8d\n\tWORD $0x8944; BYTE $0xc0 // movl\t%r8d, %eax\n\tWORD $0xe083; BYTE $0x03 // andl\t$3, %eax\n\tWORD $0xf983; BYTE $0x04 // cmpl\t$4, %ecx\n\tJAE  LBB8_15\n\tWORD $0xc931             // xorl\t%ecx, %ecx\n\tJMP  LBB8_11\n\nLBB8_15:\n\tLONG $0xfce08141; WORD $0xffff; BYTE $0x7f // andl\t$2147483644, %r8d               # imm = 0x7FFFFFFC\n\tWORD $0xc931                               // xorl\t%ecx, %ecx\n\nLBB8_16:\n\tLONG $0x0410fac5; BYTE $0x8f   // vmovss\t(%rdi,%rcx,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x045efac5; BYTE $0x8e   // vdivss\t(%rsi,%rcx,4), %xmm0, %xmm0\n\tLONG $0x0411fac5; BYTE $0x8a   // vmovss\t%xmm0, (%rdx,%rcx,4)\n\tLONG $0x4410fac5; WORD $0x048f // vmovss\t4(%rdi,%rcx,4), %xmm0           # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x445efac5; WORD $0x048e // vdivss\t4(%rsi,%rcx,4), %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x048a // vmovss\t%xmm0, 4(%rdx,%rcx,4)\n\tLONG $0x4410fac5; WORD $0x088f // vmovss\t8(%rdi,%rcx,4), %xmm0           # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x445efac5; WORD $0x088e // vdivss\t8(%rsi,%rcx,4), %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x088a // vmovss\t%xmm0, 8(%rdx,%rcx,4)\n\tLONG $0x4410fac5; WORD $0x0c8f // vmovss\t12(%rdi,%rcx,4), %xmm0          # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x445efac5; WORD $0x0c8e // vdivss\t12(%rsi,%rcx,4), %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x0c8a // vmovss\t%xmm0, 12(%rdx,%rcx,4)\n\tLONG $0x04c18348               // addq\t$4, %rcx\n\tWORD $0x3949; BYTE $0xc8       // cmpq\t%rcx, %r8\n\tJNE  LBB8_16\n\nLBB8_11:\n\tWORD $0x8548; BYTE $0xc0 // testq\t%rax, %rax\n\tJE   LBB8_14\n\tLONG $0x8a148d48         // leaq\t(%rdx,%rcx,4), %rdx\n\tLONG $0x8e348d48         // leaq\t(%rsi,%rcx,4), %rsi\n\tLONG $0x8f0c8d48         // leaq\t(%rdi,%rcx,4), %rcx\n\tWORD $0xff31             // xorl\t%edi, %edi\n\nLBB8_13:\n\tLONG $0x0410fac5; BYTE $0xb9 // vmovss\t(%rcx,%rdi,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x045efac5; BYTE $0xbe // vdivss\t(%rsi,%rdi,4), %xmm0, %xmm0\n\tLONG $0x0411fac5; BYTE $0xba // vmovss\t%xmm0, (%rdx,%rdi,4)\n\tWORD $0xff48; BYTE $0xc7     // incq\t%rdi\n\tWORD $0x3948; BYTE $0xf8     // cmpq\t%rdi, %rax\n\tJNE  LBB8_13\n\nLBB8_14:\n\tWORD $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tRET\n\nTEXT ·_mm512_sqrt_to(SB), $0-24\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ n+16(FP), DX\n\tBYTE $0x55                     // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5       // movq\t%rsp, %rbp\n\tLONG $0xf8e48348               // andq\t$-8, %rsp\n\tLONG $0x0f428d48               // leaq\t15(%rdx), %rax\n\tWORD $0x8548; BYTE $0xd2       // testq\t%rdx, %rdx\n\tLONG $0xc2490f48               // cmovnsq\t%rdx, %rax\n\tWORD $0x8948; BYTE $0xc1       // movq\t%rax, %rcx\n\tLONG $0x04f9c148               // sarq\t$4, %rcx\n\tLONG $0xf0e08348               // andq\t$-16, %rax\n\tWORD $0x2948; BYTE $0xc2       // subq\t%rax, %rdx\n\tWORD $0xc985                   // testl\t%ecx, %ecx\n\tJLE  LBB9_6\n\tWORD $0xc889                   // movl\t%ecx, %eax\n\tWORD $0xe083; BYTE $0x03       // andl\t$3, %eax\n\tWORD $0xf983; BYTE $0x04       // cmpl\t$4, %ecx\n\tJB   LBB9_4\n\tLONG $0xfffce181; WORD $0x7fff // andl\t$2147483644, %ecx               # imm = 0x7FFFFFFC\n\nLBB9_3:\n\tLONG $0x487cf162; WORD $0x0751             // vsqrtps\t(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x0611             // vmovups\t%zmm0, (%rsi)\n\tLONG $0x487cf162; WORD $0x4751; BYTE $0x01 // vsqrtps\t64(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x4611; BYTE $0x01 // vmovups\t%zmm0, 64(%rsi)\n\tLONG $0x487cf162; WORD $0x4751; BYTE $0x02 // vsqrtps\t128(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x4611; BYTE $0x02 // vmovups\t%zmm0, 128(%rsi)\n\tLONG $0x487cf162; WORD $0x4751; BYTE $0x03 // vsqrtps\t192(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x4611; BYTE $0x03 // vmovups\t%zmm0, 192(%rsi)\n\tLONG $0x00c78148; WORD $0x0001; BYTE $0x00 // addq\t$256, %rdi                      # imm = 0x100\n\tLONG $0x00c68148; WORD $0x0001; BYTE $0x00 // addq\t$256, %rsi                      # imm = 0x100\n\tWORD $0xc183; BYTE $0xfc                   // addl\t$-4, %ecx\n\tJNE  LBB9_3\n\nLBB9_4:\n\tWORD $0xc085 // testl\t%eax, %eax\n\tJE   LBB9_6\n\nLBB9_5:\n\tLONG $0x487cf162; WORD $0x0751 // vsqrtps\t(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x0611 // vmovups\t%zmm0, (%rsi)\n\tLONG $0x40c78348               // addq\t$64, %rdi\n\tLONG $0x40c68348               // addq\t$64, %rsi\n\tWORD $0xc8ff                   // decl\t%eax\n\tJNE  LBB9_5\n\nLBB9_6:\n\tLONG $0x07fa8348         // cmpq\t$7, %rdx\n\tJLE  LBB9_8\n\tLONG $0x0751fcc5         // vsqrtps\t(%rdi), %ymm0\n\tLONG $0x0611fcc5         // vmovups\t%ymm0, (%rsi)\n\tLONG $0x20c78348         // addq\t$32, %rdi\n\tLONG $0x20c68348         // addq\t$32, %rsi\n\tWORD $0xc283; BYTE $0xf8 // addl\t$-8, %edx\n\nLBB9_8:\n\tWORD $0xd285             // testl\t%edx, %edx\n\tJLE  LBB9_14\n\tWORD $0xd189             // movl\t%edx, %ecx\n\tWORD $0xc889             // movl\t%ecx, %eax\n\tWORD $0xe083; BYTE $0x03 // andl\t$3, %eax\n\tWORD $0xfa83; BYTE $0x04 // cmpl\t$4, %edx\n\tJAE  LBB9_15\n\tWORD $0xd231             // xorl\t%edx, %edx\n\tJMP  LBB9_11\n\nLBB9_15:\n\tLONG $0xfffce181; WORD $0x7fff // andl\t$2147483644, %ecx               # imm = 0x7FFFFFFC\n\tWORD $0xd231                   // xorl\t%edx, %edx\n\nLBB9_16:\n\tLONG $0x0410fac5; BYTE $0x97   // vmovss\t(%rdi,%rdx,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0xc051fac5               // vsqrtss\t%xmm0, %xmm0, %xmm0\n\tLONG $0x0411fac5; BYTE $0x96   // vmovss\t%xmm0, (%rsi,%rdx,4)\n\tLONG $0x4410fac5; WORD $0x0497 // vmovss\t4(%rdi,%rdx,4), %xmm0           # xmm0 = mem[0],zero,zero,zero\n\tLONG $0xc051fac5               // vsqrtss\t%xmm0, %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x0496 // vmovss\t%xmm0, 4(%rsi,%rdx,4)\n\tLONG $0x4410fac5; WORD $0x0897 // vmovss\t8(%rdi,%rdx,4), %xmm0           # xmm0 = mem[0],zero,zero,zero\n\tLONG $0xc051fac5               // vsqrtss\t%xmm0, %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x0896 // vmovss\t%xmm0, 8(%rsi,%rdx,4)\n\tLONG $0x4410fac5; WORD $0x0c97 // vmovss\t12(%rdi,%rdx,4), %xmm0          # xmm0 = mem[0],zero,zero,zero\n\tLONG $0xc051fac5               // vsqrtss\t%xmm0, %xmm0, %xmm0\n\tLONG $0x4411fac5; WORD $0x0c96 // vmovss\t%xmm0, 12(%rsi,%rdx,4)\n\tLONG $0x04c28348               // addq\t$4, %rdx\n\tWORD $0x3948; BYTE $0xd1       // cmpq\t%rdx, %rcx\n\tJNE  LBB9_16\n\nLBB9_11:\n\tWORD $0x8548; BYTE $0xc0 // testq\t%rax, %rax\n\tJE   LBB9_14\n\tLONG $0x960c8d48         // leaq\t(%rsi,%rdx,4), %rcx\n\tLONG $0x97148d48         // leaq\t(%rdi,%rdx,4), %rdx\n\tWORD $0xf631             // xorl\t%esi, %esi\n\nLBB9_13:\n\tLONG $0x0410fac5; BYTE $0xb2 // vmovss\t(%rdx,%rsi,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0xc051fac5             // vsqrtss\t%xmm0, %xmm0, %xmm0\n\tLONG $0x0411fac5; BYTE $0xb1 // vmovss\t%xmm0, (%rcx,%rsi,4)\n\tWORD $0xff48; BYTE $0xc6     // incq\t%rsi\n\tWORD $0x3948; BYTE $0xf0     // cmpq\t%rsi, %rax\n\tJNE  LBB9_13\n\nLBB9_14:\n\tWORD $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tRET\n\nTEXT ·_mm512_dot(SB), $8-32\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ n+16(FP), DX\n\tBYTE $0x55                             // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5               // movq\t%rsp, %rbp\n\tLONG $0xf8e48348                       // andq\t$-8, %rsp\n\tLONG $0x0f428d48                       // leaq\t15(%rdx), %rax\n\tWORD $0x8548; BYTE $0xd2               // testq\t%rdx, %rdx\n\tLONG $0xc2490f48                       // cmovnsq\t%rdx, %rax\n\tWORD $0x8948; BYTE $0xc1               // movq\t%rax, %rcx\n\tLONG $0x04f9c148                       // sarq\t$4, %rcx\n\tLONG $0xf0e08348                       // andq\t$-16, %rax\n\tWORD $0x2948; BYTE $0xc2               // subq\t%rax, %rdx\n\tWORD $0xc985                           // testl\t%ecx, %ecx\n\tJLE  LBB10_1\n\tLONG $0x487cf162; WORD $0x0710         // vmovups\t(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x0e59         // vmulps\t(%rsi), %zmm0, %zmm1\n\tLONG $0x40c78348                       // addq\t$64, %rdi\n\tLONG $0x40c68348                       // addq\t$64, %rsi\n\tWORD $0xf983; BYTE $0x01               // cmpl\t$1, %ecx\n\tJE   LBB10_9\n\tWORD $0x8949; BYTE $0xc8               // movq\t%rcx, %r8\n\tLONG $0x06e0c149                       // shlq\t$6, %r8\n\tQUAD $0x003fffffff80b848; WORD $0x0000 // movabsq\t$274877906816, %rax             # imm = 0x3FFFFFFF80\n\tWORD $0x0149; BYTE $0xc0               // addq\t%rax, %r8\n\tLONG $0x40c88348                       // orq\t$64, %rax\n\tWORD $0x214c; BYTE $0xc0               // andq\t%r8, %rax\n\tLONG $0xff518d44                       // leal\t-1(%rcx), %r10d\n\tLONG $0xfe418d44                       // leal\t-2(%rcx), %r8d\n\tLONG $0x03f88341                       // cmpl\t$3, %r8d\n\tJAE  LBB10_14\n\tWORD $0x8949; BYTE $0xf8               // movq\t%rdi, %r8\n\tWORD $0x8949; BYTE $0xf1               // movq\t%rsi, %r9\n\tLONG $0x487cf162; WORD $0xc128         // vmovaps\t%zmm1, %zmm0\n\tJMP  LBB10_5\n\nLBB10_1:\n\tLONG $0xc957f0c5 // vxorps\t%xmm1, %xmm1, %xmm1\n\tJMP  LBB10_9\n\nLBB10_14:\n\tWORD $0x8945; BYTE $0xd3 // movl\t%r10d, %r11d\n\tLONG $0xfce38341         // andl\t$-4, %r11d\n\tWORD $0x8949; BYTE $0xf8 // movq\t%rdi, %r8\n\tWORD $0x8949; BYTE $0xf1 // movq\t%rsi, %r9\n\nLBB10_15:\n\tLONG $0x487cd162; WORD $0x0010             // vmovups\t(%r8), %zmm0\n\tLONG $0x487cd162; WORD $0x5010; BYTE $0x01 // vmovups\t64(%r8), %zmm2\n\tLONG $0x487cd162; WORD $0x5810; BYTE $0x02 // vmovups\t128(%r8), %zmm3\n\tLONG $0x487cd162; WORD $0x6010; BYTE $0x03 // vmovups\t192(%r8), %zmm4\n\tLONG $0x4875d262; WORD $0x0198             // vfmadd132ps\t(%r9), %zmm1, %zmm0     # zmm0 = (zmm0 * mem) + zmm1\n\tLONG $0x486dd262; WORD $0x41b8; BYTE $0x01 // vfmadd231ps\t64(%r9), %zmm2, %zmm0   # zmm0 = (zmm2 * mem) + zmm0\n\tLONG $0x4865d262; WORD $0x41b8; BYTE $0x02 // vfmadd231ps\t128(%r9), %zmm3, %zmm0  # zmm0 = (zmm3 * mem) + zmm0\n\tLONG $0x485dd262; WORD $0x41b8; BYTE $0x03 // vfmadd231ps\t192(%r9), %zmm4, %zmm0  # zmm0 = (zmm4 * mem) + zmm0\n\tLONG $0x00c08149; WORD $0x0001; BYTE $0x00 // addq\t$256, %r8                       # imm = 0x100\n\tLONG $0x00c18149; WORD $0x0001; BYTE $0x00 // addq\t$256, %r9                       # imm = 0x100\n\tLONG $0x487cf162; WORD $0xc828             // vmovaps\t%zmm0, %zmm1\n\tLONG $0xfcc38341                           // addl\t$-4, %r11d\n\tJNE  LBB10_15\n\nLBB10_5:\n\tLONG $0x40588d4c         // leaq\t64(%rax), %r11\n\tLONG $0x03c2f641         // testb\t$3, %r10b\n\tJE   LBB10_8\n\tWORD $0xc9fe             // decb\t%cl\n\tWORD $0xb60f; BYTE $0xc9 // movzbl\t%cl, %ecx\n\tWORD $0xe183; BYTE $0x03 // andl\t$3, %ecx\n\tWORD $0xe1c1; BYTE $0x06 // shll\t$6, %ecx\n\tWORD $0x3145; BYTE $0xd2 // xorl\t%r10d, %r10d\n\nLBB10_7:\n\tLONG $0x487c9162; WORD $0x0c10; BYTE $0x10 // vmovups\t(%r8,%r10), %zmm1\n\tLONG $0x48759262; WORD $0x04b8; BYTE $0x11 // vfmadd231ps\t(%r9,%r10), %zmm1, %zmm0 # zmm0 = (zmm1 * mem) + zmm0\n\tLONG $0x40c28349                           // addq\t$64, %r10\n\tWORD $0x3944; BYTE $0xd1                   // cmpl\t%r10d, %ecx\n\tJNE  LBB10_7\n\nLBB10_8:\n\tWORD $0x0148; BYTE $0xc7       // addq\t%rax, %rdi\n\tLONG $0x40c78348               // addq\t$64, %rdi\n\tWORD $0x014c; BYTE $0xde       // addq\t%r11, %rsi\n\tLONG $0x487cf162; WORD $0xc828 // vmovaps\t%zmm0, %zmm1\n\nLBB10_9:\n\tLONG $0x48fdf362; WORD $0xc81b; BYTE $0x01 // vextractf64x4\t$1, %zmm1, %ymm0\n\tLONG $0xc058f4c5                           // vaddps\t%ymm0, %ymm1, %ymm0\n\tLONG $0x197de3c4; WORD $0x01c1             // vextractf128\t$1, %ymm0, %xmm1\n\tLONG $0xc158f8c5                           // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0xc8c6f9c5; BYTE $0x01               // vshufpd\t$1, %xmm0, %xmm0, %xmm1         # xmm1 = xmm0[1,0]\n\tLONG $0xc158f8c5                           // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0xc816fac5                           // vmovshdup\t%xmm0, %xmm1            # xmm1 = xmm0[1,1,3,3]\n\tLONG $0xc158f8c5                           // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0x07fa8348                           // cmpq\t$7, %rdx\n\tJLE  LBB10_11\n\tLONG $0x0f10fcc5                           // vmovups\t(%rdi), %ymm1\n\tLONG $0x0e59f4c5                           // vmulps\t(%rsi), %ymm1, %ymm1\n\tLONG $0x20c78348                           // addq\t$32, %rdi\n\tLONG $0x20c68348                           // addq\t$32, %rsi\n\tLONG $0x197de3c4; WORD $0x01ca             // vextractf128\t$1, %ymm1, %xmm2\n\tLONG $0xc958e8c5                           // vaddps\t%xmm1, %xmm2, %xmm1\n\tLONG $0xd1c6f1c5; BYTE $0x01               // vshufpd\t$1, %xmm1, %xmm1, %xmm2         # xmm2 = xmm1[1,0]\n\tLONG $0xca58f0c5                           // vaddps\t%xmm2, %xmm1, %xmm1\n\tLONG $0xd116fac5                           // vmovshdup\t%xmm1, %xmm2            # xmm2 = xmm1[1,1,3,3]\n\tLONG $0xca58f2c5                           // vaddss\t%xmm2, %xmm1, %xmm1\n\tLONG $0xc158fac5                           // vaddss\t%xmm1, %xmm0, %xmm0\n\tWORD $0xc283; BYTE $0xf8                   // addl\t$-8, %edx\n\nLBB10_11:\n\tWORD $0xd285             // testl\t%edx, %edx\n\tJLE  LBB10_21\n\tWORD $0x8941; BYTE $0xd0 // movl\t%edx, %r8d\n\tWORD $0x8944; BYTE $0xc0 // movl\t%r8d, %eax\n\tWORD $0xe083; BYTE $0x03 // andl\t$3, %eax\n\tWORD $0xfa83; BYTE $0x04 // cmpl\t$4, %edx\n\tJAE  LBB10_16\n\tWORD $0xc931             // xorl\t%ecx, %ecx\n\tJMP  LBB10_18\n\nLBB10_16:\n\tLONG $0xfce08141; WORD $0xffff; BYTE $0x7f // andl\t$2147483644, %r8d               # imm = 0x7FFFFFFC\n\tWORD $0xc931                               // xorl\t%ecx, %ecx\n\nLBB10_17:\n\tLONG $0x0c10fac5; BYTE $0x8f               // vmovss\t(%rdi,%rcx,4), %xmm1            # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x5410fac5; WORD $0x048f             // vmovss\t4(%rdi,%rcx,4), %xmm2           # xmm2 = mem[0],zero,zero,zero\n\tLONG $0x9979e2c4; WORD $0x8e0c             // vfmadd132ss\t(%rsi,%rcx,4), %xmm0, %xmm1 # xmm1 = (xmm1 * mem) + xmm0\n\tLONG $0xb969e2c4; WORD $0x8e4c; BYTE $0x04 // vfmadd231ss\t4(%rsi,%rcx,4), %xmm2, %xmm1 # xmm1 = (xmm2 * mem) + xmm1\n\tLONG $0x5410fac5; WORD $0x088f             // vmovss\t8(%rdi,%rcx,4), %xmm2           # xmm2 = mem[0],zero,zero,zero\n\tLONG $0x9971e2c4; WORD $0x8e54; BYTE $0x08 // vfmadd132ss\t8(%rsi,%rcx,4), %xmm1, %xmm2 # xmm2 = (xmm2 * mem) + xmm1\n\tLONG $0x4410fac5; WORD $0x0c8f             // vmovss\t12(%rdi,%rcx,4), %xmm0          # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x9969e2c4; WORD $0x8e44; BYTE $0x0c // vfmadd132ss\t12(%rsi,%rcx,4), %xmm2, %xmm0 # xmm0 = (xmm0 * mem) + xmm2\n\tLONG $0x04c18348                           // addq\t$4, %rcx\n\tWORD $0x3949; BYTE $0xc8                   // cmpq\t%rcx, %r8\n\tJNE  LBB10_17\n\nLBB10_18:\n\tWORD $0x8548; BYTE $0xc0 // testq\t%rax, %rax\n\tJE   LBB10_21\n\tLONG $0x8e148d48         // leaq\t(%rsi,%rcx,4), %rdx\n\tLONG $0x8f0c8d48         // leaq\t(%rdi,%rcx,4), %rcx\n\tWORD $0xf631             // xorl\t%esi, %esi\n\nLBB10_20:\n\tLONG $0x0c10fac5; BYTE $0xb1   // vmovss\t(%rcx,%rsi,4), %xmm1            # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971e2c4; WORD $0xb204 // vfmadd231ss\t(%rdx,%rsi,4), %xmm1, %xmm0 # xmm0 = (xmm1 * mem) + xmm0\n\tWORD $0xff48; BYTE $0xc6       // incq\t%rsi\n\tWORD $0x3948; BYTE $0xf0       // cmpq\t%rsi, %rax\n\tJNE  LBB10_20\n\nLBB10_21:\n\tWORD  $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE  $0x5d               // popq\t%rbp\n\tWORD  $0xf8c5; BYTE $0x77 // vzeroupper\n\tMOVSS X0, result+24(FP)\n\tRET\n\nTEXT ·_mm512_euclidean(SB), $8-32\n\tMOVQ a+0(FP), DI\n\tMOVQ b+8(FP), SI\n\tMOVQ n+16(FP), DX\n\tBYTE $0x55                             // pushq\t%rbp\n\tWORD $0x8948; BYTE $0xe5               // movq\t%rsp, %rbp\n\tLONG $0xf8e48348                       // andq\t$-8, %rsp\n\tLONG $0x0f428d48                       // leaq\t15(%rdx), %rax\n\tWORD $0x8548; BYTE $0xd2               // testq\t%rdx, %rdx\n\tLONG $0xc2490f48                       // cmovnsq\t%rdx, %rax\n\tWORD $0x8948; BYTE $0xc1               // movq\t%rax, %rcx\n\tLONG $0x04f9c148                       // sarq\t$4, %rcx\n\tLONG $0xf0e08348                       // andq\t$-16, %rax\n\tWORD $0x2948; BYTE $0xc2               // subq\t%rax, %rdx\n\tWORD $0xc985                           // testl\t%ecx, %ecx\n\tJLE  LBB11_1\n\tLONG $0x487cf162; WORD $0x0710         // vmovups\t(%rdi), %zmm0\n\tLONG $0x487cf162; WORD $0x065c         // vsubps\t(%rsi), %zmm0, %zmm0\n\tLONG $0x487cf162; WORD $0xc059         // vmulps\t%zmm0, %zmm0, %zmm0\n\tLONG $0x40c78348                       // addq\t$64, %rdi\n\tLONG $0x40c68348                       // addq\t$64, %rsi\n\tWORD $0xf983; BYTE $0x01               // cmpl\t$1, %ecx\n\tJE   LBB11_9\n\tWORD $0x8949; BYTE $0xc8               // movq\t%rcx, %r8\n\tLONG $0x06e0c149                       // shlq\t$6, %r8\n\tQUAD $0x003fffffff80b848; WORD $0x0000 // movabsq\t$274877906816, %rax             # imm = 0x3FFFFFFF80\n\tWORD $0x0149; BYTE $0xc0               // addq\t%rax, %r8\n\tLONG $0x40c88348                       // orq\t$64, %rax\n\tWORD $0x214c; BYTE $0xc0               // andq\t%r8, %rax\n\tLONG $0xff518d44                       // leal\t-1(%rcx), %r10d\n\tLONG $0xfe418d44                       // leal\t-2(%rcx), %r8d\n\tLONG $0x03f88341                       // cmpl\t$3, %r8d\n\tJAE  LBB11_18\n\tWORD $0x8949; BYTE $0xf8               // movq\t%rdi, %r8\n\tWORD $0x8949; BYTE $0xf1               // movq\t%rsi, %r9\n\tJMP  LBB11_5\n\nLBB11_1:\n\tLONG $0xc057f8c5 // vxorps\t%xmm0, %xmm0, %xmm0\n\tJMP  LBB11_9\n\nLBB11_18:\n\tWORD $0x8945; BYTE $0xd3 // movl\t%r10d, %r11d\n\tLONG $0xfce38341         // andl\t$-4, %r11d\n\tWORD $0x8949; BYTE $0xf8 // movq\t%rdi, %r8\n\tWORD $0x8949; BYTE $0xf1 // movq\t%rsi, %r9\n\nLBB11_19:\n\tLONG $0x487cd162; WORD $0x0810             // vmovups\t(%r8), %zmm1\n\tLONG $0x487cd162; WORD $0x5010; BYTE $0x01 // vmovups\t64(%r8), %zmm2\n\tLONG $0x487cd162; WORD $0x5810; BYTE $0x02 // vmovups\t128(%r8), %zmm3\n\tLONG $0x487cd162; WORD $0x6010; BYTE $0x03 // vmovups\t192(%r8), %zmm4\n\tLONG $0x4874d162; WORD $0x095c             // vsubps\t(%r9), %zmm1, %zmm1\n\tLONG $0x4874f162; WORD $0xc959             // vmulps\t%zmm1, %zmm1, %zmm1\n\tLONG $0x487cf162; WORD $0xc158             // vaddps\t%zmm1, %zmm0, %zmm0\n\tLONG $0x486cd162; WORD $0x495c; BYTE $0x01 // vsubps\t64(%r9), %zmm2, %zmm1\n\tLONG $0x4874f162; WORD $0xc959             // vmulps\t%zmm1, %zmm1, %zmm1\n\tLONG $0x487cf162; WORD $0xc158             // vaddps\t%zmm1, %zmm0, %zmm0\n\tLONG $0x4864d162; WORD $0x495c; BYTE $0x02 // vsubps\t128(%r9), %zmm3, %zmm1\n\tLONG $0x4874f162; WORD $0xc959             // vmulps\t%zmm1, %zmm1, %zmm1\n\tLONG $0x487cf162; WORD $0xc158             // vaddps\t%zmm1, %zmm0, %zmm0\n\tLONG $0x485cd162; WORD $0x495c; BYTE $0x03 // vsubps\t192(%r9), %zmm4, %zmm1\n\tLONG $0x4874f162; WORD $0xc959             // vmulps\t%zmm1, %zmm1, %zmm1\n\tLONG $0x487cf162; WORD $0xc158             // vaddps\t%zmm1, %zmm0, %zmm0\n\tLONG $0x00c08149; WORD $0x0001; BYTE $0x00 // addq\t$256, %r8                       # imm = 0x100\n\tLONG $0x00c18149; WORD $0x0001; BYTE $0x00 // addq\t$256, %r9                       # imm = 0x100\n\tLONG $0xfcc38341                           // addl\t$-4, %r11d\n\tJNE  LBB11_19\n\nLBB11_5:\n\tLONG $0x40588d4c         // leaq\t64(%rax), %r11\n\tLONG $0x03c2f641         // testb\t$3, %r10b\n\tJE   LBB11_8\n\tWORD $0xc9fe             // decb\t%cl\n\tWORD $0xb60f; BYTE $0xc9 // movzbl\t%cl, %ecx\n\tWORD $0xe183; BYTE $0x03 // andl\t$3, %ecx\n\tWORD $0xe1c1; BYTE $0x06 // shll\t$6, %ecx\n\tWORD $0x3145; BYTE $0xd2 // xorl\t%r10d, %r10d\n\nLBB11_7:\n\tLONG $0x487c9162; WORD $0x0c10; BYTE $0x10 // vmovups\t(%r8,%r10), %zmm1\n\tLONG $0x48749162; WORD $0x0c5c; BYTE $0x11 // vsubps\t(%r9,%r10), %zmm1, %zmm1\n\tLONG $0x4874f162; WORD $0xc959             // vmulps\t%zmm1, %zmm1, %zmm1\n\tLONG $0x487cf162; WORD $0xc158             // vaddps\t%zmm1, %zmm0, %zmm0\n\tLONG $0x40c28349                           // addq\t$64, %r10\n\tWORD $0x3944; BYTE $0xd1                   // cmpl\t%r10d, %ecx\n\tJNE  LBB11_7\n\nLBB11_8:\n\tWORD $0x0148; BYTE $0xc7 // addq\t%rax, %rdi\n\tLONG $0x40c78348         // addq\t$64, %rdi\n\tWORD $0x014c; BYTE $0xde // addq\t%r11, %rsi\n\nLBB11_9:\n\tLONG $0x48fdf362; WORD $0xc11b; BYTE $0x01 // vextractf64x4\t$1, %zmm0, %ymm1\n\tLONG $0xc158fcc5                           // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0x197de3c4; WORD $0x01c1             // vextractf128\t$1, %ymm0, %xmm1\n\tLONG $0xc158f8c5                           // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0xc8c6f9c5; BYTE $0x03               // vshufpd\t$3, %xmm0, %xmm0, %xmm1         # xmm1 = xmm0[1,1]\n\tLONG $0xc158f8c5                           // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0xc816fac5                           // vmovshdup\t%xmm0, %xmm1            # xmm1 = xmm0[1,1,3,3]\n\tLONG $0xc158f8c5                           // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0x07fa8348                           // cmpq\t$7, %rdx\n\tJLE  LBB11_11\n\tLONG $0x0f10fcc5                           // vmovups\t(%rdi), %ymm1\n\tLONG $0x0e5cf4c5                           // vsubps\t(%rsi), %ymm1, %ymm1\n\tLONG $0xc959f4c5                           // vmulps\t%ymm1, %ymm1, %ymm1\n\tLONG $0x20c78348                           // addq\t$32, %rdi\n\tLONG $0x20c68348                           // addq\t$32, %rsi\n\tLONG $0x197de3c4; WORD $0x01ca             // vextractf128\t$1, %ymm1, %xmm2\n\tLONG $0xc958e8c5                           // vaddps\t%xmm1, %xmm2, %xmm1\n\tLONG $0xd1c6f1c5; BYTE $0x01               // vshufpd\t$1, %xmm1, %xmm1, %xmm2         # xmm2 = xmm1[1,0]\n\tLONG $0xca58f0c5                           // vaddps\t%xmm2, %xmm1, %xmm1\n\tLONG $0xd116fac5                           // vmovshdup\t%xmm1, %xmm2            # xmm2 = xmm1[1,1,3,3]\n\tLONG $0xca58f2c5                           // vaddss\t%xmm2, %xmm1, %xmm1\n\tLONG $0xc158fac5                           // vaddss\t%xmm1, %xmm0, %xmm0\n\tWORD $0xc283; BYTE $0xf8                   // addl\t$-8, %edx\n\nLBB11_11:\n\tWORD $0xd285             // testl\t%edx, %edx\n\tJLE  LBB11_17\n\tWORD $0x8941; BYTE $0xd0 // movl\t%edx, %r8d\n\tWORD $0x8944; BYTE $0xc0 // movl\t%r8d, %eax\n\tWORD $0xe083; BYTE $0x03 // andl\t$3, %eax\n\tWORD $0xfa83; BYTE $0x04 // cmpl\t$4, %edx\n\tJAE  LBB11_20\n\tWORD $0xc931             // xorl\t%ecx, %ecx\n\tJMP  LBB11_14\n\nLBB11_20:\n\tLONG $0xfce08141; WORD $0xffff; BYTE $0x7f // andl\t$2147483644, %r8d               # imm = 0x7FFFFFFC\n\tWORD $0xc931                               // xorl\t%ecx, %ecx\n\nLBB11_21:\n\tLONG $0x0c10fac5; BYTE $0x8f   // vmovss\t(%rdi,%rcx,4), %xmm1            # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x5410fac5; WORD $0x048f // vmovss\t4(%rdi,%rcx,4), %xmm2           # xmm2 = mem[0],zero,zero,zero\n\tLONG $0x0c5cf2c5; BYTE $0x8e   // vsubss\t(%rsi,%rcx,4), %xmm1, %xmm1\n\tLONG $0x545ceac5; WORD $0x048e // vsubss\t4(%rsi,%rcx,4), %xmm2, %xmm2\n\tLONG $0xa971e2c4; BYTE $0xc8   // vfmadd213ss\t%xmm0, %xmm1, %xmm1     # xmm1 = (xmm1 * xmm1) + xmm0\n\tLONG $0x4410fac5; WORD $0x088f // vmovss\t8(%rdi,%rcx,4), %xmm0           # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x5c5cfac5; WORD $0x088e // vsubss\t8(%rsi,%rcx,4), %xmm0, %xmm3\n\tLONG $0xa969e2c4; BYTE $0xd1   // vfmadd213ss\t%xmm1, %xmm2, %xmm2     # xmm2 = (xmm2 * xmm2) + xmm1\n\tLONG $0x4410fac5; WORD $0x0c8f // vmovss\t12(%rdi,%rcx,4), %xmm0          # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x445cfac5; WORD $0x0c8e // vsubss\t12(%rsi,%rcx,4), %xmm0, %xmm0\n\tLONG $0xa961e2c4; BYTE $0xda   // vfmadd213ss\t%xmm2, %xmm3, %xmm3     # xmm3 = (xmm3 * xmm3) + xmm2\n\tLONG $0xa979e2c4; BYTE $0xc3   // vfmadd213ss\t%xmm3, %xmm0, %xmm0     # xmm0 = (xmm0 * xmm0) + xmm3\n\tLONG $0x04c18348               // addq\t$4, %rcx\n\tWORD $0x3949; BYTE $0xc8       // cmpq\t%rcx, %r8\n\tJNE  LBB11_21\n\nLBB11_14:\n\tWORD $0x8548; BYTE $0xc0 // testq\t%rax, %rax\n\tJE   LBB11_17\n\tLONG $0x8e148d48         // leaq\t(%rsi,%rcx,4), %rdx\n\tLONG $0x8f0c8d48         // leaq\t(%rdi,%rcx,4), %rcx\n\tWORD $0xf631             // xorl\t%esi, %esi\n\nLBB11_16:\n\tLONG $0x0c10fac5; BYTE $0xb1 // vmovss\t(%rcx,%rsi,4), %xmm1            # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x0c5cf2c5; BYTE $0xb2 // vsubss\t(%rdx,%rsi,4), %xmm1, %xmm1\n\tLONG $0xb971e2c4; BYTE $0xc1 // vfmadd231ss\t%xmm1, %xmm1, %xmm0     # xmm0 = (xmm1 * xmm1) + xmm0\n\tWORD $0xff48; BYTE $0xc6     // incq\t%rsi\n\tWORD $0x3948; BYTE $0xf0     // cmpq\t%rsi, %rax\n\tJNE  LBB11_16\n\nLBB11_17:\n\tLONG  $0xc051fac5         // vsqrtss\t%xmm0, %xmm0, %xmm0\n\tWORD  $0x8948; BYTE $0xec // movq\t%rbp, %rsp\n\tBYTE  $0x5d               // popq\t%rbp\n\tWORD  $0xf8c5; BYTE $0x77 // vzeroupper\n\tMOVSS X0, result+24(FP)\n\tRET\n\nTEXT ·_mm512_mm(SB), $0-88\n\tMOVQ  transA+0(FP), DI\n\tMOVQ  transB+1(FP), SI\n\tMOVQ  m+8(FP), DX\n\tMOVQ  n+16(FP), CX\n\tMOVQ  k+24(FP), R8\n\tMOVQ  a+32(FP), R9\n\tPUSHQ ldc+72(FP)\n\tPUSHQ c+64(FP)\n\tPUSHQ ldb+56(FP)\n\tPUSHQ b+48(FP)\n\tPUSHQ lda+40(FP)\n\tPUSHQ $0\n\tBYTE  $0x55                                 // pushq\t%rbp\n\tWORD  $0x8948; BYTE $0xe5                   // movq\t%rsp, %rbp\n\tWORD  $0x5741                               // pushq\t%r15\n\tWORD  $0x5641                               // pushq\t%r14\n\tWORD  $0x5541                               // pushq\t%r13\n\tWORD  $0x5441                               // pushq\t%r12\n\tBYTE  $0x53                                 // pushq\t%rbx\n\tLONG  $0xf8e48348                           // andq\t$-8, %rsp\n\tLONG  $0x88ec8148; WORD $0x0000; BYTE $0x00 // subq\t$136, %rsp\n\tLONG  $0x244c894c; BYTE $0x18               // movq\t%r9, 24(%rsp)                   # 8-byte Spill\n\tLONG  $0x30458b48                           // movq\t48(%rbp), %rax\n\tLONG  $0x24448948; BYTE $0x70               // movq\t%rax, 112(%rsp)                 # 8-byte Spill\n\tLONG  $0x28458b48                           // movq\t40(%rbp), %rax\n\tLONG  $0x24448948; BYTE $0x68               // movq\t%rax, 104(%rsp)                 # 8-byte Spill\n\tWORD  $0xf889                               // movl\t%edi, %eax\n\tWORD  $0x0840; BYTE $0xf0                   // orb\t%sil, %al\n\tLONG  $0x2404894c                           // movq\t%r8, (%rsp)                     # 8-byte Spill\n\tLONG  $0x24548948; BYTE $0x38               // movq\t%rdx, 56(%rsp)                  # 8-byte Spill\n\tJE    LBB12_1\n\tWORD  $0xf089                               // movl\t%esi, %eax\n\tWORD  $0x0134                               // xorb\t$1, %al\n\tWORD  $0x0840; BYTE $0xf8                   // orb\t%dil, %al\n\tJE    LBB12_22\n\tWORD  $0x8941; BYTE $0xf9                   // movl\t%edi, %r9d\n\tLONG  $0x01f18041                           // xorb\t$1, %r9b\n\tWORD  $0x0841; BYTE $0xf1                   // orb\t%sil, %r9b\n\tWORD  $0x854d; BYTE $0xc0                   // testq\t%r8, %r8\n\tLONG  $0xc09f0f41                           // setg\t%r8b\n\tWORD  $0x8548; BYTE $0xc9                   // testq\t%rcx, %rcx\n\tWORD  $0x9f0f; BYTE $0xc0                   // setg\t%al\n\tWORD  $0x2044; BYTE $0xc0                   // andb\t%r8b, %al\n\tWORD  $0x8445; BYTE $0xc9                   // testb\t%r9b, %r9b\n\tJE    LBB12_99\n\tWORD  $0x8548; BYTE $0xd2                   // testq\t%rdx, %rdx\n\tWORD  $0x9f0f; BYTE $0xc2                   // setg\t%dl\n\tWORD  $0x2040; BYTE $0xf0                   // andb\t%sil, %al\n\tWORD  $0x2040; BYTE $0xfa                   // andb\t%dil, %dl\n\tWORD  $0xc220                               // andb\t%al, %dl\n\tWORD  $0xfa80; BYTE $0x01                   // cmpb\t$1, %dl\n\tJNE   LBB12_140\n\tLONG  $0x30458b48                           // movq\t48(%rbp), %rax\n\tQUAD  $0x00000000850c8d4c                   // leaq\t(,%rax,4), %r9\n\tLONG  $0x24148b48                           // movq\t(%rsp), %rdx                    # 8-byte Reload\n\tLONG  $0xff428d48                           // leaq\t-1(%rdx), %rax\n\tLONG  $0x45af0f48; BYTE $0x10               // imulq\t16(%rbp), %rax\n\tWORD  $0x0148; BYTE $0xca                   // addq\t%rcx, %rdx\n\tLONG  $0x187d8b48                           // movq\t24(%rbp), %rdi\n\tLONG  $0x97548d48; BYTE $0xfc               // leaq\t-4(%rdi,%rdx,4), %rdx\n\tLONG  $0x24548948; BYTE $0x48               // movq\t%rdx, 72(%rsp)                  # 8-byte Spill\n\tLONG  $0x28458b4c                           // movq\t40(%rbp), %r8\n\tLONG  $0x88148d49                           // leaq\t(%r8,%rcx,4), %rdx\n\tLONG  $0x24548948; BYTE $0x40               // movq\t%rdx, 64(%rsp)                  # 8-byte Spill\n\tLONG  $0x24548b48; BYTE $0x18               // movq\t24(%rsp), %rdx                  # 8-byte Reload\n\tLONG  $0x82048d48                           // leaq\t(%rdx,%rax,4), %rax\n\tLONG  $0x04c08348                           // addq\t$4, %rax\n\tQUAD  $0x0000008024848948                   // movq\t%rax, 128(%rsp)                 # 8-byte Spill\n\tLONG  $0x08f98348                           // cmpq\t$8, %rcx\n\tWORD  $0x930f; BYTE $0xc0                   // setae\t%al\n\tLONG  $0x20558b48                           // movq\t32(%rbp), %rdx\n\tLONG  $0x01fa8348                           // cmpq\t$1, %rdx\n\tLONG  $0xc2940f41                           // sete\t%r10b\n\tWORD  $0x2041; BYTE $0xc2                   // andb\t%al, %r10b\n\tQUAD  $0xffffffffffc0be48; WORD $0x7fff     // movabsq\t$9223372036854775744, %rsi      # imm = 0x7FFFFFFFFFFFFFC0\n\tWORD  $0x8948; BYTE $0xc8                   // movq\t%rcx, %rax\n\tWORD  $0x2148; BYTE $0xf0                   // andq\t%rsi, %rax\n\tLONG  $0x24448948; BYTE $0x28               // movq\t%rax, 40(%rsp)                  # 8-byte Spill\n\tLONG  $0x38ce8348                           // orq\t$56, %rsi\n\tWORD  $0x2148; BYTE $0xce                   // andq\t%rcx, %rsi\n\tLONG  $0xff418d48                           // leaq\t-1(%rcx), %rax\n\tLONG  $0x24448948; BYTE $0x20               // movq\t%rax, 32(%rsp)                  # 8-byte Spill\n\tLONG  $0xc0878d48; WORD $0x0000; BYTE $0x00 // leaq\t192(%rdi), %rax\n\tLONG  $0x24448948; BYTE $0x78               // movq\t%rax, 120(%rsp)                 # 8-byte Spill\n\tLONG  $0xc0808d49; WORD $0x0000; BYTE $0x00 // leaq\t192(%r8), %rax\n\tQUAD  $0x00000000953c8d48                   // leaq\t(,%rdx,4), %rdi\n\tLONG  $0x247c8948; BYTE $0x60               // movq\t%rdi, 96(%rsp)                  # 8-byte Spill\n\tQUAD  $0x00000000d53c8d48                   // leaq\t(,%rdx,8), %rdi\n\tWORD  $0xf641; BYTE $0xd2                   // notb\t%r10b\n\tLONG  $0x24548844; BYTE $0x0f               // movb\t%r10b, 15(%rsp)                 # 1-byte Spill\n\tWORD  $0x3145; BYTE $0xf6                   // xorl\t%r14d, %r14d\n\tWORD  $0x894c; BYTE $0xc2                   // movq\t%r8, %rdx\n\tLONG  $0x244c894c; BYTE $0x50               // movq\t%r9, 80(%rsp)                   # 8-byte Spill\n\tJMP   LBB12_121\n\nLBB12_139:\n\tLONG $0x24748b4c; BYTE $0x58 // movq\t88(%rsp), %r14                  # 8-byte Reload\n\tWORD $0xff49; BYTE $0xc6     // incq\t%r14\n\tLONG $0x244c8b4c; BYTE $0x50 // movq\t80(%rsp), %r9                   # 8-byte Reload\n\tWORD $0x014c; BYTE $0xc8     // addq\t%r9, %rax\n\tWORD $0x014c; BYTE $0xca     // addq\t%r9, %rdx\n\tLONG $0x24743b4c; BYTE $0x38 // cmpq\t56(%rsp), %r14                  # 8-byte Folded Reload\n\tJE   LBB12_140\n\nLBB12_121:\n\tQUAD $0x000000000000b849; WORD $0x2000 // movabsq\t$2305843009213693952, %r8       # imm = 0x2000000000000000\n\tLONG $0x1045854c                       // testq\t%r8, 16(%rbp)\n\tLONG $0xc0950f41                       // setne\t%r8b\n\tLONG $0xceaf0f4d                       // imulq\t%r14, %r9\n\tLONG $0x287d8b4c                       // movq\t40(%rbp), %r15\n\tLONG $0x0f148d4f                       // leaq\t(%r15,%r9), %r10\n\tLONG $0x244c034c; BYTE $0x40           // addq\t64(%rsp), %r9                   # 8-byte Folded Reload\n\tLONG $0x245c8b4c; BYTE $0x18           // movq\t24(%rsp), %r11                  # 8-byte Reload\n\tLONG $0xb31c8d4b                       // leaq\t(%r11,%r14,4), %rbx\n\tQUAD $0x00000080249c8b4c               // movq\t128(%rsp), %r11                 # 8-byte Reload\n\tLONG $0xb31c8d4f                       // leaq\t(%r11,%r14,4), %r11\n\tLONG $0x2474894c; BYTE $0x58           // movq\t%r14, 88(%rsp)                  # 8-byte Spill\n\tLONG $0x75af0f4c; BYTE $0x30           // imulq\t48(%rbp), %r14\n\tLONG $0xb7348d4f                       // leaq\t(%r15,%r14,4), %r14\n\tLONG $0x2474894c; BYTE $0x30           // movq\t%r14, 48(%rsp)                  # 8-byte Spill\n\tWORD $0x394d; BYTE $0xda               // cmpq\t%r11, %r10\n\tLONG $0xc3920f41                       // setb\t%r11b\n\tWORD $0x394c; BYTE $0xcb               // cmpq\t%r9, %rbx\n\tLONG $0xc6920f41                       // setb\t%r14b\n\tWORD $0x2045; BYTE $0xde               // andb\t%r11b, %r14b\n\tWORD $0x0845; BYTE $0xc6               // orb\t%r8b, %r14b\n\tLONG $0x24543b4c; BYTE $0x48           // cmpq\t72(%rsp), %r10                  # 8-byte Folded Reload\n\tLONG $0xc0920f41                       // setb\t%r8b\n\tLONG $0x18658b4c                       // movq\t24(%rbp), %r12\n\tWORD $0x394d; BYTE $0xe1               // cmpq\t%r12, %r9\n\tLONG $0xc1970f41                       // seta\t%r9b\n\tWORD $0x2045; BYTE $0xc1               // andb\t%r8b, %r9b\n\tWORD $0x0845; BYTE $0xf1               // orb\t%r14b, %r9b\n\tLONG $0x244c0a44; BYTE $0x0f           // orb\t15(%rsp), %r9b                  # 1-byte Folded Reload\n\tLONG $0x244c8844; BYTE $0x10           // movb\t%r9b, 16(%rsp)                  # 1-byte Spill\n\tLONG $0x244c8b4c; BYTE $0x78           // movq\t120(%rsp), %r9                  # 8-byte Reload\n\tWORD $0x3145; BYTE $0xed               // xorl\t%r13d, %r13d\n\tJMP  LBB12_122\n\nLBB12_138:\n\tWORD $0xff49; BYTE $0xc5 // incq\t%r13\n\tLONG $0x04c18349         // addq\t$4, %r9\n\tLONG $0x04c48349         // addq\t$4, %r12\n\tLONG $0x242c3b4c         // cmpq\t(%rsp), %r13                    # 8-byte Folded Reload\n\tJE   LBB12_139\n\nLBB12_122:\n\tWORD $0x894d; BYTE $0xe8     // movq\t%r13, %r8\n\tLONG $0x45af0f4c; BYTE $0x10 // imulq\t16(%rbp), %r8\n\tLONG $0x102444f6; BYTE $0x01 // testb\t$1, 16(%rsp)                    # 1-byte Folded Reload\n\tJE   LBB12_124\n\tWORD $0x3145; BYTE $0xff     // xorl\t%r15d, %r15d\n\tJMP  LBB12_133\n\nLBB12_124:\n\tLONG $0x40f98348         // cmpq\t$64, %rcx\n\tJAE  LBB12_126\n\tWORD $0x3145; BYTE $0xd2 // xorl\t%r10d, %r10d\n\tJMP  LBB12_130\n\nLBB12_126:\n\tLONG $0x487db262; WORD $0x0418; BYTE $0x83 // vbroadcastss\t(%rbx,%r8,4), %zmm0\n\tWORD $0x3145; BYTE $0xdb                   // xorl\t%r11d, %r11d\n\tLONG $0x24548b4c; BYTE $0x28               // movq\t40(%rsp), %r10                  # 8-byte Reload\n\nLBB12_127:\n\tQUAD $0xfd994c10487c9162                   // vmovups\t-192(%r9,%r11,4), %zmm1\n\tQUAD $0xfe995410487c9162                   // vmovups\t-128(%r9,%r11,4), %zmm2\n\tQUAD $0xff995c10487c9162                   // vmovups\t-64(%r9,%r11,4), %zmm3\n\tLONG $0x487c9162; WORD $0x2410; BYTE $0x99 // vmovups\t(%r9,%r11,4), %zmm4\n\tQUAD $0xfd984ca8487db262                   // vfmadd213ps\t-192(%rax,%r11,4), %zmm0, %zmm1 # zmm1 = (zmm0 * zmm1) + mem\n\tQUAD $0xfe9854a8487db262                   // vfmadd213ps\t-128(%rax,%r11,4), %zmm0, %zmm2 # zmm2 = (zmm0 * zmm2) + mem\n\tQUAD $0xff985ca8487db262                   // vfmadd213ps\t-64(%rax,%r11,4), %zmm0, %zmm3 # zmm3 = (zmm0 * zmm3) + mem\n\tLONG $0x487db262; WORD $0x24a8; BYTE $0x98 // vfmadd213ps\t(%rax,%r11,4), %zmm0, %zmm4 # zmm4 = (zmm0 * zmm4) + mem\n\tQUAD $0xfd984c11487cb162                   // vmovups\t%zmm1, -192(%rax,%r11,4)\n\tQUAD $0xfe985411487cb162                   // vmovups\t%zmm2, -128(%rax,%r11,4)\n\tQUAD $0xff985c11487cb162                   // vmovups\t%zmm3, -64(%rax,%r11,4)\n\tLONG $0x487cb162; WORD $0x2411; BYTE $0x98 // vmovups\t%zmm4, (%rax,%r11,4)\n\tLONG $0x40c38349                           // addq\t$64, %r11\n\tWORD $0x394d; BYTE $0xda                   // cmpq\t%r11, %r10\n\tJNE  LBB12_127\n\tWORD $0x3949; BYTE $0xca                   // cmpq\t%rcx, %r10\n\tJE   LBB12_138\n\tLONG $0x247c8b4c; BYTE $0x28               // movq\t40(%rsp), %r15                  # 8-byte Reload\n\tWORD $0x894d; BYTE $0xfa                   // movq\t%r15, %r10\n\tWORD $0xc1f6; BYTE $0x38                   // testb\t$56, %cl\n\tJE   LBB12_133\n\nLBB12_130:\n\tLONG $0x187da2c4; WORD $0x8304 // vbroadcastss\t(%rbx,%r8,4), %ymm0\n\nLBB12_131:\n\tLONG $0x107c81c4; WORD $0x940c // vmovups\t(%r12,%r10,4), %ymm1\n\tLONG $0xa87da2c4; WORD $0x920c // vfmadd213ps\t(%rdx,%r10,4), %ymm0, %ymm1 # ymm1 = (ymm0 * ymm1) + mem\n\tLONG $0x117ca1c4; WORD $0x920c // vmovups\t%ymm1, (%rdx,%r10,4)\n\tLONG $0x08c28349               // addq\t$8, %r10\n\tWORD $0x394c; BYTE $0xd6       // cmpq\t%r10, %rsi\n\tJNE  LBB12_131\n\tWORD $0x8949; BYTE $0xf7       // movq\t%rsi, %r15\n\tWORD $0x3948; BYTE $0xce       // cmpq\t%rcx, %rsi\n\tJE   LBB12_138\n\nLBB12_133:\n\tWORD $0x894d; BYTE $0xfe       // movq\t%r15, %r14\n\tWORD $0xc1f6; BYTE $0x01       // testb\t$1, %cl\n\tJE   LBB12_135\n\tLONG $0x18558b4c               // movq\t24(%rbp), %r10\n\tLONG $0xaa148d4f               // leaq\t(%r10,%r13,4), %r10\n\tLONG $0x107aa1c4; WORD $0x8304 // vmovss\t(%rbx,%r8,4), %xmm0             # xmm0 = mem[0],zero,zero,zero\n\tWORD $0x894d; BYTE $0xfb       // movq\t%r15, %r11\n\tLONG $0x5daf0f4c; BYTE $0x20   // imulq\t32(%rbp), %r11\n\tLONG $0x107a81c4; WORD $0x9a0c // vmovss\t(%r10,%r11,4), %xmm1            # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x24548b4c; BYTE $0x30   // movq\t48(%rsp), %r10                  # 8-byte Reload\n\tLONG $0xa97982c4; WORD $0xba0c // vfmadd213ss\t(%r10,%r15,4), %xmm0, %xmm1 # xmm1 = (xmm0 * xmm1) + mem\n\tLONG $0x117a81c4; WORD $0xba0c // vmovss\t%xmm1, (%r10,%r15,4)\n\tWORD $0x894d; BYTE $0xfe       // movq\t%r15, %r14\n\tLONG $0x01ce8349               // orq\t$1, %r14\n\nLBB12_135:\n\tLONG $0x247c3b4c; BYTE $0x20 // cmpq\t32(%rsp), %r15                  # 8-byte Folded Reload\n\tJE   LBB12_138\n\tLONG $0x24548b4c; BYTE $0x60 // movq\t96(%rsp), %r10                  # 8-byte Reload\n\tWORD $0x894d; BYTE $0xd7     // movq\t%r10, %r15\n\tLONG $0xfeaf0f4d             // imulq\t%r14, %r15\n\tLONG $0x015e8d4d             // leaq\t1(%r14), %r11\n\tLONG $0xdaaf0f4d             // imulq\t%r10, %r11\n\tWORD $0x894d; BYTE $0xe2     // movq\t%r12, %r10\n\nLBB12_137:\n\tLONG $0x107aa1c4; WORD $0x8304             // vmovss\t(%rbx,%r8,4), %xmm0             # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x107a81c4; WORD $0x3a0c             // vmovss\t(%r10,%r15), %xmm1              # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xa979a2c4; WORD $0xb20c             // vfmadd213ss\t(%rdx,%r14,4), %xmm0, %xmm1 # xmm1 = (xmm0 * xmm1) + mem\n\tLONG $0x117aa1c4; WORD $0xb20c             // vmovss\t%xmm1, (%rdx,%r14,4)\n\tLONG $0x107aa1c4; WORD $0x8304             // vmovss\t(%rbx,%r8,4), %xmm0             # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x107a81c4; WORD $0x1a0c             // vmovss\t(%r10,%r11), %xmm1              # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xa979a2c4; WORD $0xb24c; BYTE $0x04 // vfmadd213ss\t4(%rdx,%r14,4), %xmm0, %xmm1 # xmm1 = (xmm0 * xmm1) + mem\n\tLONG $0x117aa1c4; WORD $0xb24c; BYTE $0x04 // vmovss\t%xmm1, 4(%rdx,%r14,4)\n\tLONG $0x02c68349                           // addq\t$2, %r14\n\tWORD $0x0149; BYTE $0xfa                   // addq\t%rdi, %r10\n\tWORD $0x394c; BYTE $0xf1                   // cmpq\t%r14, %rcx\n\tJNE  LBB12_137\n\tJMP  LBB12_138\n\nLBB12_1:\n\tWORD $0x8548; BYTE $0xd2                   // testq\t%rdx, %rdx\n\tWORD $0x9e0f; BYTE $0xc0                   // setle\t%al\n\tWORD $0x854d; BYTE $0xc0                   // testq\t%r8, %r8\n\tWORD $0x9e0f; BYTE $0xc2                   // setle\t%dl\n\tWORD $0xc208                               // orb\t%al, %dl\n\tWORD $0x8548; BYTE $0xc9                   // testq\t%rcx, %rcx\n\tWORD $0x9e0f; BYTE $0xc0                   // setle\t%al\n\tWORD $0xd008                               // orb\t%dl, %al\n\tJNE  LBB12_140\n\tLONG $0x30458b48                           // movq\t48(%rbp), %rax\n\tQUAD $0x0000000085048d4c                   // leaq\t(,%rax,4), %r8\n\tLONG $0x10458b48                           // movq\t16(%rbp), %rax\n\tQUAD $0x0000000085048d48                   // leaq\t(,%rax,4), %rax\n\tLONG $0x24448948; BYTE $0x60               // movq\t%rax, 96(%rsp)                  # 8-byte Spill\n\tLONG $0x243c8b48                           // movq\t(%rsp), %rdi                    # 8-byte Reload\n\tLONG $0xff478d48                           // leaq\t-1(%rdi), %rax\n\tLONG $0x20558b48                           // movq\t32(%rbp), %rdx\n\tLONG $0xc2af0f48                           // imulq\t%rdx, %rax\n\tWORD $0x0148; BYTE $0xc8                   // addq\t%rcx, %rax\n\tLONG $0x18758b48                           // movq\t24(%rbp), %rsi\n\tLONG $0x86048d48                           // leaq\t(%rsi,%rax,4), %rax\n\tLONG $0x24448948; BYTE $0x58               // movq\t%rax, 88(%rsp)                  # 8-byte Spill\n\tLONG $0x284d8b4c                           // movq\t40(%rbp), %r9\n\tLONG $0x89048d49                           // leaq\t(%r9,%rcx,4), %rax\n\tLONG $0x24448948; BYTE $0x50               // movq\t%rax, 80(%rsp)                  # 8-byte Spill\n\tLONG $0x24448b48; BYTE $0x18               // movq\t24(%rsp), %rax                  # 8-byte Reload\n\tLONG $0xb8048d48                           // leaq\t(%rax,%rdi,4), %rax\n\tLONG $0x24448948; BYTE $0x48               // movq\t%rax, 72(%rsp)                  # 8-byte Spill\n\tQUAD $0xffffffffffc0be49; WORD $0x7fff     // movabsq\t$9223372036854775744, %r14      # imm = 0x7FFFFFFFFFFFFFC0\n\tWORD $0x8949; BYTE $0xcc                   // movq\t%rcx, %r12\n\tWORD $0x214d; BYTE $0xf4                   // andq\t%r14, %r12\n\tLONG $0x38ce8349                           // orq\t$56, %r14\n\tWORD $0x2149; BYTE $0xce                   // andq\t%rcx, %r14\n\tLONG $0xff418d48                           // leaq\t-1(%rcx), %rax\n\tLONG $0x24448948; BYTE $0x10               // movq\t%rax, 16(%rsp)                  # 8-byte Spill\n\tLONG $0xc0868d48; WORD $0x0000; BYTE $0x00 // leaq\t192(%rsi), %rax\n\tLONG $0x24448948; BYTE $0x40               // movq\t%rax, 64(%rsp)                  # 8-byte Spill\n\tQUAD $0x0000000095048d48                   // leaq\t(,%rdx,4), %rax\n\tLONG $0xc0b18d49; WORD $0x0000; BYTE $0x00 // leaq\t192(%r9), %rsi\n\tWORD $0x3145; BYTE $0xff                   // xorl\t%r15d, %r15d\n\tLONG $0x2444894c; BYTE $0x28               // movq\t%r8, 40(%rsp)                   # 8-byte Spill\n\tJMP  LBB12_3\n\nLBB12_20:\n\tLONG $0x247c8b4c; BYTE $0x30 // movq\t48(%rsp), %r15                  # 8-byte Reload\n\tWORD $0xff49; BYTE $0xc7     // incq\t%r15\n\tLONG $0x24448b4c; BYTE $0x28 // movq\t40(%rsp), %r8                   # 8-byte Reload\n\tWORD $0x014c; BYTE $0xc6     // addq\t%r8, %rsi\n\tWORD $0x014d; BYTE $0xc1     // addq\t%r8, %r9\n\tLONG $0x247c3b4c; BYTE $0x38 // cmpq\t56(%rsp), %r15                  # 8-byte Folded Reload\n\tJE   LBB12_140\n\nLBB12_3:\n\tQUAD $0x000000000000ba48; WORD $0x2000 // movabsq\t$2305843009213693952, %rdx      # imm = 0x2000000000000000\n\tLONG $0x20558548                       // testq\t%rdx, 32(%rbp)\n\tWORD $0x950f; BYTE $0xc2               // setne\t%dl\n\tWORD $0x894c; BYTE $0xc7               // movq\t%r8, %rdi\n\tLONG $0xffaf0f49                       // imulq\t%r15, %rdi\n\tLONG $0x28458b4c                       // movq\t40(%rbp), %r8\n\tWORD $0x0149; BYTE $0xf8               // addq\t%rdi, %r8\n\tLONG $0x247c0348; BYTE $0x50           // addq\t80(%rsp), %rdi                  # 8-byte Folded Reload\n\tLONG $0x24548b4c; BYTE $0x60           // movq\t96(%rsp), %r10                  # 8-byte Reload\n\tLONG $0xd7af0f4d                       // imulq\t%r15, %r10\n\tLONG $0x246c8b4c; BYTE $0x18           // movq\t24(%rsp), %r13                  # 8-byte Reload\n\tLONG $0x2a1c8d4f                       // leaq\t(%r10,%r13), %r11\n\tLONG $0x2454034c; BYTE $0x48           // addq\t72(%rsp), %r10                  # 8-byte Folded Reload\n\tWORD $0x894c; BYTE $0xfb               // movq\t%r15, %rbx\n\tLONG $0x5daf0f48; BYTE $0x10           // imulq\t16(%rbp), %rbx\n\tLONG $0x247c894c; BYTE $0x30           // movq\t%r15, 48(%rsp)                  # 8-byte Spill\n\tLONG $0x7daf0f4c; BYTE $0x30           // imulq\t48(%rbp), %r15\n\tWORD $0x394d; BYTE $0xd0               // cmpq\t%r10, %r8\n\tLONG $0xc2920f41                       // setb\t%r10b\n\tWORD $0x3949; BYTE $0xfb               // cmpq\t%rdi, %r11\n\tLONG $0x9d5c8d49; BYTE $0x00           // leaq\t(%r13,%rbx,4), %rbx\n\tLONG $0x285d8b4c                       // movq\t40(%rbp), %r11\n\tLONG $0xbb1c8d4f                       // leaq\t(%r11,%r15,4), %r11\n\tLONG $0x245c894c; BYTE $0x20           // movq\t%r11, 32(%rsp)                  # 8-byte Spill\n\tLONG $0xc5920f41                       // setb\t%r13b\n\tWORD $0x2045; BYTE $0xd5               // andb\t%r10b, %r13b\n\tLONG $0x24443b4c; BYTE $0x58           // cmpq\t88(%rsp), %r8                   # 8-byte Folded Reload\n\tLONG $0xc0920f41                       // setb\t%r8b\n\tLONG $0x18558b4c                       // movq\t24(%rbp), %r10\n\tWORD $0x394c; BYTE $0xd7               // cmpq\t%r10, %rdi\n\tLONG $0xc7970f40                       // seta\t%dil\n\tWORD $0x2044; BYTE $0xc7               // andb\t%r8b, %dil\n\tWORD $0x0841; BYTE $0xd5               // orb\t%dl, %r13b\n\tWORD $0x0841; BYTE $0xfd               // orb\t%dil, %r13b\n\tLONG $0x24548b48; BYTE $0x40           // movq\t64(%rsp), %rdx                  # 8-byte Reload\n\tWORD $0xff31                           // xorl\t%edi, %edi\n\tLONG $0x241c8b4c                       // movq\t(%rsp), %r11                    # 8-byte Reload\n\tJMP  LBB12_4\n\nLBB12_19:\n\tWORD $0xff48; BYTE $0xc7 // incq\t%rdi\n\tWORD $0x0148; BYTE $0xc2 // addq\t%rax, %rdx\n\tWORD $0x0149; BYTE $0xc2 // addq\t%rax, %r10\n\tWORD $0x394c; BYTE $0xdf // cmpq\t%r11, %rdi\n\tJE   LBB12_20\n\nLBB12_4:\n\tLONG $0x08f98348         // cmpq\t$8, %rcx\n\tLONG $0xc0920f41         // setb\t%r8b\n\tWORD $0x0845; BYTE $0xe8 // orb\t%r13b, %r8b\n\tLONG $0x01c0f641         // testb\t$1, %r8b\n\tJE   LBB12_6\n\tWORD $0x3145; BYTE $0xc0 // xorl\t%r8d, %r8d\n\tJMP  LBB12_15\n\nLBB12_6:\n\tLONG $0x40f98348         // cmpq\t$64, %rcx\n\tJAE  LBB12_8\n\tWORD $0x3145; BYTE $0xff // xorl\t%r15d, %r15d\n\tJMP  LBB12_12\n\nLBB12_8:\n\tLONG $0x487df262; WORD $0x0418; BYTE $0xbb // vbroadcastss\t(%rbx,%rdi,4), %zmm0\n\tWORD $0x3145; BYTE $0xc0                   // xorl\t%r8d, %r8d\n\nLBB12_9:\n\tQUAD $0xfd824c10487cb162                   // vmovups\t-192(%rdx,%r8,4), %zmm1\n\tQUAD $0xfe825410487cb162                   // vmovups\t-128(%rdx,%r8,4), %zmm2\n\tQUAD $0xff825c10487cb162                   // vmovups\t-64(%rdx,%r8,4), %zmm3\n\tLONG $0x487cb162; WORD $0x2410; BYTE $0x82 // vmovups\t(%rdx,%r8,4), %zmm4\n\tQUAD $0xfd864ca8487db262                   // vfmadd213ps\t-192(%rsi,%r8,4), %zmm0, %zmm1 # zmm1 = (zmm0 * zmm1) + mem\n\tQUAD $0xfe8654a8487db262                   // vfmadd213ps\t-128(%rsi,%r8,4), %zmm0, %zmm2 # zmm2 = (zmm0 * zmm2) + mem\n\tQUAD $0xff865ca8487db262                   // vfmadd213ps\t-64(%rsi,%r8,4), %zmm0, %zmm3 # zmm3 = (zmm0 * zmm3) + mem\n\tLONG $0x487db262; WORD $0x24a8; BYTE $0x86 // vfmadd213ps\t(%rsi,%r8,4), %zmm0, %zmm4 # zmm4 = (zmm0 * zmm4) + mem\n\tQUAD $0xfd864c11487cb162                   // vmovups\t%zmm1, -192(%rsi,%r8,4)\n\tQUAD $0xfe865411487cb162                   // vmovups\t%zmm2, -128(%rsi,%r8,4)\n\tQUAD $0xff865c11487cb162                   // vmovups\t%zmm3, -64(%rsi,%r8,4)\n\tLONG $0x487cb162; WORD $0x2411; BYTE $0x86 // vmovups\t%zmm4, (%rsi,%r8,4)\n\tLONG $0x40c08349                           // addq\t$64, %r8\n\tWORD $0x394d; BYTE $0xc4                   // cmpq\t%r8, %r12\n\tJNE  LBB12_9\n\tWORD $0x3949; BYTE $0xcc                   // cmpq\t%rcx, %r12\n\tJE   LBB12_19\n\tWORD $0x894d; BYTE $0xe7                   // movq\t%r12, %r15\n\tWORD $0x894d; BYTE $0xe0                   // movq\t%r12, %r8\n\tWORD $0xc1f6; BYTE $0x38                   // testb\t$56, %cl\n\tJE   LBB12_15\n\nLBB12_12:\n\tLONG $0x187de2c4; WORD $0xbb04 // vbroadcastss\t(%rbx,%rdi,4), %ymm0\n\nLBB12_13:\n\tLONG $0x107c81c4; WORD $0xba0c // vmovups\t(%r10,%r15,4), %ymm1\n\tLONG $0xa87d82c4; WORD $0xb90c // vfmadd213ps\t(%r9,%r15,4), %ymm0, %ymm1 # ymm1 = (ymm0 * ymm1) + mem\n\tLONG $0x117c81c4; WORD $0xb90c // vmovups\t%ymm1, (%r9,%r15,4)\n\tLONG $0x08c78349               // addq\t$8, %r15\n\tWORD $0x394d; BYTE $0xfe       // cmpq\t%r15, %r14\n\tJNE  LBB12_13\n\tWORD $0x894d; BYTE $0xf0       // movq\t%r14, %r8\n\tWORD $0x3949; BYTE $0xce       // cmpq\t%rcx, %r14\n\tJE   LBB12_19\n\nLBB12_15:\n\tWORD $0x894d; BYTE $0xc7       // movq\t%r8, %r15\n\tWORD $0xc1f6; BYTE $0x01       // testb\t$1, %cl\n\tJE   LBB12_17\n\tWORD $0x8949; BYTE $0xff       // movq\t%rdi, %r15\n\tLONG $0x7daf0f4c; BYTE $0x20   // imulq\t32(%rbp), %r15\n\tLONG $0x185d8b4c               // movq\t24(%rbp), %r11\n\tLONG $0xbb3c8d4f               // leaq\t(%r11,%r15,4), %r15\n\tLONG $0x241c8b4c               // movq\t(%rsp), %r11                    # 8-byte Reload\n\tLONG $0x0410fac5; BYTE $0xbb   // vmovss\t(%rbx,%rdi,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x107a81c4; WORD $0x870c // vmovss\t(%r15,%r8,4), %xmm1             # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x247c8b4c; BYTE $0x20   // movq\t32(%rsp), %r15                  # 8-byte Reload\n\tLONG $0xa97982c4; WORD $0x870c // vfmadd213ss\t(%r15,%r8,4), %xmm0, %xmm1 # xmm1 = (xmm0 * xmm1) + mem\n\tLONG $0x117a81c4; WORD $0x870c // vmovss\t%xmm1, (%r15,%r8,4)\n\tWORD $0x894d; BYTE $0xc7       // movq\t%r8, %r15\n\tLONG $0x01cf8349               // orq\t$1, %r15\n\nLBB12_17:\n\tLONG $0x24443b4c; BYTE $0x10 // cmpq\t16(%rsp), %r8                   # 8-byte Folded Reload\n\tJE   LBB12_19\n\nLBB12_18:\n\tLONG $0x0410fac5; BYTE $0xbb               // vmovss\t(%rbx,%rdi,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x107a81c4; WORD $0xba0c             // vmovss\t(%r10,%r15,4), %xmm1            # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xa97982c4; WORD $0xb90c             // vfmadd213ss\t(%r9,%r15,4), %xmm0, %xmm1 # xmm1 = (xmm0 * xmm1) + mem\n\tLONG $0x117a81c4; WORD $0xb90c             // vmovss\t%xmm1, (%r9,%r15,4)\n\tLONG $0x0410fac5; BYTE $0xbb               // vmovss\t(%rbx,%rdi,4), %xmm0            # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x107a81c4; WORD $0xba4c; BYTE $0x04 // vmovss\t4(%r10,%r15,4), %xmm1           # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xa97982c4; WORD $0xb94c; BYTE $0x04 // vfmadd213ss\t4(%r9,%r15,4), %xmm0, %xmm1 # xmm1 = (xmm0 * xmm1) + mem\n\tLONG $0x117a81c4; WORD $0xb94c; BYTE $0x04 // vmovss\t%xmm1, 4(%r9,%r15,4)\n\tLONG $0x02c78349                           // addq\t$2, %r15\n\tWORD $0x394c; BYTE $0xf9                   // cmpq\t%r15, %rcx\n\tJNE  LBB12_18\n\tJMP  LBB12_19\n\nLBB12_22:\n\tWORD $0x8548; BYTE $0xd2     // testq\t%rdx, %rdx\n\tJLE  LBB12_140\n\tLONG $0x24148b48             // movq\t(%rsp), %rdx                    # 8-byte Reload\n\tLONG $0x0f428d48             // leaq\t15(%rdx), %rax\n\tWORD $0x8548; BYTE $0xd2     // testq\t%rdx, %rdx\n\tLONG $0xc2490f48             // cmovnsq\t%rdx, %rax\n\tWORD $0x8548; BYTE $0xc9     // testq\t%rcx, %rcx\n\tJLE  LBB12_140\n\tWORD $0x8948; BYTE $0xc6     // movq\t%rax, %rsi\n\tLONG $0x04fec148             // sarq\t$4, %rsi\n\tLONG $0xf0e08348             // andq\t$-16, %rax\n\tLONG $0x24248b4c             // movq\t(%rsp), %r12                    # 8-byte Reload\n\tWORD $0x2949; BYTE $0xc4     // subq\t%rax, %r12\n\tLONG $0x07fc8349             // cmpq\t$7, %r12\n\tJLE  LBB12_25\n\tLONG $0x247c8d41; BYTE $0xf8 // leal\t-8(%r12), %edi\n\tWORD $0xfe83; BYTE $0x01     // cmpl\t$1, %esi\n\tJLE  LBB12_30\n\tLONG $0x08fc8349             // cmpq\t$8, %r12\n\tJLE  LBB12_35\n\tLONG $0xff468d44             // leal\t-1(%rsi), %r8d\n\tWORD $0x468d; BYTE $0xfe     // leal\t-2(%rsi), %eax\n\tWORD $0x0489; BYTE $0x24     // movl\t%eax, (%rsp)                    # 4-byte Spill\n\tWORD $0x8944; BYTE $0xc0     // movl\t%r8d, %eax\n\tWORD $0xe083; BYTE $0xfc     // andl\t$-4, %eax\n\tLONG $0x20244489             // movl\t%eax, 32(%rsp)                  # 4-byte Spill\n\tLONG $0x10458b48             // movq\t16(%rbp), %rax\n\tQUAD $0x0000000085048d48     // leaq\t(,%rax,4), %rax\n\tLONG $0x24448948; BYTE $0x28 // movq\t%rax, 40(%rsp)                  # 8-byte Spill\n\tLONG $0x20458b48             // movq\t32(%rbp), %rax\n\tQUAD $0x00000000852c8d4c     // leaq\t(,%rax,4), %r13\n\tWORD $0xfe40; BYTE $0xce     // decb\t%sil\n\tLONG $0xf6b60f40             // movzbl\t%sil, %esi\n\tWORD $0xe683; BYTE $0x03     // andl\t$3, %esi\n\tWORD $0xe6c1; BYTE $0x06     // shll\t$6, %esi\n\tWORD $0x3145; BYTE $0xdb     // xorl\t%r11d, %r11d\n\tLONG $0x244c8b4c; BYTE $0x18 // movq\t24(%rsp), %r9                   # 8-byte Reload\n\tJMP  LBB12_40\n\nLBB12_58:\n\tLONG $0x245c8b4c; BYTE $0x30 // movq\t48(%rsp), %r11                  # 8-byte Reload\n\tWORD $0xff49; BYTE $0xc3     // incq\t%r11\n\tLONG $0x244c034c; BYTE $0x28 // addq\t40(%rsp), %r9                   # 8-byte Folded Reload\n\tLONG $0x245c3b4c; BYTE $0x38 // cmpq\t56(%rsp), %r11                  # 8-byte Folded Reload\n\tJE   LBB12_140\n\nLBB12_40:\n\tWORD $0x894c; BYTE $0xd8     // movq\t%r11, %rax\n\tLONG $0x45af0f48; BYTE $0x10 // imulq\t16(%rbp), %rax\n\tLONG $0x24548b4c; BYTE $0x18 // movq\t24(%rsp), %r10                  # 8-byte Reload\n\tLONG $0x82148d49             // leaq\t(%r10,%rax,4), %rdx\n\tLONG $0x82048d49             // leaq\t(%r10,%rax,4), %rax\n\tLONG $0x40c08348             // addq\t$64, %rax\n\tLONG $0x24448948; BYTE $0x10 // movq\t%rax, 16(%rsp)                  # 8-byte Spill\n\tLONG $0x245c894c; BYTE $0x30 // movq\t%r11, 48(%rsp)                  # 8-byte Spill\n\tWORD $0x894c; BYTE $0xd8     // movq\t%r11, %rax\n\tLONG $0x45af0f48; BYTE $0x30 // imulq\t48(%rbp), %rax\n\tLONG $0x28558b4c             // movq\t40(%rbp), %r10\n\tLONG $0x82248d4d             // leaq\t(%r10,%rax,4), %r12\n\tLONG $0x185d8b48             // movq\t24(%rbp), %rbx\n\tWORD $0xc031                 // xorl\t%eax, %eax\n\tJMP  LBB12_41\n\nLBB12_57:\n\tLONG $0x117ac1c4; WORD $0x8404 // vmovss\t%xmm0, (%r12,%rax,4)\n\tWORD $0xff48; BYTE $0xc0       // incq\t%rax\n\tWORD $0x014c; BYTE $0xeb       // addq\t%r13, %rbx\n\tWORD $0x3948; BYTE $0xc8       // cmpq\t%rcx, %rax\n\tJE   LBB12_58\n\nLBB12_41:\n\tWORD $0x8949; BYTE $0xc2                   // movq\t%rax, %r10\n\tLONG $0x55af0f4c; BYTE $0x20               // imulq\t32(%rbp), %r10\n\tLONG $0x487cf162; WORD $0x0210             // vmovups\t(%rdx), %zmm0\n\tLONG $0x185d8b4c                           // movq\t24(%rbp), %r11\n\tLONG $0x487c9162; WORD $0x0459; BYTE $0x93 // vmulps\t(%r11,%r10,4), %zmm0, %zmm0\n\tLONG $0x03243c83                           // cmpl\t$3, (%rsp)                      # 4-byte Folded Reload\n\tJAE  LBB12_49\n\tLONG $0x93348d4f                           // leaq\t(%r11,%r10,4), %r14\n\tLONG $0x40c68349                           // addq\t$64, %r14\n\tLONG $0x247c8b4c; BYTE $0x10               // movq\t16(%rsp), %r15                  # 8-byte Reload\n\tLONG $0x03c0f641                           // testb\t$3, %r8b\n\tJNE  LBB12_53\n\tJMP  LBB12_56\n\nLBB12_49:\n\tLONG $0x24548b44; BYTE $0x20 // movl\t32(%rsp), %r10d                 # 4-byte Reload\n\tWORD $0x3145; BYTE $0xf6     // xorl\t%r14d, %r14d\n\nLBB12_50:\n\tQUAD $0x01314c10487c9162                   // vmovups\t64(%r9,%r14), %zmm1\n\tQUAD $0x02315410487c9162                   // vmovups\t128(%r9,%r14), %zmm2\n\tQUAD $0x03315c10487c9162                   // vmovups\t192(%r9,%r14), %zmm3\n\tQUAD $0x01334c98487db262                   // vfmadd132ps\t64(%rbx,%r14), %zmm0, %zmm1 # zmm1 = (zmm1 * mem) + zmm0\n\tQUAD $0x02334cb8486db262                   // vfmadd231ps\t128(%rbx,%r14), %zmm2, %zmm1 # zmm1 = (zmm2 * mem) + zmm1\n\tQUAD $0x04315410487c9162                   // vmovups\t256(%r9,%r14), %zmm2\n\tQUAD $0x03334cb84865b262                   // vfmadd231ps\t192(%rbx,%r14), %zmm3, %zmm1 # zmm1 = (zmm3 * mem) + zmm1\n\tLONG $0x487cf162; WORD $0xc128             // vmovaps\t%zmm1, %zmm0\n\tQUAD $0x043344b8486db262                   // vfmadd231ps\t256(%rbx,%r14), %zmm2, %zmm0 # zmm0 = (zmm2 * mem) + zmm0\n\tLONG $0x00c68149; WORD $0x0001; BYTE $0x00 // addq\t$256, %r14                      # imm = 0x100\n\tLONG $0xfcc28341                           // addl\t$-4, %r10d\n\tJNE  LBB12_50\n\tLONG $0x31148d4f                           // leaq\t(%r9,%r14), %r10\n\tLONG $0x331c8d4e                           // leaq\t(%rbx,%r14), %r11\n\tLONG $0x313c8d4f                           // leaq\t(%r9,%r14), %r15\n\tLONG $0x40c78349                           // addq\t$64, %r15\n\tWORD $0x0149; BYTE $0xde                   // addq\t%rbx, %r14\n\tLONG $0x40c68349                           // addq\t$64, %r14\n\tLONG $0x03c0f641                           // testb\t$3, %r8b\n\tJE   LBB12_56\n\nLBB12_53:\n\tWORD $0x3145; BYTE $0xd2 // xorl\t%r10d, %r10d\n\tWORD $0x3145; BYTE $0xdb // xorl\t%r11d, %r11d\n\nLBB12_54:\n\tLONG $0x487cd162; WORD $0x0f10             // vmovups\t(%r15), %zmm1\n\tLONG $0x48759262; WORD $0x04b8; BYTE $0x16 // vfmadd231ps\t(%r14,%r10), %zmm1, %zmm0 # zmm0 = (zmm1 * mem) + zmm0\n\tLONG $0x40c78349                           // addq\t$64, %r15\n\tLONG $0xc0c38349                           // addq\t$-64, %r11\n\tLONG $0x40c28349                           // addq\t$64, %r10\n\tWORD $0x3944; BYTE $0xd6                   // cmpl\t%r10d, %esi\n\tJNE  LBB12_54\n\tLONG $0xc0578d4d                           // leaq\t-64(%r15), %r10\n\tWORD $0x294d; BYTE $0xde                   // subq\t%r11, %r14\n\tLONG $0xc05e8d4d                           // leaq\t-64(%r14), %r11\n\nLBB12_56:\n\tLONG $0x48fdf362; WORD $0xc11b; BYTE $0x01 // vextractf64x4\t$1, %zmm0, %ymm1\n\tLONG $0xc158fcc5                           // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0x197de3c4; WORD $0x01c1             // vextractf128\t$1, %ymm0, %xmm1\n\tLONG $0xc158f8c5                           // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0xc8c6f9c5; BYTE $0x01               // vshufpd\t$1, %xmm0, %xmm0, %xmm1         # xmm1 = xmm0[1,0]\n\tLONG $0xc158f8c5                           // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0x107cc1c4; BYTE $0x0f               // vmovups\t(%r15), %ymm1\n\tLONG $0x5974c1c4; BYTE $0x0e               // vmulps\t(%r14), %ymm1, %ymm1\n\tLONG $0x197de3c4; WORD $0x01ca             // vextractf128\t$1, %ymm1, %xmm2\n\tLONG $0xc958e8c5                           // vaddps\t%xmm1, %xmm2, %xmm1\n\tLONG $0xd1c6f1c5; BYTE $0x01               // vshufpd\t$1, %xmm1, %xmm1, %xmm2         # xmm2 = xmm1[1,0]\n\tLONG $0xca58f0c5                           // vaddps\t%xmm2, %xmm1, %xmm1\n\tLONG $0xd116fac5                           // vmovshdup\t%xmm1, %xmm2            # xmm2 = xmm1[1,1,3,3]\n\tLONG $0xca58f2c5                           // vaddss\t%xmm2, %xmm1, %xmm1\n\tLONG $0xd016fac5                           // vmovshdup\t%xmm0, %xmm2            # xmm2 = xmm0[1,1,3,3]\n\tLONG $0xc258fac5                           // vaddss\t%xmm2, %xmm0, %xmm0\n\tLONG $0xc958fac5                           // vaddss\t%xmm1, %xmm0, %xmm1\n\tLONG $0x107ac1c4; WORD $0x6042             // vmovss\t96(%r10), %xmm0                 # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x9971c2c4; WORD $0x6043             // vfmadd132ss\t96(%r11), %xmm1, %xmm0  # xmm0 = (xmm0 * mem) + xmm1\n\tWORD $0xff83; BYTE $0x01                   // cmpl\t$1, %edi\n\tJE   LBB12_57\n\tLONG $0x107ac1c4; WORD $0x644a             // vmovss\t100(%r10), %xmm1                # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971c2c4; WORD $0x6443             // vfmadd231ss\t100(%r11), %xmm1, %xmm0 # xmm0 = (xmm1 * mem) + xmm0\n\tWORD $0xff83; BYTE $0x02                   // cmpl\t$2, %edi\n\tJE   LBB12_57\n\tLONG $0x107ac1c4; WORD $0x684a             // vmovss\t104(%r10), %xmm1                # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971c2c4; WORD $0x6843             // vfmadd231ss\t104(%r11), %xmm1, %xmm0 # xmm0 = (xmm1 * mem) + xmm0\n\tWORD $0xff83; BYTE $0x03                   // cmpl\t$3, %edi\n\tJE   LBB12_57\n\tLONG $0x107ac1c4; WORD $0x6c4a             // vmovss\t108(%r10), %xmm1                # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971c2c4; WORD $0x6c43             // vfmadd231ss\t108(%r11), %xmm1, %xmm0 # xmm0 = (xmm1 * mem) + xmm0\n\tWORD $0xff83; BYTE $0x04                   // cmpl\t$4, %edi\n\tJE   LBB12_57\n\tLONG $0x107ac1c4; WORD $0x704a             // vmovss\t112(%r10), %xmm1                # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971c2c4; WORD $0x7043             // vfmadd231ss\t112(%r11), %xmm1, %xmm0 # xmm0 = (xmm1 * mem) + xmm0\n\tWORD $0xff83; BYTE $0x05                   // cmpl\t$5, %edi\n\tJE   LBB12_57\n\tLONG $0x107ac1c4; WORD $0x744a             // vmovss\t116(%r10), %xmm1                # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971c2c4; WORD $0x7443             // vfmadd231ss\t116(%r11), %xmm1, %xmm0 # xmm0 = (xmm1 * mem) + xmm0\n\tWORD $0xff83; BYTE $0x06                   // cmpl\t$6, %edi\n\tJE   LBB12_57\n\tLONG $0x107ac1c4; WORD $0x784a             // vmovss\t120(%r10), %xmm1                # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971c2c4; WORD $0x7843             // vfmadd231ss\t120(%r11), %xmm1, %xmm0 # xmm0 = (xmm1 * mem) + xmm0\n\tJMP  LBB12_57\n\nLBB12_99:\n\tWORD $0x8548; BYTE $0xd2                   // testq\t%rdx, %rdx\n\tWORD $0x9e0f; BYTE $0xc2                   // setle\t%dl\n\tWORD $0x0134                               // xorb\t$1, %al\n\tWORD $0xd008                               // orb\t%dl, %al\n\tJNE  LBB12_140\n\tLONG $0x30458b48                           // movq\t48(%rbp), %rax\n\tQUAD $0x00000000850c8d4c                   // leaq\t(,%rax,4), %r9\n\tLONG $0x24048b48                           // movq\t(%rsp), %rax                    # 8-byte Reload\n\tWORD $0xff48; BYTE $0xc8                   // decq\t%rax\n\tWORD $0x8948; BYTE $0xc2                   // movq\t%rax, %rdx\n\tLONG $0x55af0f48; BYTE $0x10               // imulq\t16(%rbp), %rdx\n\tLONG $0x20758b48                           // movq\t32(%rbp), %rsi\n\tLONG $0xc6af0f48                           // imulq\t%rsi, %rax\n\tWORD $0x0148; BYTE $0xc8                   // addq\t%rcx, %rax\n\tLONG $0x18458b4c                           // movq\t24(%rbp), %r8\n\tLONG $0x80048d49                           // leaq\t(%r8,%rax,4), %rax\n\tLONG $0x24448948; BYTE $0x58               // movq\t%rax, 88(%rsp)                  # 8-byte Spill\n\tLONG $0x287d8b48                           // movq\t40(%rbp), %rdi\n\tLONG $0x8f048d48                           // leaq\t(%rdi,%rcx,4), %rax\n\tLONG $0x24448948; BYTE $0x50               // movq\t%rax, 80(%rsp)                  # 8-byte Spill\n\tLONG $0x24448b48; BYTE $0x18               // movq\t24(%rsp), %rax                  # 8-byte Reload\n\tLONG $0x90448d48; BYTE $0x04               // leaq\t4(%rax,%rdx,4), %rax\n\tLONG $0x24448948; BYTE $0x48               // movq\t%rax, 72(%rsp)                  # 8-byte Spill\n\tQUAD $0xffffffffffc0bc49; WORD $0x7fff     // movabsq\t$9223372036854775744, %r12      # imm = 0x7FFFFFFFFFFFFFC0\n\tWORD $0x8949; BYTE $0xcf                   // movq\t%rcx, %r15\n\tWORD $0x214d; BYTE $0xe7                   // andq\t%r12, %r15\n\tLONG $0x38cc8349                           // orq\t$56, %r12\n\tWORD $0x2149; BYTE $0xcc                   // andq\t%rcx, %r12\n\tLONG $0xff418d48                           // leaq\t-1(%rcx), %rax\n\tLONG $0x24448948; BYTE $0x20               // movq\t%rax, 32(%rsp)                  # 8-byte Spill\n\tLONG $0xc0808d49; WORD $0x0000; BYTE $0x00 // leaq\t192(%r8), %rax\n\tLONG $0x24448948; BYTE $0x40               // movq\t%rax, 64(%rsp)                  # 8-byte Spill\n\tQUAD $0x00000000b52c8d4c                   // leaq\t(,%rsi,4), %r13\n\tLONG $0xc0878d48; WORD $0x0000; BYTE $0x00 // leaq\t192(%rdi), %rax\n\tWORD $0xdb31                               // xorl\t%ebx, %ebx\n\tLONG $0x244c894c; BYTE $0x60               // movq\t%r9, 96(%rsp)                   # 8-byte Spill\n\tJMP  LBB12_101\n\nLBB12_118:\n\tLONG $0x245c8b48; BYTE $0x28 // movq\t40(%rsp), %rbx                  # 8-byte Reload\n\tWORD $0xff48; BYTE $0xc3     // incq\t%rbx\n\tLONG $0x244c8b4c; BYTE $0x60 // movq\t96(%rsp), %r9                   # 8-byte Reload\n\tWORD $0x014c; BYTE $0xc8     // addq\t%r9, %rax\n\tWORD $0x014c; BYTE $0xcf     // addq\t%r9, %rdi\n\tLONG $0x245c3b48; BYTE $0x38 // cmpq\t56(%rsp), %rbx                  # 8-byte Folded Reload\n\tJE   LBB12_140\n\nLBB12_101:\n\tQUAD $0x000000000000ba48; WORD $0x2000 // movabsq\t$2305843009213693952, %rdx      # imm = 0x2000000000000000\n\tLONG $0x20558548                       // testq\t%rdx, 32(%rbp)\n\tLONG $0x2444950f; BYTE $0x10           // setne\t16(%rsp)                        # 1-byte Folded Spill\n\tLONG $0x10558548                       // testq\t%rdx, 16(%rbp)\n\tLONG $0xc6950f40                       // setne\t%sil\n\tWORD $0x894d; BYTE $0xc8               // movq\t%r9, %r8\n\tLONG $0xc3af0f4c                       // imulq\t%rbx, %r8\n\tLONG $0x28558b48                       // movq\t40(%rbp), %rdx\n\tLONG $0x02148d4e                       // leaq\t(%rdx,%r8), %r10\n\tLONG $0x2444034c; BYTE $0x50           // addq\t80(%rsp), %r8                   # 8-byte Folded Reload\n\tLONG $0x244c8b4c; BYTE $0x18           // movq\t24(%rsp), %r9                   # 8-byte Reload\n\tLONG $0x990c8d4d                       // leaq\t(%r9,%rbx,4), %r9\n\tLONG $0x245c8b4c; BYTE $0x48           // movq\t72(%rsp), %r11                  # 8-byte Reload\n\tLONG $0x9b1c8d4d                       // leaq\t(%r11,%rbx,4), %r11\n\tLONG $0x245c8948; BYTE $0x28           // movq\t%rbx, 40(%rsp)                  # 8-byte Spill\n\tLONG $0x5daf0f48; BYTE $0x30           // imulq\t48(%rbp), %rbx\n\tWORD $0x394d; BYTE $0xda               // cmpq\t%r11, %r10\n\tLONG $0xc3920f41                       // setb\t%r11b\n\tWORD $0x394d; BYTE $0xc1               // cmpq\t%r8, %r9\n\tLONG $0xc6920f41                       // setb\t%r14b\n\tWORD $0x2045; BYTE $0xde               // andb\t%r11b, %r14b\n\tLONG $0x9a148d48                       // leaq\t(%rdx,%rbx,4), %rdx\n\tLONG $0x24548948; BYTE $0x30           // movq\t%rdx, 48(%rsp)                  # 8-byte Spill\n\tWORD $0x0841; BYTE $0xf6               // orb\t%sil, %r14b\n\tLONG $0x24543b4c; BYTE $0x58           // cmpq\t88(%rsp), %r10                  # 8-byte Folded Reload\n\tLONG $0xc6920f40                       // setb\t%sil\n\tLONG $0x18558b4c                       // movq\t24(%rbp), %r10\n\tWORD $0x394d; BYTE $0xd0               // cmpq\t%r10, %r8\n\tWORD $0x970f; BYTE $0xc2               // seta\t%dl\n\tWORD $0x2040; BYTE $0xf2               // andb\t%sil, %dl\n\tLONG $0x1024540a                       // orb\t16(%rsp), %dl                   # 1-byte Folded Reload\n\tWORD $0x0844; BYTE $0xf2               // orb\t%r14b, %dl\n\tLONG $0x10245488                       // movb\t%dl, 16(%rsp)                   # 1-byte Spill\n\tLONG $0x24548b48; BYTE $0x40           // movq\t64(%rsp), %rdx                  # 8-byte Reload\n\tWORD $0xf631                           // xorl\t%esi, %esi\n\tLONG $0x241c8b4c                       // movq\t(%rsp), %r11                    # 8-byte Reload\n\tJMP  LBB12_102\n\nLBB12_117:\n\tWORD $0xff48; BYTE $0xc6 // incq\t%rsi\n\tWORD $0x014c; BYTE $0xea // addq\t%r13, %rdx\n\tWORD $0x014d; BYTE $0xea // addq\t%r13, %r10\n\tWORD $0x394c; BYTE $0xde // cmpq\t%r11, %rsi\n\tJE   LBB12_118\n\nLBB12_102:\n\tLONG $0x08f98348             // cmpq\t$8, %rcx\n\tWORD $0x920f; BYTE $0xc3     // setb\t%bl\n\tWORD $0x8949; BYTE $0xf0     // movq\t%rsi, %r8\n\tLONG $0x45af0f4c; BYTE $0x10 // imulq\t16(%rbp), %r8\n\tLONG $0x10245c0a             // orb\t16(%rsp), %bl                   # 1-byte Folded Reload\n\tWORD $0xc3f6; BYTE $0x01     // testb\t$1, %bl\n\tJE   LBB12_104\n\tWORD $0x3145; BYTE $0xf6     // xorl\t%r14d, %r14d\n\tJMP  LBB12_113\n\nLBB12_104:\n\tLONG $0x40f98348 // cmpq\t$64, %rcx\n\tJAE  LBB12_106\n\tWORD $0xdb31     // xorl\t%ebx, %ebx\n\tJMP  LBB12_110\n\nLBB12_106:\n\tLONG $0x487d9262; WORD $0x0418; BYTE $0x81 // vbroadcastss\t(%r9,%r8,4), %zmm0\n\tWORD $0xdb31                               // xorl\t%ebx, %ebx\n\nLBB12_107:\n\tQUAD $0xfd9a4c10487cf162                   // vmovups\t-192(%rdx,%rbx,4), %zmm1\n\tQUAD $0xfe9a5410487cf162                   // vmovups\t-128(%rdx,%rbx,4), %zmm2\n\tQUAD $0xff9a5c10487cf162                   // vmovups\t-64(%rdx,%rbx,4), %zmm3\n\tLONG $0x487cf162; WORD $0x2410; BYTE $0x9a // vmovups\t(%rdx,%rbx,4), %zmm4\n\tQUAD $0xfd984ca8487df262                   // vfmadd213ps\t-192(%rax,%rbx,4), %zmm0, %zmm1 # zmm1 = (zmm0 * zmm1) + mem\n\tQUAD $0xfe9854a8487df262                   // vfmadd213ps\t-128(%rax,%rbx,4), %zmm0, %zmm2 # zmm2 = (zmm0 * zmm2) + mem\n\tQUAD $0xff985ca8487df262                   // vfmadd213ps\t-64(%rax,%rbx,4), %zmm0, %zmm3 # zmm3 = (zmm0 * zmm3) + mem\n\tLONG $0x487df262; WORD $0x24a8; BYTE $0x98 // vfmadd213ps\t(%rax,%rbx,4), %zmm0, %zmm4 # zmm4 = (zmm0 * zmm4) + mem\n\tQUAD $0xfd984c11487cf162                   // vmovups\t%zmm1, -192(%rax,%rbx,4)\n\tQUAD $0xfe985411487cf162                   // vmovups\t%zmm2, -128(%rax,%rbx,4)\n\tQUAD $0xff985c11487cf162                   // vmovups\t%zmm3, -64(%rax,%rbx,4)\n\tLONG $0x487cf162; WORD $0x2411; BYTE $0x98 // vmovups\t%zmm4, (%rax,%rbx,4)\n\tLONG $0x40c38348                           // addq\t$64, %rbx\n\tWORD $0x3949; BYTE $0xdf                   // cmpq\t%rbx, %r15\n\tJNE  LBB12_107\n\tWORD $0x3949; BYTE $0xcf                   // cmpq\t%rcx, %r15\n\tJE   LBB12_117\n\tWORD $0x894c; BYTE $0xfb                   // movq\t%r15, %rbx\n\tWORD $0x894d; BYTE $0xfe                   // movq\t%r15, %r14\n\tWORD $0xc1f6; BYTE $0x38                   // testb\t$56, %cl\n\tJE   LBB12_113\n\nLBB12_110:\n\tLONG $0x187d82c4; WORD $0x8104 // vbroadcastss\t(%r9,%r8,4), %ymm0\n\nLBB12_111:\n\tLONG $0x107cc1c4; WORD $0x9a0c // vmovups\t(%r10,%rbx,4), %ymm1\n\tLONG $0xa87de2c4; WORD $0x9f0c // vfmadd213ps\t(%rdi,%rbx,4), %ymm0, %ymm1 # ymm1 = (ymm0 * ymm1) + mem\n\tLONG $0x0c11fcc5; BYTE $0x9f   // vmovups\t%ymm1, (%rdi,%rbx,4)\n\tLONG $0x08c38348               // addq\t$8, %rbx\n\tWORD $0x3949; BYTE $0xdc       // cmpq\t%rbx, %r12\n\tJNE  LBB12_111\n\tWORD $0x894d; BYTE $0xe6       // movq\t%r12, %r14\n\tWORD $0x3949; BYTE $0xcc       // cmpq\t%rcx, %r12\n\tJE   LBB12_117\n\nLBB12_113:\n\tWORD $0x894c; BYTE $0xf3       // movq\t%r14, %rbx\n\tWORD $0xc1f6; BYTE $0x01       // testb\t$1, %cl\n\tJE   LBB12_115\n\tWORD $0x8948; BYTE $0xf3       // movq\t%rsi, %rbx\n\tLONG $0x5daf0f48; BYTE $0x20   // imulq\t32(%rbp), %rbx\n\tLONG $0x185d8b4c               // movq\t24(%rbp), %r11\n\tLONG $0x9b1c8d49               // leaq\t(%r11,%rbx,4), %rbx\n\tLONG $0x241c8b4c               // movq\t(%rsp), %r11                    # 8-byte Reload\n\tLONG $0x107a81c4; WORD $0x8104 // vmovss\t(%r9,%r8,4), %xmm0              # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x107aa1c4; WORD $0xb30c // vmovss\t(%rbx,%r14,4), %xmm1            # xmm1 = mem[0],zero,zero,zero\n\tLONG $0x245c8b48; BYTE $0x30   // movq\t48(%rsp), %rbx                  # 8-byte Reload\n\tLONG $0xa979a2c4; WORD $0xb30c // vfmadd213ss\t(%rbx,%r14,4), %xmm0, %xmm1 # xmm1 = (xmm0 * xmm1) + mem\n\tLONG $0x117aa1c4; WORD $0xb30c // vmovss\t%xmm1, (%rbx,%r14,4)\n\tWORD $0x894c; BYTE $0xf3       // movq\t%r14, %rbx\n\tLONG $0x01cb8348               // orq\t$1, %rbx\n\nLBB12_115:\n\tLONG $0x24743b4c; BYTE $0x20 // cmpq\t32(%rsp), %r14                  # 8-byte Folded Reload\n\tJE   LBB12_117\n\nLBB12_116:\n\tLONG $0x107a81c4; WORD $0x8104             // vmovss\t(%r9,%r8,4), %xmm0              # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x107ac1c4; WORD $0x9a0c             // vmovss\t(%r10,%rbx,4), %xmm1            # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xa979e2c4; WORD $0x9f0c             // vfmadd213ss\t(%rdi,%rbx,4), %xmm0, %xmm1 # xmm1 = (xmm0 * xmm1) + mem\n\tLONG $0x0c11fac5; BYTE $0x9f               // vmovss\t%xmm1, (%rdi,%rbx,4)\n\tLONG $0x107a81c4; WORD $0x8104             // vmovss\t(%r9,%r8,4), %xmm0              # xmm0 = mem[0],zero,zero,zero\n\tLONG $0x107ac1c4; WORD $0x9a4c; BYTE $0x04 // vmovss\t4(%r10,%rbx,4), %xmm1           # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xa979e2c4; WORD $0x9f4c; BYTE $0x04 // vfmadd213ss\t4(%rdi,%rbx,4), %xmm0, %xmm1 # xmm1 = (xmm0 * xmm1) + mem\n\tLONG $0x4c11fac5; WORD $0x049f             // vmovss\t%xmm1, 4(%rdi,%rbx,4)\n\tLONG $0x02c38348                           // addq\t$2, %rbx\n\tWORD $0x3948; BYTE $0xd9                   // cmpq\t%rbx, %rcx\n\tJNE  LBB12_116\n\tJMP  LBB12_117\n\nLBB12_25:\n\tWORD $0x8948; BYTE $0xf0               // movq\t%rsi, %rax\n\tLONG $0x06e0c148                       // shlq\t$6, %rax\n\tQUAD $0x003fffffff80ba48; WORD $0x0000 // movabsq\t$274877906816, %rdx             # imm = 0x3FFFFFFF80\n\tWORD $0x0148; BYTE $0xd0               // addq\t%rdx, %rax\n\tLONG $0x40ca8348                       // orq\t$64, %rdx\n\tWORD $0x2148; BYTE $0xc2               // andq\t%rax, %rdx\n\tLONG $0x40c28348                       // addq\t$64, %rdx\n\tLONG $0x24148948                       // movq\t%rdx, (%rsp)                    # 8-byte Spill\n\tWORD $0x894c; BYTE $0xe7               // movq\t%r12, %rdi\n\tWORD $0x8944; BYTE $0xe2               // movl\t%r12d, %edx\n\tWORD $0x468d; BYTE $0xff               // leal\t-1(%rsi), %eax\n\tLONG $0xfe468d44                       // leal\t-2(%rsi), %r8d\n\tLONG $0x24448944; BYTE $0x20           // movl\t%r8d, 32(%rsp)                  # 4-byte Spill\n\tLONG $0x10244489                       // movl\t%eax, 16(%rsp)                  # 4-byte Spill\n\tWORD $0xe083; BYTE $0xfc               // andl\t$-4, %eax\n\tLONG $0x30244489                       // movl\t%eax, 48(%rsp)                  # 4-byte Spill\n\tWORD $0xf089                           // movl\t%esi, %eax\n\tWORD $0xc8fe                           // decb\t%al\n\tLONG $0xe8b60f44                       // movzbl\t%al, %r13d\n\tLONG $0x03e58341                       // andl\t$3, %r13d\n\tLONG $0x06e5c141                       // shll\t$6, %r13d\n\tWORD $0x3145; BYTE $0xc9               // xorl\t%r9d, %r9d\n\tJMP  LBB12_26\n\nLBB12_89:\n\tLONG $0x244c8b4c; BYTE $0x28 // movq\t40(%rsp), %r9                   # 8-byte Reload\n\tWORD $0xff49; BYTE $0xc1     // incq\t%r9\n\tLONG $0x244c3b4c; BYTE $0x38 // cmpq\t56(%rsp), %r9                   # 8-byte Folded Reload\n\tJE   LBB12_140\n\nLBB12_26:\n\tWORD $0x894c; BYTE $0xc8     // movq\t%r9, %rax\n\tLONG $0x45af0f48; BYTE $0x10 // imulq\t16(%rbp), %rax\n\tLONG $0x24448b4c; BYTE $0x18 // movq\t24(%rsp), %r8                   # 8-byte Reload\n\tLONG $0x801c8d49             // leaq\t(%r8,%rax,4), %rbx\n\tLONG $0x80348d4d             // leaq\t(%r8,%rax,4), %r14\n\tLONG $0x40c68349             // addq\t$64, %r14\n\tLONG $0x244c894c; BYTE $0x28 // movq\t%r9, 40(%rsp)                   # 8-byte Spill\n\tWORD $0x894c; BYTE $0xc8     // movq\t%r9, %rax\n\tLONG $0x45af0f48; BYTE $0x30 // imulq\t48(%rbp), %rax\n\tLONG $0x28458b4c             // movq\t40(%rbp), %r8\n\tLONG $0x80048d49             // leaq\t(%r8,%rax,4), %rax\n\tLONG $0x24048b4c             // movq\t(%rsp), %r8                     # 8-byte Reload\n\tLONG $0x183c8d4d             // leaq\t(%r8,%rbx), %r15\n\tLONG $0x40c78349             // addq\t$64, %r15\n\tWORD $0x3145; BYTE $0xc9     // xorl\t%r9d, %r9d\n\tLONG $0x185d8b4c             // movq\t24(%rbp), %r11\n\tJMP  LBB12_27\n\nLBB12_88:\n\tLONG $0x117aa1c4; WORD $0x8804 // vmovss\t%xmm0, (%rax,%r9,4)\n\tWORD $0xff49; BYTE $0xc1       // incq\t%r9\n\tWORD $0x3949; BYTE $0xc9       // cmpq\t%rcx, %r9\n\tJE   LBB12_89\n\nLBB12_27:\n\tWORD $0x894d; BYTE $0xc8       // movq\t%r9, %r8\n\tLONG $0x45af0f4c; BYTE $0x20   // imulq\t32(%rbp), %r8\n\tLONG $0x83048d4f               // leaq\t(%r11,%r8,4), %r8\n\tWORD $0xf685                   // testl\t%esi, %esi\n\tJLE  LBB12_28\n\tLONG $0x40508d4d               // leaq\t64(%r8), %r10\n\tLONG $0x487cf162; WORD $0x0310 // vmovups\t(%rbx), %zmm0\n\tLONG $0x487cd162; WORD $0x0859 // vmulps\t(%r8), %zmm0, %zmm1\n\tWORD $0xfe83; BYTE $0x02       // cmpl\t$2, %esi\n\tJL   LBB12_81\n\tWORD $0x894d; BYTE $0xf0       // movq\t%r14, %r8\n\tWORD $0x894d; BYTE $0xd3       // movq\t%r10, %r11\n\tLONG $0x24648b44; BYTE $0x30   // movl\t48(%rsp), %r12d                 # 4-byte Reload\n\tLONG $0x20247c83; BYTE $0x03   // cmpl\t$3, 32(%rsp)                    # 4-byte Folded Reload\n\tJB   LBB12_83\n\nLBB12_97:\n\tLONG $0x487cd162; WORD $0x0010             // vmovups\t(%r8), %zmm0\n\tLONG $0x487cd162; WORD $0x5010; BYTE $0x01 // vmovups\t64(%r8), %zmm2\n\tLONG $0x487cd162; WORD $0x5810; BYTE $0x02 // vmovups\t128(%r8), %zmm3\n\tLONG $0x487cd162; WORD $0x6010; BYTE $0x03 // vmovups\t192(%r8), %zmm4\n\tLONG $0x4875d262; WORD $0x0398             // vfmadd132ps\t(%r11), %zmm1, %zmm0    # zmm0 = (zmm0 * mem) + zmm1\n\tLONG $0x486dd262; WORD $0x43b8; BYTE $0x01 // vfmadd231ps\t64(%r11), %zmm2, %zmm0  # zmm0 = (zmm2 * mem) + zmm0\n\tLONG $0x4865d262; WORD $0x43b8; BYTE $0x02 // vfmadd231ps\t128(%r11), %zmm3, %zmm0 # zmm0 = (zmm3 * mem) + zmm0\n\tLONG $0x485dd262; WORD $0x43b8; BYTE $0x03 // vfmadd231ps\t192(%r11), %zmm4, %zmm0 # zmm0 = (zmm4 * mem) + zmm0\n\tLONG $0x00c08149; WORD $0x0001; BYTE $0x00 // addq\t$256, %r8                       # imm = 0x100\n\tLONG $0x00c38149; WORD $0x0001; BYTE $0x00 // addq\t$256, %r11                      # imm = 0x100\n\tLONG $0x487cf162; WORD $0xc828             // vmovaps\t%zmm0, %zmm1\n\tLONG $0xfcc48341                           // addl\t$-4, %r12d\n\tJNE  LBB12_97\n\nLBB12_83:\n\tLONG $0x102444f6; BYTE $0x03   // testb\t$3, 16(%rsp)                    # 1-byte Folded Reload\n\tJE   LBB12_86\n\tWORD $0x3145; BYTE $0xe4       // xorl\t%r12d, %r12d\n\tLONG $0x487cf162; WORD $0xc128 // vmovaps\t%zmm1, %zmm0\n\nLBB12_85:\n\tLONG $0x487c9162; WORD $0x0c10; BYTE $0x20 // vmovups\t(%r8,%r12), %zmm1\n\tLONG $0x48759262; WORD $0x04b8; BYTE $0x23 // vfmadd231ps\t(%r11,%r12), %zmm1, %zmm0 # zmm0 = (zmm1 * mem) + zmm0\n\tLONG $0x40c48349                           // addq\t$64, %r12\n\tWORD $0x3945; BYTE $0xe5                   // cmpl\t%r12d, %r13d\n\tJNE  LBB12_85\n\nLBB12_86:\n\tLONG $0x2414034c               // addq\t(%rsp), %r10                    # 8-byte Folded Reload\n\tLONG $0x487cf162; WORD $0xc828 // vmovaps\t%zmm0, %zmm1\n\tWORD $0x894d; BYTE $0xd0       // movq\t%r10, %r8\n\tWORD $0x894d; BYTE $0xfa       // movq\t%r15, %r10\n\tLONG $0x185d8b4c               // movq\t24(%rbp), %r11\n\tJMP  LBB12_87\n\nLBB12_28:\n\tLONG $0xc957f0c5         // vxorps\t%xmm1, %xmm1, %xmm1\n\tWORD $0x8949; BYTE $0xda // movq\t%rbx, %r10\n\tJMP  LBB12_87\n\nLBB12_81:\n\tWORD $0x894d; BYTE $0xd0 // movq\t%r10, %r8\n\tWORD $0x894d; BYTE $0xf2 // movq\t%r14, %r10\n\nLBB12_87:\n\tLONG $0x48fdf362; WORD $0xc81b; BYTE $0x01 // vextractf64x4\t$1, %zmm1, %ymm0\n\tLONG $0xc058f4c5                           // vaddps\t%ymm0, %ymm1, %ymm0\n\tLONG $0x197de3c4; WORD $0x01c1             // vextractf128\t$1, %ymm0, %xmm1\n\tLONG $0xc158f8c5                           // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0xc8c6f9c5; BYTE $0x01               // vshufpd\t$1, %xmm0, %xmm0, %xmm1         # xmm1 = xmm0[1,0]\n\tLONG $0xc158f8c5                           // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0xc816fac5                           // vmovshdup\t%xmm0, %xmm1            # xmm1 = xmm0[1,1,3,3]\n\tLONG $0xc158fac5                           // vaddss\t%xmm1, %xmm0, %xmm0\n\tWORD $0x8548; BYTE $0xff                   // testq\t%rdi, %rdi\n\tJLE  LBB12_88\n\tLONG $0x107ac1c4; BYTE $0x0a               // vmovss\t(%r10), %xmm1                   # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971c2c4; BYTE $0x00               // vfmadd231ss\t(%r8), %xmm1, %xmm0     # xmm0 = (xmm1 * mem) + xmm0\n\tWORD $0xfa83; BYTE $0x01                   // cmpl\t$1, %edx\n\tJE   LBB12_88\n\tLONG $0x107ac1c4; WORD $0x044a             // vmovss\t4(%r10), %xmm1                  # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971c2c4; WORD $0x0440             // vfmadd231ss\t4(%r8), %xmm1, %xmm0    # xmm0 = (xmm1 * mem) + xmm0\n\tWORD $0xfa83; BYTE $0x02                   // cmpl\t$2, %edx\n\tJE   LBB12_88\n\tLONG $0x107ac1c4; WORD $0x084a             // vmovss\t8(%r10), %xmm1                  # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971c2c4; WORD $0x0840             // vfmadd231ss\t8(%r8), %xmm1, %xmm0    # xmm0 = (xmm1 * mem) + xmm0\n\tWORD $0xfa83; BYTE $0x03                   // cmpl\t$3, %edx\n\tJE   LBB12_88\n\tLONG $0x107ac1c4; WORD $0x0c4a             // vmovss\t12(%r10), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971c2c4; WORD $0x0c40             // vfmadd231ss\t12(%r8), %xmm1, %xmm0   # xmm0 = (xmm1 * mem) + xmm0\n\tWORD $0xfa83; BYTE $0x04                   // cmpl\t$4, %edx\n\tJE   LBB12_88\n\tLONG $0x107ac1c4; WORD $0x104a             // vmovss\t16(%r10), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971c2c4; WORD $0x1040             // vfmadd231ss\t16(%r8), %xmm1, %xmm0   # xmm0 = (xmm1 * mem) + xmm0\n\tWORD $0xfa83; BYTE $0x05                   // cmpl\t$5, %edx\n\tJE   LBB12_88\n\tLONG $0x107ac1c4; WORD $0x144a             // vmovss\t20(%r10), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971c2c4; WORD $0x1440             // vfmadd231ss\t20(%r8), %xmm1, %xmm0   # xmm0 = (xmm1 * mem) + xmm0\n\tWORD $0xfa83; BYTE $0x06                   // cmpl\t$6, %edx\n\tJE   LBB12_88\n\tLONG $0x107ac1c4; WORD $0x184a             // vmovss\t24(%r10), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971c2c4; WORD $0x1840             // vfmadd231ss\t24(%r8), %xmm1, %xmm0   # xmm0 = (xmm1 * mem) + xmm0\n\tJMP  LBB12_88\n\nLBB12_30:\n\tLONG $0x20458b48               // movq\t32(%rbp), %rax\n\tQUAD $0x0000000085048d48       // leaq\t(,%rax,4), %rax\n\tLONG $0x2464c148; WORD $0x0270 // shlq\t$2, 112(%rsp)                   # 8-byte Folded Spill\n\tWORD $0xd231                   // xorl\t%edx, %edx\n\tJMP  LBB12_31\n\nLBB12_72:\n\tWORD $0xff48; BYTE $0xc2     // incq\t%rdx\n\tLONG $0x24448b4c; BYTE $0x68 // movq\t104(%rsp), %r8                  # 8-byte Reload\n\tLONG $0x2444034c; BYTE $0x70 // addq\t112(%rsp), %r8                  # 8-byte Folded Reload\n\tLONG $0x2444894c; BYTE $0x68 // movq\t%r8, 104(%rsp)                  # 8-byte Spill\n\tLONG $0x24543b48; BYTE $0x38 // cmpq\t56(%rsp), %rdx                  # 8-byte Folded Reload\n\tJE   LBB12_140\n\nLBB12_31:\n\tWORD $0x8949; BYTE $0xd1     // movq\t%rdx, %r9\n\tLONG $0x4daf0f4c; BYTE $0x10 // imulq\t16(%rbp), %r9\n\tLONG $0x24548b4c; BYTE $0x18 // movq\t24(%rsp), %r10                  # 8-byte Reload\n\tLONG $0x8a048d4f             // leaq\t(%r10,%r9,4), %r8\n\tLONG $0x8a0c8d4f             // leaq\t(%r10,%r9,4), %r9\n\tLONG $0x40c18349             // addq\t$64, %r9\n\tLONG $0x187d8b4c             // movq\t24(%rbp), %r15\n\tWORD $0x894d; BYTE $0xfa     // movq\t%r15, %r10\n\tWORD $0x3145; BYTE $0xdb     // xorl\t%r11d, %r11d\n\tJMP  LBB12_32\n\nLBB12_71:\n\tLONG $0x245c8b48; BYTE $0x68   // movq\t104(%rsp), %rbx                 # 8-byte Reload\n\tLONG $0x117aa1c4; WORD $0x9b04 // vmovss\t%xmm0, (%rbx,%r11,4)\n\tWORD $0xff49; BYTE $0xc3       // incq\t%r11\n\tWORD $0x0149; BYTE $0xc2       // addq\t%rax, %r10\n\tWORD $0x394c; BYTE $0xd9       // cmpq\t%r11, %rcx\n\tJE   LBB12_72\n\nLBB12_32:\n\tWORD $0xf685                   // testl\t%esi, %esi\n\tJLE  LBB12_33\n\tWORD $0x894c; BYTE $0xdb       // movq\t%r11, %rbx\n\tLONG $0x5daf0f48; BYTE $0x20   // imulq\t32(%rbp), %rbx\n\tLONG $0x9f1c8d49               // leaq\t(%r15,%rbx,4), %rbx\n\tLONG $0x487cd162; WORD $0x0010 // vmovups\t(%r8), %zmm0\n\tLONG $0x487cd162; WORD $0x0259 // vmulps\t(%r10), %zmm0, %zmm0\n\tLONG $0x40c38348               // addq\t$64, %rbx\n\tWORD $0x894d; BYTE $0xce       // movq\t%r9, %r14\n\tJMP  LBB12_70\n\nLBB12_33:\n\tLONG $0xc057f8c5         // vxorps\t%xmm0, %xmm0, %xmm0\n\tWORD $0x894c; BYTE $0xd3 // movq\t%r10, %rbx\n\tWORD $0x894d; BYTE $0xc6 // movq\t%r8, %r14\n\nLBB12_70:\n\tLONG $0x48fdf362; WORD $0xc11b; BYTE $0x01 // vextractf64x4\t$1, %zmm0, %ymm1\n\tLONG $0xc158fcc5                           // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0x197de3c4; WORD $0x01c1             // vextractf128\t$1, %ymm0, %xmm1\n\tLONG $0xc158f8c5                           // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0xc8c6f9c5; BYTE $0x01               // vshufpd\t$1, %xmm0, %xmm0, %xmm1         # xmm1 = xmm0[1,0]\n\tLONG $0xc158f8c5                           // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0x107cc1c4; BYTE $0x0e               // vmovups\t(%r14), %ymm1\n\tLONG $0x0b59f4c5                           // vmulps\t(%rbx), %ymm1, %ymm1\n\tLONG $0x197de3c4; WORD $0x01ca             // vextractf128\t$1, %ymm1, %xmm2\n\tLONG $0xc958e8c5                           // vaddps\t%xmm1, %xmm2, %xmm1\n\tLONG $0xd1c6f1c5; BYTE $0x01               // vshufpd\t$1, %xmm1, %xmm1, %xmm2         # xmm2 = xmm1[1,0]\n\tLONG $0xca58f0c5                           // vaddps\t%xmm2, %xmm1, %xmm1\n\tLONG $0xd116fac5                           // vmovshdup\t%xmm1, %xmm2            # xmm2 = xmm1[1,1,3,3]\n\tLONG $0xca58f2c5                           // vaddss\t%xmm2, %xmm1, %xmm1\n\tLONG $0xd016fac5                           // vmovshdup\t%xmm0, %xmm2            # xmm2 = xmm0[1,1,3,3]\n\tLONG $0xc258fac5                           // vaddss\t%xmm2, %xmm0, %xmm0\n\tLONG $0xc158fac5                           // vaddss\t%xmm1, %xmm0, %xmm0\n\tLONG $0x08fc8349                           // cmpq\t$8, %r12\n\tJLE  LBB12_71\n\tLONG $0x107ac1c4; WORD $0x204e             // vmovss\t32(%r14), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971e2c4; WORD $0x2043             // vfmadd231ss\t32(%rbx), %xmm1, %xmm0  # xmm0 = (xmm1 * mem) + xmm0\n\tWORD $0xff83; BYTE $0x01                   // cmpl\t$1, %edi\n\tJE   LBB12_71\n\tLONG $0x107ac1c4; WORD $0x244e             // vmovss\t36(%r14), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971e2c4; WORD $0x2443             // vfmadd231ss\t36(%rbx), %xmm1, %xmm0  # xmm0 = (xmm1 * mem) + xmm0\n\tWORD $0xff83; BYTE $0x02                   // cmpl\t$2, %edi\n\tJE   LBB12_71\n\tLONG $0x107ac1c4; WORD $0x284e             // vmovss\t40(%r14), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971e2c4; WORD $0x2843             // vfmadd231ss\t40(%rbx), %xmm1, %xmm0  # xmm0 = (xmm1 * mem) + xmm0\n\tWORD $0xff83; BYTE $0x03                   // cmpl\t$3, %edi\n\tJE   LBB12_71\n\tLONG $0x107ac1c4; WORD $0x2c4e             // vmovss\t44(%r14), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971e2c4; WORD $0x2c43             // vfmadd231ss\t44(%rbx), %xmm1, %xmm0  # xmm0 = (xmm1 * mem) + xmm0\n\tWORD $0xff83; BYTE $0x04                   // cmpl\t$4, %edi\n\tJE   LBB12_71\n\tLONG $0x107ac1c4; WORD $0x304e             // vmovss\t48(%r14), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971e2c4; WORD $0x3043             // vfmadd231ss\t48(%rbx), %xmm1, %xmm0  # xmm0 = (xmm1 * mem) + xmm0\n\tWORD $0xff83; BYTE $0x05                   // cmpl\t$5, %edi\n\tJE   LBB12_71\n\tLONG $0x107ac1c4; WORD $0x344e             // vmovss\t52(%r14), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971e2c4; WORD $0x3443             // vfmadd231ss\t52(%rbx), %xmm1, %xmm0  # xmm0 = (xmm1 * mem) + xmm0\n\tWORD $0xff83; BYTE $0x06                   // cmpl\t$6, %edi\n\tJE   LBB12_71\n\tLONG $0x107ac1c4; WORD $0x384e             // vmovss\t56(%r14), %xmm1                 # xmm1 = mem[0],zero,zero,zero\n\tLONG $0xb971e2c4; WORD $0x3843             // vfmadd231ss\t56(%rbx), %xmm1, %xmm0  # xmm0 = (xmm1 * mem) + xmm0\n\tJMP  LBB12_71\n\nLBB12_35:\n\tWORD $0x8948; BYTE $0xf2               // movq\t%rsi, %rdx\n\tLONG $0x06e2c148                       // shlq\t$6, %rdx\n\tQUAD $0x003fffffff80b848; WORD $0x0000 // movabsq\t$274877906816, %rax             # imm = 0x3FFFFFFF80\n\tWORD $0x0148; BYTE $0xc2               // addq\t%rax, %rdx\n\tLONG $0x40c88348                       // orq\t$64, %rax\n\tWORD $0x2148; BYTE $0xd0               // andq\t%rdx, %rax\n\tWORD $0x568d; BYTE $0xff               // leal\t-1(%rsi), %edx\n\tWORD $0x7e8d; BYTE $0xfe               // leal\t-2(%rsi), %edi\n\tWORD $0x8941; BYTE $0xd0               // movl\t%edx, %r8d\n\tLONG $0xfce08341                       // andl\t$-4, %r8d\n\tLONG $0x24448944; BYTE $0x10           // movl\t%r8d, 16(%rsp)                  # 4-byte Spill\n\tWORD $0x8941; BYTE $0xf0               // movl\t%esi, %r8d\n\tWORD $0xfe41; BYTE $0xc8               // decb\t%r8b\n\tLONG $0xc8b60f45                       // movzbl\t%r8b, %r9d\n\tLONG $0x03e18341                       // andl\t$3, %r9d\n\tLONG $0x06e1c141                       // shll\t$6, %r9d\n\tWORD $0xdb31                           // xorl\t%ebx, %ebx\n\tJMP  LBB12_36\n\nLBB12_68:\n\tLONG $0x245c8b48; BYTE $0x20 // movq\t32(%rsp), %rbx                  # 8-byte Reload\n\tWORD $0xff48; BYTE $0xc3     // incq\t%rbx\n\tLONG $0x245c3b48; BYTE $0x38 // cmpq\t56(%rsp), %rbx                  # 8-byte Folded Reload\n\tJE   LBB12_140\n\nLBB12_36:\n\tWORD $0x8949; BYTE $0xd8     // movq\t%rbx, %r8\n\tLONG $0x45af0f4c; BYTE $0x10 // imulq\t16(%rbp), %r8\n\tLONG $0x245c8b4c; BYTE $0x18 // movq\t24(%rsp), %r11                  # 8-byte Reload\n\tLONG $0x83148d4f             // leaq\t(%r11,%r8,4), %r10\n\tLONG $0x83048d4f             // leaq\t(%r11,%r8,4), %r8\n\tLONG $0x40c08349             // addq\t$64, %r8\n\tLONG $0x2404894c             // movq\t%r8, (%rsp)                     # 8-byte Spill\n\tLONG $0x245c8948; BYTE $0x20 // movq\t%rbx, 32(%rsp)                  # 8-byte Spill\n\tWORD $0x8949; BYTE $0xd8     // movq\t%rbx, %r8\n\tLONG $0x45af0f4c; BYTE $0x30 // imulq\t48(%rbp), %r8\n\tLONG $0x285d8b4c             // movq\t40(%rbp), %r11\n\tLONG $0x83348d4f             // leaq\t(%r11,%r8,4), %r14\n\tWORD $0x3145; BYTE $0xff     // xorl\t%r15d, %r15d\n\tJMP  LBB12_37\n\nLBB12_67:\n\tLONG $0x48fdf362; WORD $0xc11b; BYTE $0x01 // vextractf64x4\t$1, %zmm0, %ymm1\n\tLONG $0xc158fcc5                           // vaddps\t%ymm1, %ymm0, %ymm0\n\tLONG $0x197de3c4; WORD $0x01c1             // vextractf128\t$1, %ymm0, %xmm1\n\tLONG $0xc158f8c5                           // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0xc8c6f9c5; BYTE $0x01               // vshufpd\t$1, %xmm0, %xmm0, %xmm1         # xmm1 = xmm0[1,0]\n\tLONG $0xc158f8c5                           // vaddps\t%xmm1, %xmm0, %xmm0\n\tLONG $0x4c10fcc5; WORD $0x4003             // vmovups\t64(%rbx,%rax), %ymm1\n\tLONG $0x5974c1c4; WORD $0x004c; BYTE $0x40 // vmulps\t64(%r8,%rax), %ymm1, %ymm1\n\tLONG $0x197de3c4; WORD $0x01ca             // vextractf128\t$1, %ymm1, %xmm2\n\tLONG $0xc958e8c5                           // vaddps\t%xmm1, %xmm2, %xmm1\n\tLONG $0xd1c6f1c5; BYTE $0x01               // vshufpd\t$1, %xmm1, %xmm1, %xmm2         # xmm2 = xmm1[1,0]\n\tLONG $0xca58f0c5                           // vaddps\t%xmm2, %xmm1, %xmm1\n\tLONG $0xd116fac5                           // vmovshdup\t%xmm1, %xmm2            # xmm2 = xmm1[1,1,3,3]\n\tLONG $0xca58f2c5                           // vaddss\t%xmm2, %xmm1, %xmm1\n\tLONG $0xd016fac5                           // vmovshdup\t%xmm0, %xmm2            # xmm2 = xmm0[1,1,3,3]\n\tLONG $0xc258fac5                           // vaddss\t%xmm2, %xmm0, %xmm0\n\tLONG $0xc158fac5                           // vaddss\t%xmm1, %xmm0, %xmm0\n\tLONG $0x117a81c4; WORD $0xbe04             // vmovss\t%xmm0, (%r14,%r15,4)\n\tWORD $0xff49; BYTE $0xc7                   // incq\t%r15\n\tWORD $0x3949; BYTE $0xcf                   // cmpq\t%rcx, %r15\n\tJE   LBB12_68\n\nLBB12_37:\n\tWORD $0x894d; BYTE $0xf8       // movq\t%r15, %r8\n\tLONG $0x45af0f4c; BYTE $0x20   // imulq\t32(%rbp), %r8\n\tLONG $0x185d8b4c               // movq\t24(%rbp), %r11\n\tLONG $0x83048d4f               // leaq\t(%r11,%r8,4), %r8\n\tWORD $0xf685                   // testl\t%esi, %esi\n\tJLE  LBB12_38\n\tLONG $0x487cd162; WORD $0x0210 // vmovups\t(%r10), %zmm0\n\tLONG $0x487cd162; WORD $0x0859 // vmulps\t(%r8), %zmm0, %zmm1\n\tLONG $0x40c08349               // addq\t$64, %r8\n\tLONG $0x241c8b48               // movq\t(%rsp), %rbx                    # 8-byte Reload\n\tWORD $0xff83; BYTE $0x03       // cmpl\t$3, %edi\n\tJAE  LBB12_62\n\nLBB12_61:\n\tWORD $0x8949; BYTE $0xdd       // movq\t%rbx, %r13\n\tWORD $0x894d; BYTE $0xc4       // movq\t%r8, %r12\n\tLONG $0x487cf162; WORD $0xc128 // vmovaps\t%zmm1, %zmm0\n\tJMP  LBB12_64\n\nLBB12_38:\n\tLONG $0xc957f0c5         // vxorps\t%xmm1, %xmm1, %xmm1\n\tWORD $0x894c; BYTE $0xd3 // movq\t%r10, %rbx\n\tWORD $0xff83; BYTE $0x03 // cmpl\t$3, %edi\n\tJB   LBB12_61\n\nLBB12_62:\n\tLONG $0x245c8b44; BYTE $0x10 // movl\t16(%rsp), %r11d                 # 4-byte Reload\n\tWORD $0x8949; BYTE $0xdd     // movq\t%rbx, %r13\n\tWORD $0x894d; BYTE $0xc4     // movq\t%r8, %r12\n\nLBB12_63:\n\tLONG $0x487cd162; WORD $0x4510; BYTE $0x00 // vmovups\t(%r13), %zmm0\n\tLONG $0x487cd162; WORD $0x5510; BYTE $0x01 // vmovups\t64(%r13), %zmm2\n\tLONG $0x487cd162; WORD $0x5d10; BYTE $0x02 // vmovups\t128(%r13), %zmm3\n\tLONG $0x487cd162; WORD $0x6510; BYTE $0x03 // vmovups\t192(%r13), %zmm4\n\tLONG $0x4875d262; WORD $0x0498; BYTE $0x24 // vfmadd132ps\t(%r12), %zmm1, %zmm0    # zmm0 = (zmm0 * mem) + zmm1\n\tQUAD $0x012444b8486dd262                   // vfmadd231ps\t64(%r12), %zmm2, %zmm0  # zmm0 = (zmm2 * mem) + zmm0\n\tQUAD $0x022444b84865d262                   // vfmadd231ps\t128(%r12), %zmm3, %zmm0 # zmm0 = (zmm3 * mem) + zmm0\n\tQUAD $0x032444b8485dd262                   // vfmadd231ps\t192(%r12), %zmm4, %zmm0 # zmm0 = (zmm4 * mem) + zmm0\n\tLONG $0x00c58149; WORD $0x0001; BYTE $0x00 // addq\t$256, %r13                      # imm = 0x100\n\tLONG $0x00c48149; WORD $0x0001; BYTE $0x00 // addq\t$256, %r12                      # imm = 0x100\n\tLONG $0x487cf162; WORD $0xc828             // vmovaps\t%zmm0, %zmm1\n\tLONG $0xfcc38341                           // addl\t$-4, %r11d\n\tJNE  LBB12_63\n\nLBB12_64:\n\tWORD $0xc2f6; BYTE $0x03 // testb\t$3, %dl\n\tJE   LBB12_67\n\tWORD $0x3145; BYTE $0xdb // xorl\t%r11d, %r11d\n\nLBB12_66:\n\tQUAD $0x001d4c10487c9162                   // vmovups\t(%r13,%r11), %zmm1\n\tLONG $0x48759262; WORD $0x04b8; BYTE $0x1c // vfmadd231ps\t(%r12,%r11), %zmm1, %zmm0 # zmm0 = (zmm1 * mem) + zmm0\n\tLONG $0x40c38349                           // addq\t$64, %r11\n\tWORD $0x3945; BYTE $0xd9                   // cmpl\t%r11d, %r9d\n\tJNE  LBB12_66\n\tJMP  LBB12_67\n\nLBB12_140:\n\tLONG $0xd8658d48         // leaq\t-40(%rbp), %rsp\n\tBYTE $0x5b               // popq\t%rbx\n\tWORD $0x5c41             // popq\t%r12\n\tWORD $0x5d41             // popq\t%r13\n\tWORD $0x5e41             // popq\t%r14\n\tWORD $0x5f41             // popq\t%r15\n\tBYTE $0x5d               // popq\t%rbp\n\tWORD $0xf8c5; BYTE $0x77 // vzeroupper\n\tPOPQ DI\n\tPOPQ DI\n\tPOPQ DI\n\tPOPQ DI\n\tPOPQ DI\n\tPOPQ DI\n\tRET\n"
  },
  {
    "path": "common/floats/floats_neon.go",
    "content": "//go:build !noasm && arm64\n// Code generated by GoAT. DO NOT EDIT.\n// versions:\n// \tclang   18.1.3 (1ubuntu1)\n// \tobjdump 2.42\n// flags: -O3\n// source: src/floats_neon.c\n\npackage floats\n\nimport \"unsafe\"\n\n//go:noescape\nfunc vmul_const_add_to(a, b, c, dst unsafe.Pointer, n int64)\n\n//go:noescape\nfunc vmul_const_add(a, b, c unsafe.Pointer, n int64)\n\n//go:noescape\nfunc vmul_const_to(a, b, c unsafe.Pointer, n int64)\n\n//go:noescape\nfunc vmul_const(a, b unsafe.Pointer, n int64)\n\n//go:noescape\nfunc vadd_const(a, b unsafe.Pointer, n int64)\n\n//go:noescape\nfunc vsub_to(a, b, c unsafe.Pointer, n int64)\n\n//go:noescape\nfunc vsub(a, b unsafe.Pointer, n int64)\n\n//go:noescape\nfunc vmul_to(a, b, c unsafe.Pointer, n int64)\n\n//go:noescape\nfunc vdiv_to(a, b, c unsafe.Pointer, n int64)\n\n//go:noescape\nfunc vsqrt_to(a, b unsafe.Pointer, n int64)\n\n//go:noescape\nfunc vdot(a, b unsafe.Pointer, n int64) (result float32)\n\n//go:noescape\nfunc veuclidean(a, b unsafe.Pointer, n int64) (result float32)\n\n//go:noescape\nfunc vmm(transA, transB bool, m, n, k int64, a unsafe.Pointer, lda int64, b unsafe.Pointer, ldb int64, c unsafe.Pointer, ldc int64)\n"
  },
  {
    "path": "common/floats/floats_neon.s",
    "content": "//go:build !noasm && arm64\n// Code generated by GoAT. DO NOT EDIT.\n// versions:\n// \tclang   18.1.3 (1ubuntu1)\n// \tobjdump 2.42\n// flags: -O3\n// source: src/floats_neon.c\n\nTEXT ·vmul_const_add_to(SB), $0-40\n\tMOVD a+0(FP), R0\n\tMOVD b+8(FP), R1\n\tMOVD c+16(FP), R2\n\tMOVD dst+24(FP), R3\n\tMOVD n+32(FP), R4\n\tWORD $0xf100049f    // cmp\tx4, #1\n\tBLT  LBB0_6\n\tWORD $0xa9bf7bfd    // stp\tx29, x30, [sp, #-16]!\n\tWORD $0xf100309f    // cmp\tx4, #12\n\tWORD $0x910003fd    // mov\tx29, sp\n\tBHS  LBB0_7\n\tWORD $0xaa1f03e8    // mov\tx8, xzr\n\nLBB0_3:\n\tWORD $0xd37ef50b // lsl\tx11, x8, #2\n\tWORD $0xcb080088 // sub\tx8, x4, x8\n\tWORD $0x8b0b0069 // add\tx9, x3, x11\n\tWORD $0x8b0b004a // add\tx10, x2, x11\n\tWORD $0x8b0b000b // add\tx11, x0, x11\n\nLBB0_4:\n\tWORD $0xbc404560 // ldr\ts0, [x11], #4\n\tWORD $0xbd400021 // ldr\ts1, [x1]\n\tWORD $0xbc404542 // ldr\ts2, [x10], #4\n\tWORD $0xf1000508 // subs\tx8, x8, #1\n\tWORD $0x1f010800 // fmadd\ts0, s0, s1, s2\n\tWORD $0xbc004520 // str\ts0, [x9], #4\n\tBNE  LBB0_4\n\nLBB0_5:\n\tWORD $0xa8c17bfd // ldp\tx29, x30, [sp], #16\n\nLBB0_6:\n\tRET\n\nLBB0_7:\n\tWORD $0xd37ef488 // lsl\tx8, x4, #2\n\tWORD $0x91001029 // add\tx9, x1, #4\n\tWORD $0xeb03013f // cmp\tx9, x3\n\tWORD $0x8b08006b // add\tx11, x3, x8\n\tWORD $0x8b08004a // add\tx10, x2, x8\n\tWORD $0x8b080008 // add\tx8, x0, x8\n\tWORD $0xfa418160 // ccmp\tx11, x1, #0, hi\n\tWORD $0x1a9f97e9 // cset\tw9, hi\n\tWORD $0xeb03015f // cmp\tx10, x3\n\tWORD $0xfa428160 // ccmp\tx11, x2, #0, hi\n\tWORD $0x1a9f97ea // cset\tw10, hi\n\tWORD $0xeb00017f // cmp\tx11, x0\n\tWORD $0xfa438100 // ccmp\tx8, x3, #0, hi\n\tWORD $0xaa1f03e8 // mov\tx8, xzr\n\tBHI  LBB0_3\n\tWORD $0x3707fc69 // tbnz\tw9, #0, .LBB0_3\n\tWORD $0x3707fc4a // tbnz\tw10, #0, .LBB0_3\n\tWORD $0x4d40c820 // ld1r\t{ v0.4s }, [x1]\n\tWORD $0x927dec88 // and\tx8, x4, #0x7ffffffffffffff8\n\tWORD $0x91004009 // add\tx9, x0, #16\n\tWORD $0x9100404a // add\tx10, x2, #16\n\tWORD $0x9100406b // add\tx11, x3, #16\n\tWORD $0xaa0803ec // mov\tx12, x8\n\nLBB0_11:\n\tWORD $0xad7f9141 // ldp\tq1, q4, [x10, #-16]\n\tWORD $0xf100218c // subs\tx12, x12, #8\n\tWORD $0xad7f8d22 // ldp\tq2, q3, [x9, #-16]\n\tWORD $0x91008129 // add\tx9, x9, #32\n\tWORD $0x9100814a // add\tx10, x10, #32\n\tWORD $0x4e22cc01 // fmla\tv1.4s, v0.4s, v2.4s\n\tWORD $0x4e23cc04 // fmla\tv4.4s, v0.4s, v3.4s\n\tWORD $0xad3f9161 // stp\tq1, q4, [x11, #-16]\n\tWORD $0x9100816b // add\tx11, x11, #32\n\tBNE  LBB0_11\n\tWORD $0xeb04011f // cmp\tx8, x4\n\tBNE  LBB0_3\n\tB    LBB0_5\n\nTEXT ·vmul_const_add(SB), $0-32\n\tMOVD a+0(FP), R0\n\tMOVD b+8(FP), R1\n\tMOVD c+16(FP), R2\n\tMOVD n+24(FP), R3\n\tWORD $0xf100047f  // cmp\tx3, #1\n\tBLT  LBB1_6\n\tWORD $0xa9bf7bfd  // stp\tx29, x30, [sp, #-16]!\n\tWORD $0xf100207f  // cmp\tx3, #8\n\tWORD $0x910003fd  // mov\tx29, sp\n\tBHS  LBB1_7\n\tWORD $0xaa1f03e8  // mov\tx8, xzr\n\nLBB1_3:\n\tWORD $0xd37ef50a // lsl\tx10, x8, #2\n\tWORD $0xcb080068 // sub\tx8, x3, x8\n\tWORD $0x8b0a0049 // add\tx9, x2, x10\n\tWORD $0x8b0a000a // add\tx10, x0, x10\n\nLBB1_4:\n\tWORD $0xbc404540 // ldr\ts0, [x10], #4\n\tWORD $0xbd400021 // ldr\ts1, [x1]\n\tWORD $0xbd400122 // ldr\ts2, [x9]\n\tWORD $0xf1000508 // subs\tx8, x8, #1\n\tWORD $0x1f010800 // fmadd\ts0, s0, s1, s2\n\tWORD $0xbc004520 // str\ts0, [x9], #4\n\tBNE  LBB1_4\n\nLBB1_5:\n\tWORD $0xa8c17bfd // ldp\tx29, x30, [sp], #16\n\nLBB1_6:\n\tRET\n\nLBB1_7:\n\tWORD $0xd37ef468 // lsl\tx8, x3, #2\n\tWORD $0x91001029 // add\tx9, x1, #4\n\tWORD $0xeb02013f // cmp\tx9, x2\n\tWORD $0x8b08004a // add\tx10, x2, x8\n\tWORD $0x8b080008 // add\tx8, x0, x8\n\tWORD $0xfa418140 // ccmp\tx10, x1, #0, hi\n\tWORD $0x1a9f97e9 // cset\tw9, hi\n\tWORD $0xeb00015f // cmp\tx10, x0\n\tWORD $0xfa428100 // ccmp\tx8, x2, #0, hi\n\tWORD $0xaa1f03e8 // mov\tx8, xzr\n\tBHI  LBB1_3\n\tWORD $0x3707fd09 // tbnz\tw9, #0, .LBB1_3\n\tWORD $0x4d40c820 // ld1r\t{ v0.4s }, [x1]\n\tWORD $0x927dec68 // and\tx8, x3, #0x7ffffffffffffff8\n\tWORD $0x91004009 // add\tx9, x0, #16\n\tWORD $0x9100404a // add\tx10, x2, #16\n\tWORD $0xaa0803eb // mov\tx11, x8\n\nLBB1_10:\n\tWORD $0xad7f9141 // ldp\tq1, q4, [x10, #-16]\n\tWORD $0xf100216b // subs\tx11, x11, #8\n\tWORD $0xad7f8d22 // ldp\tq2, q3, [x9, #-16]\n\tWORD $0x91008129 // add\tx9, x9, #32\n\tWORD $0x4e22cc01 // fmla\tv1.4s, v0.4s, v2.4s\n\tWORD $0x4e23cc04 // fmla\tv4.4s, v0.4s, v3.4s\n\tWORD $0xad3f9141 // stp\tq1, q4, [x10, #-16]\n\tWORD $0x9100814a // add\tx10, x10, #32\n\tBNE  LBB1_10\n\tWORD $0xeb03011f // cmp\tx8, x3\n\tBNE  LBB1_3\n\tB    LBB1_5\n\nTEXT ·vmul_const_to(SB), $0-32\n\tMOVD a+0(FP), R0\n\tMOVD b+8(FP), R1\n\tMOVD c+16(FP), R2\n\tMOVD n+24(FP), R3\n\tWORD $0xf100047f  // cmp\tx3, #1\n\tBLT  LBB2_6\n\tWORD $0xa9bf7bfd  // stp\tx29, x30, [sp, #-16]!\n\tWORD $0xf100207f  // cmp\tx3, #8\n\tWORD $0x910003fd  // mov\tx29, sp\n\tBHS  LBB2_7\n\tWORD $0xaa1f03e8  // mov\tx8, xzr\n\nLBB2_3:\n\tWORD $0xd37ef50a // lsl\tx10, x8, #2\n\tWORD $0xcb080068 // sub\tx8, x3, x8\n\tWORD $0x8b0a0049 // add\tx9, x2, x10\n\tWORD $0x8b0a000a // add\tx10, x0, x10\n\nLBB2_4:\n\tWORD $0xbc404540 // ldr\ts0, [x10], #4\n\tWORD $0xbd400021 // ldr\ts1, [x1]\n\tWORD $0xf1000508 // subs\tx8, x8, #1\n\tWORD $0x1e210800 // fmul\ts0, s0, s1\n\tWORD $0xbc004520 // str\ts0, [x9], #4\n\tBNE  LBB2_4\n\nLBB2_5:\n\tWORD $0xa8c17bfd // ldp\tx29, x30, [sp], #16\n\nLBB2_6:\n\tRET\n\nLBB2_7:\n\tWORD $0xd37ef468 // lsl\tx8, x3, #2\n\tWORD $0x91001029 // add\tx9, x1, #4\n\tWORD $0xeb02013f // cmp\tx9, x2\n\tWORD $0x8b08004a // add\tx10, x2, x8\n\tWORD $0x8b080008 // add\tx8, x0, x8\n\tWORD $0xfa418140 // ccmp\tx10, x1, #0, hi\n\tWORD $0x1a9f97e9 // cset\tw9, hi\n\tWORD $0xeb00015f // cmp\tx10, x0\n\tWORD $0xfa428100 // ccmp\tx8, x2, #0, hi\n\tWORD $0xaa1f03e8 // mov\tx8, xzr\n\tBHI  LBB2_3\n\tWORD $0x3707fd29 // tbnz\tw9, #0, .LBB2_3\n\tWORD $0x4d40c820 // ld1r\t{ v0.4s }, [x1]\n\tWORD $0x927dec68 // and\tx8, x3, #0x7ffffffffffffff8\n\tWORD $0x91004009 // add\tx9, x0, #16\n\tWORD $0x9100404a // add\tx10, x2, #16\n\tWORD $0xaa0803eb // mov\tx11, x8\n\nLBB2_10:\n\tWORD $0xad7f8921 // ldp\tq1, q2, [x9, #-16]\n\tWORD $0xf100216b // subs\tx11, x11, #8\n\tWORD $0x91008129 // add\tx9, x9, #32\n\tWORD $0x6e20dc21 // fmul\tv1.4s, v1.4s, v0.4s\n\tWORD $0x6e20dc42 // fmul\tv2.4s, v2.4s, v0.4s\n\tWORD $0xad3f8941 // stp\tq1, q2, [x10, #-16]\n\tWORD $0x9100814a // add\tx10, x10, #32\n\tBNE  LBB2_10\n\tWORD $0xeb03011f // cmp\tx8, x3\n\tBNE  LBB2_3\n\tB    LBB2_5\n\nTEXT ·vmul_const(SB), $0-24\n\tMOVD a+0(FP), R0\n\tMOVD b+8(FP), R1\n\tMOVD n+16(FP), R2\n\tWORD $0xf100045f  // cmp\tx2, #1\n\tBLT  LBB3_8\n\tWORD $0xa9bf7bfd  // stp\tx29, x30, [sp, #-16]!\n\tWORD $0xf100205f  // cmp\tx2, #8\n\tWORD $0x910003fd  // mov\tx29, sp\n\tBLO  LBB3_4\n\tWORD $0x91001028  // add\tx8, x1, #4\n\tWORD $0xeb00011f  // cmp\tx8, x0\n\tBLS  LBB3_9\n\tWORD $0x8b020808  // add\tx8, x0, x2, lsl #2\n\tWORD $0xeb01011f  // cmp\tx8, x1\n\tBLS  LBB3_9\n\nLBB3_4:\n\tWORD $0xaa1f03e8 // mov\tx8, xzr\n\nLBB3_5:\n\tWORD $0x8b080809 // add\tx9, x0, x8, lsl #2\n\tWORD $0xcb080048 // sub\tx8, x2, x8\n\nLBB3_6:\n\tWORD $0xbd400020 // ldr\ts0, [x1]\n\tWORD $0xbd400121 // ldr\ts1, [x9]\n\tWORD $0xf1000508 // subs\tx8, x8, #1\n\tWORD $0x1e210800 // fmul\ts0, s0, s1\n\tWORD $0xbc004520 // str\ts0, [x9], #4\n\tBNE  LBB3_6\n\nLBB3_7:\n\tWORD $0xa8c17bfd // ldp\tx29, x30, [sp], #16\n\nLBB3_8:\n\tRET\n\nLBB3_9:\n\tWORD $0x4d40c820 // ld1r\t{ v0.4s }, [x1]\n\tWORD $0x927dec48 // and\tx8, x2, #0x7ffffffffffffff8\n\tWORD $0x91004009 // add\tx9, x0, #16\n\tWORD $0xaa0803ea // mov\tx10, x8\n\nLBB3_10:\n\tWORD $0xad7f8921 // ldp\tq1, q2, [x9, #-16]\n\tWORD $0xf100214a // subs\tx10, x10, #8\n\tWORD $0x6e21dc01 // fmul\tv1.4s, v0.4s, v1.4s\n\tWORD $0x6e22dc02 // fmul\tv2.4s, v0.4s, v2.4s\n\tWORD $0xad3f8921 // stp\tq1, q2, [x9, #-16]\n\tWORD $0x91008129 // add\tx9, x9, #32\n\tBNE  LBB3_10\n\tWORD $0xeb02011f // cmp\tx8, x2\n\tBEQ  LBB3_7\n\tB    LBB3_5\n\nTEXT ·vadd_const(SB), $0-24\n\tMOVD a+0(FP), R0\n\tMOVD b+8(FP), R1\n\tMOVD n+16(FP), R2\n\tWORD $0xf100045f  // cmp\tx2, #1\n\tBLT  LBB4_8\n\tWORD $0xa9bf7bfd  // stp\tx29, x30, [sp, #-16]!\n\tWORD $0xf100205f  // cmp\tx2, #8\n\tWORD $0x910003fd  // mov\tx29, sp\n\tBLO  LBB4_4\n\tWORD $0x91001028  // add\tx8, x1, #4\n\tWORD $0xeb00011f  // cmp\tx8, x0\n\tBLS  LBB4_9\n\tWORD $0x8b020808  // add\tx8, x0, x2, lsl #2\n\tWORD $0xeb01011f  // cmp\tx8, x1\n\tBLS  LBB4_9\n\nLBB4_4:\n\tWORD $0xaa1f03e8 // mov\tx8, xzr\n\nLBB4_5:\n\tWORD $0x8b080809 // add\tx9, x0, x8, lsl #2\n\tWORD $0xcb080048 // sub\tx8, x2, x8\n\nLBB4_6:\n\tWORD $0xbd400020 // ldr\ts0, [x1]\n\tWORD $0xbd400121 // ldr\ts1, [x9]\n\tWORD $0xf1000508 // subs\tx8, x8, #1\n\tWORD $0x1e212800 // fadd\ts0, s0, s1\n\tWORD $0xbc004520 // str\ts0, [x9], #4\n\tBNE  LBB4_6\n\nLBB4_7:\n\tWORD $0xa8c17bfd // ldp\tx29, x30, [sp], #16\n\nLBB4_8:\n\tRET\n\nLBB4_9:\n\tWORD $0x4d40c820 // ld1r\t{ v0.4s }, [x1]\n\tWORD $0x927dec48 // and\tx8, x2, #0x7ffffffffffffff8\n\tWORD $0x91004009 // add\tx9, x0, #16\n\tWORD $0xaa0803ea // mov\tx10, x8\n\nLBB4_10:\n\tWORD $0xad7f8921 // ldp\tq1, q2, [x9, #-16]\n\tWORD $0xf100214a // subs\tx10, x10, #8\n\tWORD $0x4e21d401 // fadd\tv1.4s, v0.4s, v1.4s\n\tWORD $0x4e22d402 // fadd\tv2.4s, v0.4s, v2.4s\n\tWORD $0xad3f8921 // stp\tq1, q2, [x9, #-16]\n\tWORD $0x91008129 // add\tx9, x9, #32\n\tBNE  LBB4_10\n\tWORD $0xeb02011f // cmp\tx8, x2\n\tBEQ  LBB4_7\n\tB    LBB4_5\n\nTEXT ·vsub_to(SB), $0-32\n\tMOVD a+0(FP), R0\n\tMOVD b+8(FP), R1\n\tMOVD c+16(FP), R2\n\tMOVD n+24(FP), R3\n\tWORD $0xf100047f  // cmp\tx3, #1\n\tBLT  LBB5_6\n\tWORD $0xa9bf7bfd  // stp\tx29, x30, [sp, #-16]!\n\tWORD $0xf100207f  // cmp\tx3, #8\n\tWORD $0x910003fd  // mov\tx29, sp\n\tBHS  LBB5_7\n\tWORD $0xaa1f03e8  // mov\tx8, xzr\n\nLBB5_3:\n\tWORD $0xd37ef50b // lsl\tx11, x8, #2\n\tWORD $0xcb080068 // sub\tx8, x3, x8\n\tWORD $0x8b0b0049 // add\tx9, x2, x11\n\tWORD $0x8b0b002a // add\tx10, x1, x11\n\tWORD $0x8b0b000b // add\tx11, x0, x11\n\nLBB5_4:\n\tWORD $0xbc404560 // ldr\ts0, [x11], #4\n\tWORD $0xf1000508 // subs\tx8, x8, #1\n\tWORD $0xbc404541 // ldr\ts1, [x10], #4\n\tWORD $0x1e213800 // fsub\ts0, s0, s1\n\tWORD $0xbc004520 // str\ts0, [x9], #4\n\tBNE  LBB5_4\n\nLBB5_5:\n\tWORD $0xa8c17bfd // ldp\tx29, x30, [sp], #16\n\nLBB5_6:\n\tRET\n\nLBB5_7:\n\tWORD $0xcb000048 // sub\tx8, x2, x0\n\tWORD $0xf100811f // cmp\tx8, #32\n\tWORD $0xaa1f03e8 // mov\tx8, xzr\n\tBLO  LBB5_3\n\tWORD $0xcb010049 // sub\tx9, x2, x1\n\tWORD $0xf100813f // cmp\tx9, #32\n\tBLO  LBB5_3\n\tWORD $0x927dec68 // and\tx8, x3, #0x7ffffffffffffff8\n\tWORD $0x91004009 // add\tx9, x0, #16\n\tWORD $0x9100402a // add\tx10, x1, #16\n\tWORD $0x9100404b // add\tx11, x2, #16\n\tWORD $0xaa0803ec // mov\tx12, x8\n\nLBB5_10:\n\tWORD $0xad7f8d40 // ldp\tq0, q3, [x10, #-16]\n\tWORD $0xf100218c // subs\tx12, x12, #8\n\tWORD $0xad7f8921 // ldp\tq1, q2, [x9, #-16]\n\tWORD $0x91008129 // add\tx9, x9, #32\n\tWORD $0x9100814a // add\tx10, x10, #32\n\tWORD $0x4ea0d420 // fsub\tv0.4s, v1.4s, v0.4s\n\tWORD $0x4ea3d441 // fsub\tv1.4s, v2.4s, v3.4s\n\tWORD $0xad3f8560 // stp\tq0, q1, [x11, #-16]\n\tWORD $0x9100816b // add\tx11, x11, #32\n\tBNE  LBB5_10\n\tWORD $0xeb03011f // cmp\tx8, x3\n\tBNE  LBB5_3\n\tB    LBB5_5\n\nTEXT ·vsub(SB), $0-24\n\tMOVD a+0(FP), R0\n\tMOVD b+8(FP), R1\n\tMOVD n+16(FP), R2\n\tWORD $0xf100045f  // cmp\tx2, #1\n\tBLT  LBB6_8\n\tWORD $0xa9bf7bfd  // stp\tx29, x30, [sp, #-16]!\n\tWORD $0xf100205f  // cmp\tx2, #8\n\tWORD $0x910003fd  // mov\tx29, sp\n\tBLO  LBB6_4\n\tWORD $0xd37ef448  // lsl\tx8, x2, #2\n\tWORD $0x8b080029  // add\tx9, x1, x8\n\tWORD $0xeb00013f  // cmp\tx9, x0\n\tBLS  LBB6_9\n\tWORD $0x8b080008  // add\tx8, x0, x8\n\tWORD $0xeb01011f  // cmp\tx8, x1\n\tBLS  LBB6_9\n\nLBB6_4:\n\tWORD $0xaa1f03e8 // mov\tx8, xzr\n\nLBB6_5:\n\tWORD $0xd37ef50a // lsl\tx10, x8, #2\n\tWORD $0xcb080048 // sub\tx8, x2, x8\n\tWORD $0x8b0a0009 // add\tx9, x0, x10\n\tWORD $0x8b0a002a // add\tx10, x1, x10\n\nLBB6_6:\n\tWORD $0xbc404540 // ldr\ts0, [x10], #4\n\tWORD $0xbd400121 // ldr\ts1, [x9]\n\tWORD $0xf1000508 // subs\tx8, x8, #1\n\tWORD $0x1e203820 // fsub\ts0, s1, s0\n\tWORD $0xbc004520 // str\ts0, [x9], #4\n\tBNE  LBB6_6\n\nLBB6_7:\n\tWORD $0xa8c17bfd // ldp\tx29, x30, [sp], #16\n\nLBB6_8:\n\tRET\n\nLBB6_9:\n\tWORD $0x927dec48 // and\tx8, x2, #0x7ffffffffffffff8\n\tWORD $0x91004029 // add\tx9, x1, #16\n\tWORD $0x9100400a // add\tx10, x0, #16\n\tWORD $0xaa0803eb // mov\tx11, x8\n\nLBB6_10:\n\tWORD $0xad7f8d40 // ldp\tq0, q3, [x10, #-16]\n\tWORD $0xf100216b // subs\tx11, x11, #8\n\tWORD $0xad7f8921 // ldp\tq1, q2, [x9, #-16]\n\tWORD $0x91008129 // add\tx9, x9, #32\n\tWORD $0x4ea1d400 // fsub\tv0.4s, v0.4s, v1.4s\n\tWORD $0x4ea2d461 // fsub\tv1.4s, v3.4s, v2.4s\n\tWORD $0xad3f8540 // stp\tq0, q1, [x10, #-16]\n\tWORD $0x9100814a // add\tx10, x10, #32\n\tBNE  LBB6_10\n\tWORD $0xeb02011f // cmp\tx8, x2\n\tBEQ  LBB6_7\n\tB    LBB6_5\n\nTEXT ·vmul_to(SB), $0-32\n\tMOVD a+0(FP), R0\n\tMOVD b+8(FP), R1\n\tMOVD c+16(FP), R2\n\tMOVD n+24(FP), R3\n\tWORD $0xf100047f  // cmp\tx3, #1\n\tBLT  LBB7_6\n\tWORD $0xa9bf7bfd  // stp\tx29, x30, [sp, #-16]!\n\tWORD $0xf100207f  // cmp\tx3, #8\n\tWORD $0x910003fd  // mov\tx29, sp\n\tBHS  LBB7_7\n\tWORD $0xaa1f03e8  // mov\tx8, xzr\n\nLBB7_3:\n\tWORD $0xd37ef50b // lsl\tx11, x8, #2\n\tWORD $0xcb080068 // sub\tx8, x3, x8\n\tWORD $0x8b0b0049 // add\tx9, x2, x11\n\tWORD $0x8b0b002a // add\tx10, x1, x11\n\tWORD $0x8b0b000b // add\tx11, x0, x11\n\nLBB7_4:\n\tWORD $0xbc404560 // ldr\ts0, [x11], #4\n\tWORD $0xf1000508 // subs\tx8, x8, #1\n\tWORD $0xbc404541 // ldr\ts1, [x10], #4\n\tWORD $0x1e210800 // fmul\ts0, s0, s1\n\tWORD $0xbc004520 // str\ts0, [x9], #4\n\tBNE  LBB7_4\n\nLBB7_5:\n\tWORD $0xa8c17bfd // ldp\tx29, x30, [sp], #16\n\nLBB7_6:\n\tRET\n\nLBB7_7:\n\tWORD $0xcb000048 // sub\tx8, x2, x0\n\tWORD $0xf100811f // cmp\tx8, #32\n\tWORD $0xaa1f03e8 // mov\tx8, xzr\n\tBLO  LBB7_3\n\tWORD $0xcb010049 // sub\tx9, x2, x1\n\tWORD $0xf100813f // cmp\tx9, #32\n\tBLO  LBB7_3\n\tWORD $0x927dec68 // and\tx8, x3, #0x7ffffffffffffff8\n\tWORD $0x91004009 // add\tx9, x0, #16\n\tWORD $0x9100402a // add\tx10, x1, #16\n\tWORD $0x9100404b // add\tx11, x2, #16\n\tWORD $0xaa0803ec // mov\tx12, x8\n\nLBB7_10:\n\tWORD $0xad7f8d40 // ldp\tq0, q3, [x10, #-16]\n\tWORD $0xf100218c // subs\tx12, x12, #8\n\tWORD $0xad7f8921 // ldp\tq1, q2, [x9, #-16]\n\tWORD $0x91008129 // add\tx9, x9, #32\n\tWORD $0x9100814a // add\tx10, x10, #32\n\tWORD $0x6e20dc20 // fmul\tv0.4s, v1.4s, v0.4s\n\tWORD $0x6e23dc41 // fmul\tv1.4s, v2.4s, v3.4s\n\tWORD $0xad3f8560 // stp\tq0, q1, [x11, #-16]\n\tWORD $0x9100816b // add\tx11, x11, #32\n\tBNE  LBB7_10\n\tWORD $0xeb03011f // cmp\tx8, x3\n\tBNE  LBB7_3\n\tB    LBB7_5\n\nTEXT ·vdiv_to(SB), $0-32\n\tMOVD a+0(FP), R0\n\tMOVD b+8(FP), R1\n\tMOVD c+16(FP), R2\n\tMOVD n+24(FP), R3\n\tWORD $0xf100047f  // cmp\tx3, #1\n\tBLT  LBB8_6\n\tWORD $0xa9bf7bfd  // stp\tx29, x30, [sp, #-16]!\n\tWORD $0xf100207f  // cmp\tx3, #8\n\tWORD $0x910003fd  // mov\tx29, sp\n\tBHS  LBB8_7\n\tWORD $0xaa1f03e8  // mov\tx8, xzr\n\nLBB8_3:\n\tWORD $0xd37ef50b // lsl\tx11, x8, #2\n\tWORD $0xcb080068 // sub\tx8, x3, x8\n\tWORD $0x8b0b0049 // add\tx9, x2, x11\n\tWORD $0x8b0b002a // add\tx10, x1, x11\n\tWORD $0x8b0b000b // add\tx11, x0, x11\n\nLBB8_4:\n\tWORD $0xbc404560 // ldr\ts0, [x11], #4\n\tWORD $0xf1000508 // subs\tx8, x8, #1\n\tWORD $0xbc404541 // ldr\ts1, [x10], #4\n\tWORD $0x1e211800 // fdiv\ts0, s0, s1\n\tWORD $0xbc004520 // str\ts0, [x9], #4\n\tBNE  LBB8_4\n\nLBB8_5:\n\tWORD $0xa8c17bfd // ldp\tx29, x30, [sp], #16\n\nLBB8_6:\n\tRET\n\nLBB8_7:\n\tWORD $0xcb000048 // sub\tx8, x2, x0\n\tWORD $0xf100811f // cmp\tx8, #32\n\tWORD $0xaa1f03e8 // mov\tx8, xzr\n\tBLO  LBB8_3\n\tWORD $0xcb010049 // sub\tx9, x2, x1\n\tWORD $0xf100813f // cmp\tx9, #32\n\tBLO  LBB8_3\n\tWORD $0x927dec68 // and\tx8, x3, #0x7ffffffffffffff8\n\tWORD $0x91004009 // add\tx9, x0, #16\n\tWORD $0x9100402a // add\tx10, x1, #16\n\tWORD $0x9100404b // add\tx11, x2, #16\n\tWORD $0xaa0803ec // mov\tx12, x8\n\nLBB8_10:\n\tWORD $0xad7f8d40 // ldp\tq0, q3, [x10, #-16]\n\tWORD $0xf100218c // subs\tx12, x12, #8\n\tWORD $0xad7f8921 // ldp\tq1, q2, [x9, #-16]\n\tWORD $0x91008129 // add\tx9, x9, #32\n\tWORD $0x9100814a // add\tx10, x10, #32\n\tWORD $0x6e20fc20 // fdiv\tv0.4s, v1.4s, v0.4s\n\tWORD $0x6e23fc41 // fdiv\tv1.4s, v2.4s, v3.4s\n\tWORD $0xad3f8560 // stp\tq0, q1, [x11, #-16]\n\tWORD $0x9100816b // add\tx11, x11, #32\n\tBNE  LBB8_10\n\tWORD $0xeb03011f // cmp\tx8, x3\n\tBNE  LBB8_3\n\tB    LBB8_5\n\nTEXT ·vsqrt_to(SB), $0-24\n\tMOVD a+0(FP), R0\n\tMOVD b+8(FP), R1\n\tMOVD n+16(FP), R2\n\tWORD $0xa9bf7bfd  // stp\tx29, x30, [sp, #-16]!\n\tWORD $0x91000c48  // add\tx8, x2, #3\n\tWORD $0xf100005f  // cmp\tx2, #0\n\tWORD $0x910003fd  // mov\tx29, sp\n\tWORD $0x9a82b108  // csel\tx8, x8, x2, lt\n\tWORD $0xd342fd09  // lsr\tx9, x8, #2\n\tWORD $0x927ef508  // and\tx8, x8, #0xfffffffffffffffc\n\tWORD $0xcb080048  // sub\tx8, x2, x8\n\tWORD $0x7100053f  // cmp\tw9, #1\n\tBLT  LBB9_2\n\nLBB9_1:\n\tWORD $0x3cc10400 // ldr\tq0, [x0], #16\n\tWORD $0x71000529 // subs\tw9, w9, #1\n\tWORD $0x6ea1f800 // fsqrt\tv0.4s, v0.4s\n\tWORD $0x3c810420 // str\tq0, [x1], #16\n\tBNE  LBB9_1\n\nLBB9_2:\n\tWORD $0x7100051f // cmp\tw8, #1\n\tBLT  LBB9_5\n\tWORD $0x92407d08 // and\tx8, x8, #0xffffffff\n\nLBB9_4:\n\tWORD $0x0ddfc800 // ld1r\t{ v0.2s }, [x0], #4\n\tWORD $0xf1000508 // subs\tx8, x8, #1\n\tWORD $0x2ea1f800 // fsqrt\tv0.2s, v0.2s\n\tWORD $0x0d9f8020 // st1\t{ v0.s }[0], [x1], #4\n\tBNE  LBB9_4\n\nLBB9_5:\n\tWORD $0xa8c17bfd // ldp\tx29, x30, [sp], #16\n\tRET\n\nTEXT ·vdot(SB), $8-24\n\tMOVD a+0(FP), R0\n\tMOVD b+8(FP), R1\n\tMOVD n+16(FP), R2\n\tWORD $0xa9bf7bfd  // stp\tx29, x30, [sp, #-16]!\n\tWORD $0x91000c48  // add\tx8, x2, #3\n\tWORD $0xf100005f  // cmp\tx2, #0\n\tWORD $0x910003fd  // mov\tx29, sp\n\tWORD $0x9a82b108  // csel\tx8, x8, x2, lt\n\tWORD $0x9342fd0a  // asr\tx10, x8, #2\n\tWORD $0x927ef508  // and\tx8, x8, #0xfffffffffffffffc\n\tWORD $0xcb080048  // sub\tx8, x2, x8\n\tWORD $0x7100055f  // cmp\tw10, #1\n\tBLT  LBB10_5\n\tWORD $0x3cc10400  // ldr\tq0, [x0], #16\n\tWORD $0x71000549  // subs\tw9, w10, #1\n\tWORD $0x3cc10421  // ldr\tq1, [x1], #16\n\tWORD $0x6e21dc00  // fmul\tv0.4s, v0.4s, v1.4s\n\tBEQ  LBB10_6\n\tWORD $0xb27b7beb  // mov\tx11, #68719476704\n\tWORD $0xaa0103ec  // mov\tx12, x1\n\tWORD $0x8b0a116a  // add\tx10, x11, x10, lsl #4\n\tWORD $0xaa0003eb  // mov\tx11, x0\n\tWORD $0x927c7d4a  // and\tx10, x10, #0xffffffff0\n\tWORD $0x9100414a  // add\tx10, x10, #16\n\nLBB10_3:\n\tWORD $0x3cc10561 // ldr\tq1, [x11], #16\n\tWORD $0x71000529 // subs\tw9, w9, #1\n\tWORD $0x3cc10582 // ldr\tq2, [x12], #16\n\tWORD $0x4e21cc40 // fmla\tv0.4s, v2.4s, v1.4s\n\tBNE  LBB10_3\n\tWORD $0x8b0a0000 // add\tx0, x0, x10\n\tWORD $0x8b0a0021 // add\tx1, x1, x10\n\tB    LBB10_6\n\nLBB10_5:\n\tWORD $0x6f00e400 // movi\tv0.2d, #0000000000000000\n\nLBB10_6:\n\tWORD $0x2f00e401 // movi\td1, #0000000000000000\n\tWORD $0x5e0c0402 // mov\ts2, v0.s[1]\n\tWORD $0x7100011f // cmp\tw8, #0\n\tWORD $0x1e212801 // fadd\ts1, s0, s1\n\tWORD $0x1e212841 // fadd\ts1, s2, s1\n\tWORD $0x5e140402 // mov\ts2, v0.s[2]\n\tWORD $0x5e1c0400 // mov\ts0, v0.s[3]\n\tWORD $0x1e212841 // fadd\ts1, s2, s1\n\tWORD $0x1e212800 // fadd\ts0, s0, s1\n\tBLE  LBB10_14\n\tWORD $0x92407d09 // and\tx9, x8, #0xffffffff\n\tWORD $0xf100213f // cmp\tx9, #8\n\tBHS  LBB10_9\n\tWORD $0xaa1f03e8 // mov\tx8, xzr\n\tB    LBB10_12\n\nLBB10_9:\n\tWORD $0x9240090a // and\tx10, x8, #0x7\n\tWORD $0x9100400b // add\tx11, x0, #16\n\tWORD $0x9100402c // add\tx12, x1, #16\n\tWORD $0xcb0a0128 // sub\tx8, x9, x10\n\tWORD $0xaa0803ed // mov\tx13, x8\n\nLBB10_10:\n\tWORD $0xad7f9181 // ldp\tq1, q4, [x12, #-16]\n\tWORD $0xf10021ad // subs\tx13, x13, #8\n\tWORD $0xad7f8d62 // ldp\tq2, q3, [x11, #-16]\n\tWORD $0x9100816b // add\tx11, x11, #32\n\tWORD $0x9100818c // add\tx12, x12, #32\n\tWORD $0x6e21dc41 // fmul\tv1.4s, v2.4s, v1.4s\n\tWORD $0x5e0c0422 // mov\ts2, v1.s[1]\n\tWORD $0x1e212800 // fadd\ts0, s0, s1\n\tWORD $0x5e140425 // mov\ts5, v1.s[2]\n\tWORD $0x5e1c0421 // mov\ts1, v1.s[3]\n\tWORD $0x1e222800 // fadd\ts0, s0, s2\n\tWORD $0x6e24dc62 // fmul\tv2.4s, v3.4s, v4.4s\n\tWORD $0x1e252800 // fadd\ts0, s0, s5\n\tWORD $0x5e140443 // mov\ts3, v2.s[2]\n\tWORD $0x1e212800 // fadd\ts0, s0, s1\n\tWORD $0x5e0c0441 // mov\ts1, v2.s[1]\n\tWORD $0x1e222800 // fadd\ts0, s0, s2\n\tWORD $0x1e212800 // fadd\ts0, s0, s1\n\tWORD $0x5e1c0441 // mov\ts1, v2.s[3]\n\tWORD $0x1e232800 // fadd\ts0, s0, s3\n\tWORD $0x1e212800 // fadd\ts0, s0, s1\n\tBNE  LBB10_10\n\tWORD $0xb400014a // cbz\tx10, .LBB10_14\n\nLBB10_12:\n\tWORD $0xd37ef50a // lsl\tx10, x8, #2\n\tWORD $0xcb080128 // sub\tx8, x9, x8\n\tWORD $0x8b0a0029 // add\tx9, x1, x10\n\tWORD $0x8b0a000a // add\tx10, x0, x10\n\nLBB10_13:\n\tWORD $0xbc404541 // ldr\ts1, [x10], #4\n\tWORD $0xf1000508 // subs\tx8, x8, #1\n\tWORD $0xbc404522 // ldr\ts2, [x9], #4\n\tWORD $0x1f020020 // fmadd\ts0, s1, s2, s0\n\tBNE  LBB10_13\n\nLBB10_14:\n\tWORD  $0xa8c17bfd       // ldp\tx29, x30, [sp], #16\n\tFMOVS F0, result+24(FP)\n\tRET\n\nTEXT ·veuclidean(SB), $8-24\n\tMOVD a+0(FP), R0\n\tMOVD b+8(FP), R1\n\tMOVD n+16(FP), R2\n\tWORD $0xa9bf7bfd  // stp\tx29, x30, [sp, #-16]!\n\tWORD $0x910003fd  // mov\tx29, sp\n\tWORD $0xd10043ff  // sub\tsp, sp, #16\n\tWORD $0x91000c48  // add\tx8, x2, #3\n\tWORD $0xf100005f  // cmp\tx2, #0\n\tWORD $0x9a82b108  // csel\tx8, x8, x2, lt\n\tWORD $0x9342fd0a  // asr\tx10, x8, #2\n\tWORD $0x927ef508  // and\tx8, x8, #0xfffffffffffffffc\n\tWORD $0xcb080049  // sub\tx9, x2, x8\n\tWORD $0x7100055f  // cmp\tw10, #1\n\tBLT  LBB11_5\n\tWORD $0x3cc10400  // ldr\tq0, [x0], #16\n\tWORD $0x71000548  // subs\tw8, w10, #1\n\tWORD $0x3cc10421  // ldr\tq1, [x1], #16\n\tWORD $0x4ea1d400  // fsub\tv0.4s, v0.4s, v1.4s\n\tWORD $0x6e20dc00  // fmul\tv0.4s, v0.4s, v0.4s\n\tBEQ  LBB11_6\n\tWORD $0xb27b7beb  // mov\tx11, #68719476704\n\tWORD $0xaa0103ec  // mov\tx12, x1\n\tWORD $0x8b0a116a  // add\tx10, x11, x10, lsl #4\n\tWORD $0x927c7d4a  // and\tx10, x10, #0xffffffff0\n\tWORD $0x9100414b  // add\tx11, x10, #16\n\tWORD $0x8b0b000a  // add\tx10, x0, x11\n\nLBB11_3:\n\tWORD $0x3cc10401 // ldr\tq1, [x0], #16\n\tWORD $0x71000508 // subs\tw8, w8, #1\n\tWORD $0x3cc10582 // ldr\tq2, [x12], #16\n\tWORD $0x4ea2d421 // fsub\tv1.4s, v1.4s, v2.4s\n\tWORD $0x4e21cc20 // fmla\tv0.4s, v1.4s, v1.4s\n\tBNE  LBB11_3\n\tWORD $0x8b0b0021 // add\tx1, x1, x11\n\tWORD $0xaa0a03e0 // mov\tx0, x10\n\tB    LBB11_6\n\nLBB11_5:\n\tWORD $0x6f00e400 // movi\tv0.2d, #0000000000000000\n\nLBB11_6:\n\tWORD $0x7e30d801 // faddp\ts1, v0.2s\n\tWORD $0x5e140402 // mov\ts2, v0.s[2]\n\tWORD $0x7100013f // cmp\tw9, #0\n\tWORD $0x5e1c0400 // mov\ts0, v0.s[3]\n\tWORD $0x1e212841 // fadd\ts1, s2, s1\n\tWORD $0x1e212800 // fadd\ts0, s0, s1\n\tWORD $0xbd000be0 // str\ts0, [sp, #8]\n\tBLE  LBB11_15\n\tWORD $0x92407d28 // and\tx8, x9, #0xffffffff\n\tWORD $0xf100211f // cmp\tx8, #8\n\tBHS  LBB11_9\n\tWORD $0xaa1f03e9 // mov\tx9, xzr\n\tB    LBB11_12\n\nLBB11_9:\n\tWORD $0x9240092a // and\tx10, x9, #0x7\n\tWORD $0x9100400b // add\tx11, x0, #16\n\tWORD $0x9100402c // add\tx12, x1, #16\n\tWORD $0xcb0a0109 // sub\tx9, x8, x10\n\tWORD $0xaa0903ed // mov\tx13, x9\n\nLBB11_10:\n\tWORD $0xad7f9181 // ldp\tq1, q4, [x12, #-16]\n\tWORD $0xf10021ad // subs\tx13, x13, #8\n\tWORD $0xad7f8d62 // ldp\tq2, q3, [x11, #-16]\n\tWORD $0x9100816b // add\tx11, x11, #32\n\tWORD $0x9100818c // add\tx12, x12, #32\n\tWORD $0x4ea1d441 // fsub\tv1.4s, v2.4s, v1.4s\n\tWORD $0x6e21dc21 // fmul\tv1.4s, v1.4s, v1.4s\n\tWORD $0x5e0c0422 // mov\ts2, v1.s[1]\n\tWORD $0x1e212800 // fadd\ts0, s0, s1\n\tWORD $0x5e140425 // mov\ts5, v1.s[2]\n\tWORD $0x5e1c0421 // mov\ts1, v1.s[3]\n\tWORD $0x1e222800 // fadd\ts0, s0, s2\n\tWORD $0x4ea4d462 // fsub\tv2.4s, v3.4s, v4.4s\n\tWORD $0x1e252800 // fadd\ts0, s0, s5\n\tWORD $0x6e22dc42 // fmul\tv2.4s, v2.4s, v2.4s\n\tWORD $0x1e212800 // fadd\ts0, s0, s1\n\tWORD $0x5e0c0441 // mov\ts1, v2.s[1]\n\tWORD $0x5e140443 // mov\ts3, v2.s[2]\n\tWORD $0x1e222800 // fadd\ts0, s0, s2\n\tWORD $0x1e212800 // fadd\ts0, s0, s1\n\tWORD $0x5e1c0441 // mov\ts1, v2.s[3]\n\tWORD $0x1e232800 // fadd\ts0, s0, s3\n\tWORD $0x1e212800 // fadd\ts0, s0, s1\n\tBNE  LBB11_10\n\tWORD $0xb400016a // cbz\tx10, .LBB11_14\n\nLBB11_12:\n\tWORD $0xd37ef52b // lsl\tx11, x9, #2\n\tWORD $0xcb090108 // sub\tx8, x8, x9\n\tWORD $0x8b0b002a // add\tx10, x1, x11\n\tWORD $0x8b0b000b // add\tx11, x0, x11\n\nLBB11_13:\n\tWORD $0xbc404561 // ldr\ts1, [x11], #4\n\tWORD $0xf1000508 // subs\tx8, x8, #1\n\tWORD $0xbc404542 // ldr\ts2, [x10], #4\n\tWORD $0x1e223821 // fsub\ts1, s1, s2\n\tWORD $0x1f010020 // fmadd\ts0, s1, s1, s0\n\tBNE  LBB11_13\n\nLBB11_14:\n\tWORD $0xbd000be0 // str\ts0, [sp, #8]\n\nLBB11_15:\n\tWORD  $0xfd4007e0       // ldr\td0, [sp, #8]\n\tWORD  $0x2ea1f800       // fsqrt\tv0.2s, v0.2s\n\tWORD  $0x910043ff       // add\tsp, sp, #16\n\tWORD  $0xa8c17bfd       // ldp\tx29, x30, [sp], #16\n\tFMOVS F0, result+24(FP)\n\tRET\n\nTEXT ·vmm(SB), $24-88\n\tMOVD transA+0(FP), R0\n\tMOVD transB+1(FP), R1\n\tMOVD m+8(FP), R2\n\tMOVD n+16(FP), R3\n\tMOVD k+24(FP), R4\n\tMOVD a+32(FP), R5\n\tMOVD lda+40(FP), R6\n\tMOVD b+48(FP), R7\n\tMOVD ldb+56(FP), R8\n\tMOVD R8, 0(RSP)\n\tMOVD c+64(FP), R8\n\tMOVD R8, 8(RSP)\n\tMOVD ldc+72(FP), R8\n\tMOVD R8, 16(RSP)\n\tWORD $0xa9ba7bfd      // stp\tx29, x30, [sp, #-96]!\n\tWORD $0x910003fd      // mov\tx29, sp\n\tWORD $0xa9016ffc      // stp\tx28, x27, [sp, #16]\n\tWORD $0xa946a7a8      // ldp\tx8, x9, [x29, #104]\n\tWORD $0xf94033ab      // ldr\tx11, [x29, #96]\n\tWORD $0xa90267fa      // stp\tx26, x25, [sp, #32]\n\tWORD $0xa9035ff8      // stp\tx24, x23, [sp, #48]\n\tWORD $0xa90457f6      // stp\tx22, x21, [sp, #64]\n\tWORD $0xa9054ff4      // stp\tx20, x19, [sp, #80]\n\tWORD $0x37000a80      // tbnz\tw0, #0, .LBB12_16\n\tWORD $0x37000a61      // tbnz\tw1, #0, .LBB12_16\n\tWORD $0xf100045f      // cmp\tx2, #1\n\tBLT  LBB12_101\n\tWORD $0xf100048c      // subs\tx12, x4, #1\n\tBLT  LBB12_101\n\tWORD $0xf100047f      // cmp\tx3, #1\n\tBLT  LBB12_101\n\tWORD $0x9b0b0d8f      // madd\tx15, x12, x11, x3\n\tWORD $0xd37ef529      // lsl\tx9, x9, #2\n\tWORD $0xd37ef4cc      // lsl\tx12, x6, #2\n\tWORD $0x8b03090d      // add\tx13, x8, x3, lsl #2\n\tWORD $0x8b0408ae      // add\tx14, x5, x4, lsl #2\n\tWORD $0xd37ef571      // lsl\tx17, x11, #2\n\tWORD $0xd37df56b      // ubfx\tx11, x11, #61, #1\n\tWORD $0xaa1f03ea      // mov\tx10, xzr\n\tWORD $0x927df070      // and\tx16, x3, #0xfffffffffffffff8\n\tWORD $0x910040e0      // add\tx0, x7, #16\n\tWORD $0x91004101      // add\tx1, x8, #16\n\tWORD $0xaa0803f3      // mov\tx19, x8\n\tWORD $0x8b0f08ef      // add\tx15, x7, x15, lsl #2\n\tB    LBB12_7\n\nLBB12_6:\n\tWORD $0x9100054a // add\tx10, x10, #1\n\tWORD $0x8b090021 // add\tx1, x1, x9\n\tWORD $0x8b090273 // add\tx19, x19, x9\n\tWORD $0xeb02015f // cmp\tx10, x2\n\tBEQ  LBB12_101\n\nLBB12_7:\n\tWORD $0x9b0a7d35 // mul\tx21, x9, x10\n\tWORD $0xaa1f03f4 // mov\tx20, xzr\n\tWORD $0x9b0a7d96 // mul\tx22, x12, x10\n\tWORD $0x9b067d57 // mul\tx23, x10, x6\n\tWORD $0x8b150118 // add\tx24, x8, x21\n\tWORD $0x8b1501b9 // add\tx25, x13, x21\n\tWORD $0xeb0f031f // cmp\tx24, x15\n\tWORD $0x8b1601d5 // add\tx21, x14, x22\n\tWORD $0xfa473320 // ccmp\tx25, x7, #0, lo\n\tWORD $0x8b1600b6 // add\tx22, x5, x22\n\tWORD $0x1a9f957a // csinc\tw26, w11, wzr, ls\n\tWORD $0xeb15031f // cmp\tx24, x21\n\tWORD $0xaa0003f8 // mov\tx24, x0\n\tWORD $0x8b1708b5 // add\tx21, x5, x23, lsl #2\n\tWORD $0xfa5932c2 // ccmp\tx22, x25, #2, lo\n\tWORD $0xaa0703f7 // mov\tx23, x7\n\tWORD $0x1a9f2756 // csinc\tw22, w26, wzr, hs\n\tB    LBB12_9\n\nLBB12_8:\n\tWORD $0x91000694 // add\tx20, x20, #1\n\tWORD $0x8b110318 // add\tx24, x24, x17\n\tWORD $0x8b1102f7 // add\tx23, x23, x17\n\tWORD $0xeb04029f // cmp\tx20, x4\n\tBEQ  LBB12_6\n\nLBB12_9:\n\tWORD $0xf100207f // cmp\tx3, #8\n\tWORD $0x1a9f26d9 // csinc\tw25, w22, wzr, hs\n\tWORD $0x36000079 // tbz\tw25, #0, .LBB12_11\n\tWORD $0xaa1f03fb // mov\tx27, xzr\n\tB    LBB12_14\n\nLBB12_11:\n\tWORD $0x8b140ab9 // add\tx25, x21, x20, lsl #2\n\tWORD $0xaa0103fa // mov\tx26, x1\n\tWORD $0xaa1803fb // mov\tx27, x24\n\tWORD $0x4d40cb20 // ld1r\t{ v0.4s }, [x25]\n\tWORD $0xaa1003f9 // mov\tx25, x16\n\nLBB12_12:\n\tWORD $0xad7f9341 // ldp\tq1, q4, [x26, #-16]\n\tWORD $0xf1002339 // subs\tx25, x25, #8\n\tWORD $0xad7f8f62 // ldp\tq2, q3, [x27, #-16]\n\tWORD $0x9100837b // add\tx27, x27, #32\n\tWORD $0x4e20cc41 // fmla\tv1.4s, v2.4s, v0.4s\n\tWORD $0x4e20cc64 // fmla\tv4.4s, v3.4s, v0.4s\n\tWORD $0xad3f9341 // stp\tq1, q4, [x26, #-16]\n\tWORD $0x9100835a // add\tx26, x26, #32\n\tBNE  LBB12_12\n\tWORD $0xeb03021f // cmp\tx16, x3\n\tWORD $0xaa1003fb // mov\tx27, x16\n\tBEQ  LBB12_8\n\nLBB12_14:\n\tWORD $0xd37ef77a // lsl\tx26, x27, #2\n\tWORD $0xcb1b007b // sub\tx27, x3, x27\n\tWORD $0x8b1a02f9 // add\tx25, x23, x26\n\tWORD $0x8b1a027a // add\tx26, x19, x26\n\nLBB12_15:\n\tWORD $0xbc747aa0 // ldr\ts0, [x21, x20, lsl #2]\n\tWORD $0xbc404721 // ldr\ts1, [x25], #4\n\tWORD $0xbd400342 // ldr\ts2, [x26]\n\tWORD $0xf100077b // subs\tx27, x27, #1\n\tWORD $0x1f010800 // fmadd\ts0, s0, s1, s2\n\tWORD $0xbc004740 // str\ts0, [x26], #4\n\tBNE  LBB12_15\n\tB    LBB12_8\n\nLBB12_16:\n\tWORD $0x36000ca1 // tbz\tw1, #0, .LBB12_34\n\tWORD $0x37000c80 // tbnz\tw0, #0, .LBB12_34\n\tWORD $0xf100045f // cmp\tx2, #1\n\tBLT  LBB12_101\n\tWORD $0x91000c8a // add\tx10, x4, #3\n\tWORD $0xf100009f // cmp\tx4, #0\n\tWORD $0x9a84b14a // csel\tx10, x10, x4, lt\n\tWORD $0xf100047f // cmp\tx3, #1\n\tBLT  LBB12_101\n\tWORD $0x9342fd4f // asr\tx15, x10, #2\n\tWORD $0x927ef54a // and\tx10, x10, #0xfffffffffffffffc\n\tWORD $0xcb0a008c // sub\tx12, x4, x10\n\tWORD $0x710005ed // subs\tw13, w15, #1\n\tWORD $0x92407d8a // and\tx10, x12, #0xffffffff\n\tBLT  LBB12_62\n\tBNE  LBB12_74\n\tWORD $0x7100019f // cmp\tw12, #0\n\tBLE  LBB12_97\n\tWORD $0x2f00e400 // movi\td0, #0000000000000000\n\tWORD $0x9240098c // and\tx12, x12, #0x7\n\tWORD $0xd37ef4d0 // lsl\tx16, x6, #2\n\tWORD $0xd37ef571 // lsl\tx17, x11, #2\n\tWORD $0xaa1f03ed // mov\tx13, xzr\n\tWORD $0xcb0c014e // sub\tx14, x10, x12\n\tWORD $0x910080af // add\tx15, x5, #32\n\tWORD $0x910080e0 // add\tx0, x7, #32\n\tWORD $0x910040e1 // add\tx1, x7, #16\n\tWORD $0x910040a4 // add\tx4, x5, #16\n\tB    LBB12_25\n\nLBB12_24:\n\tWORD $0x910005ad // add\tx13, x13, #1\n\tWORD $0x8b1001ef // add\tx15, x15, x16\n\tWORD $0x8b100084 // add\tx4, x4, x16\n\tWORD $0xeb0201bf // cmp\tx13, x2\n\tBEQ  LBB12_101\n\nLBB12_25:\n\tWORD $0x9b067db4 // mul\tx20, x13, x6\n\tWORD $0xaa1f03f3 // mov\tx19, xzr\n\tWORD $0xaa0103f6 // mov\tx22, x1\n\tWORD $0xaa0003f7 // mov\tx23, x0\n\tWORD $0x9b097db5 // mul\tx21, x13, x9\n\tWORD $0x8b1408b4 // add\tx20, x5, x20, lsl #2\n\tWORD $0x8b150915 // add\tx21, x8, x21, lsl #2\n\tB    LBB12_27\n\nLBB12_26:\n\tWORD $0xbc337aa1 // str\ts1, [x21, x19, lsl #2]\n\tWORD $0x91000673 // add\tx19, x19, #1\n\tWORD $0x8b1102f7 // add\tx23, x23, x17\n\tWORD $0xeb03027f // cmp\tx19, x3\n\tWORD $0x8b1102d6 // add\tx22, x22, x17\n\tBEQ  LBB12_24\n\nLBB12_27:\n\tWORD $0x9b0b7e78 // mul\tx24, x19, x11\n\tWORD $0x3dc00281 // ldr\tq1, [x20]\n\tWORD $0xf100215f // cmp\tx10, #8\n\tWORD $0xd37ef718 // lsl\tx24, x24, #2\n\tWORD $0x3cf868e2 // ldr\tq2, [x7, x24]\n\tWORD $0x6e22dc21 // fmul\tv1.4s, v1.4s, v2.4s\n\tWORD $0x1e202822 // fadd\ts2, s1, s0\n\tWORD $0x5e0c0423 // mov\ts3, v1.s[1]\n\tWORD $0x1e222862 // fadd\ts2, s3, s2\n\tWORD $0x5e140423 // mov\ts3, v1.s[2]\n\tWORD $0x5e1c0421 // mov\ts1, v1.s[3]\n\tWORD $0x1e222862 // fadd\ts2, s3, s2\n\tWORD $0x1e222821 // fadd\ts1, s1, s2\n\tBHS  LBB12_29\n\tWORD $0xaa1f03f8 // mov\tx24, xzr\n\tB    LBB12_32\n\nLBB12_29:\n\tWORD $0xaa1703f8 // mov\tx24, x23\n\tWORD $0xaa0f03f9 // mov\tx25, x15\n\tWORD $0xaa0e03fa // mov\tx26, x14\n\nLBB12_30:\n\tWORD $0xad7f9702 // ldp\tq2, q5, [x24, #-16]\n\tWORD $0xf100235a // subs\tx26, x26, #8\n\tWORD $0xad7f9323 // ldp\tq3, q4, [x25, #-16]\n\tWORD $0x91008339 // add\tx25, x25, #32\n\tWORD $0x91008318 // add\tx24, x24, #32\n\tWORD $0x6e22dc62 // fmul\tv2.4s, v3.4s, v2.4s\n\tWORD $0x5e0c0443 // mov\ts3, v2.s[1]\n\tWORD $0x1e222821 // fadd\ts1, s1, s2\n\tWORD $0x5e140446 // mov\ts6, v2.s[2]\n\tWORD $0x5e1c0442 // mov\ts2, v2.s[3]\n\tWORD $0x1e232821 // fadd\ts1, s1, s3\n\tWORD $0x6e25dc83 // fmul\tv3.4s, v4.4s, v5.4s\n\tWORD $0x1e262821 // fadd\ts1, s1, s6\n\tWORD $0x5e140464 // mov\ts4, v3.s[2]\n\tWORD $0x1e222821 // fadd\ts1, s1, s2\n\tWORD $0x5e0c0462 // mov\ts2, v3.s[1]\n\tWORD $0x1e232821 // fadd\ts1, s1, s3\n\tWORD $0x1e222821 // fadd\ts1, s1, s2\n\tWORD $0x5e1c0462 // mov\ts2, v3.s[3]\n\tWORD $0x1e242821 // fadd\ts1, s1, s4\n\tWORD $0x1e222821 // fadd\ts1, s1, s2\n\tBNE  LBB12_30\n\tWORD $0xaa0e03f8 // mov\tx24, x14\n\tWORD $0xb4fffa0c // cbz\tx12, .LBB12_26\n\nLBB12_32:\n\tWORD $0xd37ef71a // lsl\tx26, x24, #2\n\tWORD $0xcb180158 // sub\tx24, x10, x24\n\tWORD $0x8b1a02d9 // add\tx25, x22, x26\n\tWORD $0x8b1a009a // add\tx26, x4, x26\n\nLBB12_33:\n\tWORD $0xbc404742 // ldr\ts2, [x26], #4\n\tWORD $0xf1000718 // subs\tx24, x24, #1\n\tWORD $0xbc404723 // ldr\ts3, [x25], #4\n\tWORD $0x1f030441 // fmadd\ts1, s2, s3, s1\n\tBNE  LBB12_33\n\tB    LBB12_26\n\nLBB12_34:\n\tWORD $0xf100009f // cmp\tx4, #0\n\tWORD $0xfa40c864 // ccmp\tx3, #0, #4, gt\n\tWORD $0x1a9fd7ea // cset\tw10, gt\n\tWORD $0x36000aa0 // tbz\tw0, #0, .LBB12_48\n\tWORD $0x37000a81 // tbnz\tw1, #0, .LBB12_48\n\tWORD $0xf100045f // cmp\tx2, #1\n\tWORD $0x5200014a // eor\tw10, w10, #0x1\n\tWORD $0x1a9fa54a // csinc\tw10, w10, wzr, ge\n\tWORD $0x370030ea // tbnz\tw10, #0, .LBB12_101\n\tWORD $0xd100048c // sub\tx12, x4, #1\n\tWORD $0xd37ef529 // lsl\tx9, x9, #2\n\tWORD $0xd37ef56e // lsl\tx14, x11, #2\n\tWORD $0x9b067d8d // mul\tx13, x12, x6\n\tWORD $0xd37df4cf // ubfx\tx15, x6, #61, #1\n\tWORD $0xaa1f03ea // mov\tx10, xzr\n\tWORD $0x910040e0 // add\tx0, x7, #16\n\tWORD $0x91004101 // add\tx1, x8, #16\n\tWORD $0xaa0803f3 // mov\tx19, x8\n\tWORD $0x9b0b0d90 // madd\tx16, x12, x11, x3\n\tWORD $0x8b03090c // add\tx12, x8, x3, lsl #2\n\tWORD $0xd37df56b // ubfx\tx11, x11, #61, #1\n\tWORD $0x8b0d08b1 // add\tx17, x5, x13, lsl #2\n\tWORD $0x927df06d // and\tx13, x3, #0xfffffffffffffff8\n\tWORD $0x8b1008f0 // add\tx16, x7, x16, lsl #2\n\tWORD $0x91001231 // add\tx17, x17, #4\n\tB    LBB12_39\n\nLBB12_38:\n\tWORD $0x9100054a // add\tx10, x10, #1\n\tWORD $0x8b090021 // add\tx1, x1, x9\n\tWORD $0x8b090273 // add\tx19, x19, x9\n\tWORD $0xeb02015f // cmp\tx10, x2\n\tBEQ  LBB12_101\n\nLBB12_39:\n\tWORD $0x9b0a7d35 // mul\tx21, x9, x10\n\tWORD $0xd37ef556 // lsl\tx22, x10, #2\n\tWORD $0xaa1f03f4 // mov\tx20, xzr\n\tWORD $0x8b160238 // add\tx24, x17, x22\n\tWORD $0x8b150117 // add\tx23, x8, x21\n\tWORD $0x8b150199 // add\tx25, x12, x21\n\tWORD $0x8b1600b5 // add\tx21, x5, x22\n\tWORD $0xeb1802ff // cmp\tx23, x24\n\tWORD $0xaa0003f8 // mov\tx24, x0\n\tWORD $0xfa5932a2 // ccmp\tx21, x25, #2, lo\n\tWORD $0x1a9f25f6 // csinc\tw22, w15, wzr, hs\n\tWORD $0xeb1002ff // cmp\tx23, x16\n\tWORD $0xfa473320 // ccmp\tx25, x7, #0, lo\n\tWORD $0x1a9f9577 // csinc\tw23, w11, wzr, ls\n\tWORD $0x2a1702d6 // orr\tw22, w22, w23\n\tWORD $0xaa0703f7 // mov\tx23, x7\n\tB    LBB12_41\n\nLBB12_40:\n\tWORD $0x91000694 // add\tx20, x20, #1\n\tWORD $0x8b0e0318 // add\tx24, x24, x14\n\tWORD $0x8b0e02f7 // add\tx23, x23, x14\n\tWORD $0xeb04029f // cmp\tx20, x4\n\tBEQ  LBB12_38\n\nLBB12_41:\n\tWORD $0x9b067e99 // mul\tx25, x20, x6\n\tWORD $0xf100207f // cmp\tx3, #8\n\tWORD $0x1a9f26da // csinc\tw26, w22, wzr, hs\n\tWORD $0x3600007a // tbz\tw26, #0, .LBB12_43\n\tWORD $0xaa1f03fc // mov\tx28, xzr\n\tB    LBB12_46\n\nLBB12_43:\n\tWORD $0x8b190aba // add\tx26, x21, x25, lsl #2\n\tWORD $0xaa0103fb // mov\tx27, x1\n\tWORD $0xaa1803fc // mov\tx28, x24\n\tWORD $0x4d40cb40 // ld1r\t{ v0.4s }, [x26]\n\tWORD $0xaa0d03fa // mov\tx26, x13\n\nLBB12_44:\n\tWORD $0xad7f9361 // ldp\tq1, q4, [x27, #-16]\n\tWORD $0xf100235a // subs\tx26, x26, #8\n\tWORD $0xad7f8f82 // ldp\tq2, q3, [x28, #-16]\n\tWORD $0x9100839c // add\tx28, x28, #32\n\tWORD $0x4e20cc41 // fmla\tv1.4s, v2.4s, v0.4s\n\tWORD $0x4e20cc64 // fmla\tv4.4s, v3.4s, v0.4s\n\tWORD $0xad3f9361 // stp\tq1, q4, [x27, #-16]\n\tWORD $0x9100837b // add\tx27, x27, #32\n\tBNE  LBB12_44\n\tWORD $0xeb0301bf // cmp\tx13, x3\n\tWORD $0xaa0d03fc // mov\tx28, x13\n\tBEQ  LBB12_40\n\nLBB12_46:\n\tWORD $0xd37ef79b // lsl\tx27, x28, #2\n\tWORD $0xcb1c007c // sub\tx28, x3, x28\n\tWORD $0x8b1b02fa // add\tx26, x23, x27\n\tWORD $0x8b1b027b // add\tx27, x19, x27\n\nLBB12_47:\n\tWORD $0xbc797aa0 // ldr\ts0, [x21, x25, lsl #2]\n\tWORD $0xbc404741 // ldr\ts1, [x26], #4\n\tWORD $0xbd400362 // ldr\ts2, [x27]\n\tWORD $0xf100079c // subs\tx28, x28, #1\n\tWORD $0x1f010800 // fmadd\ts0, s0, s1, s2\n\tWORD $0xbc004760 // str\ts0, [x27], #4\n\tBNE  LBB12_47\n\tB    LBB12_40\n\nLBB12_48:\n\tWORD $0x0a01014a // and\tw10, w10, w1\n\tWORD $0x7100055f // cmp\tw10, #1\n\tBNE  LBB12_101\n\tWORD $0xf100045f // cmp\tx2, #1\n\tBLT  LBB12_101\n\tWORD $0x36002640 // tbz\tw0, #0, .LBB12_101\n\tWORD $0xd100048c // sub\tx12, x4, #1\n\tWORD $0x8b030090 // add\tx16, x4, x3\n\tWORD $0xf1001c7f // cmp\tx3, #7\n\tWORD $0x9b067d8f // mul\tx15, x12, x6\n\tWORD $0x8b1008f1 // add\tx17, x7, x16, lsl #2\n\tWORD $0xfa418960 // ccmp\tx11, #1, #0, hi\n\tWORD $0xd37ef529 // lsl\tx9, x9, #2\n\tWORD $0x8b03090c // add\tx12, x8, x3, lsl #2\n\tWORD $0xd37df4d0 // ubfx\tx16, x6, #61, #1\n\tWORD $0x1a9f17e1 // cset\tw1, eq\n\tWORD $0xaa1f03ea // mov\tx10, xzr\n\tWORD $0x927df06d // and\tx13, x3, #0xfffffffffffffff8\n\tWORD $0x910040ee // add\tx14, x7, #16\n\tWORD $0x52000021 // eor\tw1, w1, #0x1\n\tWORD $0xaa0803f3 // mov\tx19, x8\n\tWORD $0x8b0f08a0 // add\tx0, x5, x15, lsl #2\n\tWORD $0xd37ef56f // lsl\tx15, x11, #2\n\tWORD $0xd100122b // sub\tx11, x17, #4\n\tWORD $0x91001011 // add\tx17, x0, #4\n\tWORD $0x91004100 // add\tx0, x8, #16\n\tB    LBB12_53\n\nLBB12_52:\n\tWORD $0x9100054a // add\tx10, x10, #1\n\tWORD $0x8b090000 // add\tx0, x0, x9\n\tWORD $0x8b090273 // add\tx19, x19, x9\n\tWORD $0xeb02015f // cmp\tx10, x2\n\tBEQ  LBB12_101\n\nLBB12_53:\n\tWORD $0x9b0a7d35 // mul\tx21, x9, x10\n\tWORD $0xd37ef556 // lsl\tx22, x10, #2\n\tWORD $0xaa1f03f4 // mov\tx20, xzr\n\tWORD $0x8b160238 // add\tx24, x17, x22\n\tWORD $0x8b150117 // add\tx23, x8, x21\n\tWORD $0x8b150199 // add\tx25, x12, x21\n\tWORD $0x8b1600b5 // add\tx21, x5, x22\n\tWORD $0xeb1802ff // cmp\tx23, x24\n\tWORD $0xaa0e03f8 // mov\tx24, x14\n\tWORD $0xfa5932a2 // ccmp\tx21, x25, #2, lo\n\tWORD $0x1a9f2616 // csinc\tw22, w16, wzr, hs\n\tWORD $0xeb0b02ff // cmp\tx23, x11\n\tWORD $0xaa0703f7 // mov\tx23, x7\n\tWORD $0xfa473320 // ccmp\tx25, x7, #0, lo\n\tWORD $0x1a9f96d6 // csinc\tw22, w22, wzr, ls\n\tWORD $0x2a160036 // orr\tw22, w1, w22\n\tB    LBB12_55\n\nLBB12_54:\n\tWORD $0x91000694 // add\tx20, x20, #1\n\tWORD $0x91001318 // add\tx24, x24, #4\n\tWORD $0x910012f7 // add\tx23, x23, #4\n\tWORD $0xeb04029f // cmp\tx20, x4\n\tBEQ  LBB12_52\n\nLBB12_55:\n\tWORD $0x9b067e99 // mul\tx25, x20, x6\n\tWORD $0x36000076 // tbz\tw22, #0, .LBB12_57\n\tWORD $0xaa1f03fc // mov\tx28, xzr\n\tB    LBB12_60\n\nLBB12_57:\n\tWORD $0x8b190aba // add\tx26, x21, x25, lsl #2\n\tWORD $0xaa0003fb // mov\tx27, x0\n\tWORD $0xaa1803fc // mov\tx28, x24\n\tWORD $0x4d40cb40 // ld1r\t{ v0.4s }, [x26]\n\tWORD $0xaa0d03fa // mov\tx26, x13\n\nLBB12_58:\n\tWORD $0xad7f9361 // ldp\tq1, q4, [x27, #-16]\n\tWORD $0xf100235a // subs\tx26, x26, #8\n\tWORD $0xad7f8f82 // ldp\tq2, q3, [x28, #-16]\n\tWORD $0x9100839c // add\tx28, x28, #32\n\tWORD $0x4e20cc41 // fmla\tv1.4s, v2.4s, v0.4s\n\tWORD $0x4e20cc64 // fmla\tv4.4s, v3.4s, v0.4s\n\tWORD $0xad3f9361 // stp\tq1, q4, [x27, #-16]\n\tWORD $0x9100837b // add\tx27, x27, #32\n\tBNE  LBB12_58\n\tWORD $0xeb0301bf // cmp\tx13, x3\n\tWORD $0xaa0d03fc // mov\tx28, x13\n\tBEQ  LBB12_54\n\nLBB12_60:\n\tWORD $0x9b1c5dfa // madd\tx26, x15, x28, x23\n\tWORD $0x8b1c0a7b // add\tx27, x19, x28, lsl #2\n\tWORD $0xcb1c007c // sub\tx28, x3, x28\n\nLBB12_61:\n\tWORD $0xbc797aa0 // ldr\ts0, [x21, x25, lsl #2]\n\tWORD $0xbd400341 // ldr\ts1, [x26]\n\tWORD $0xf100079c // subs\tx28, x28, #1\n\tWORD $0xbd400362 // ldr\ts2, [x27]\n\tWORD $0x8b0f035a // add\tx26, x26, x15\n\tWORD $0x1f010800 // fmadd\ts0, s0, s1, s2\n\tWORD $0xbc004760 // str\ts0, [x27], #4\n\tBNE  LBB12_61\n\tB    LBB12_54\n\nLBB12_62:\n\tWORD $0x7100019f // cmp\tw12, #0\n\tBLE  LBB12_88\n\tWORD $0x9240098c // and\tx12, x12, #0x7\n\tWORD $0xd37ef4ce // lsl\tx14, x6, #2\n\tWORD $0xd37ef56b // lsl\tx11, x11, #2\n\tWORD $0xaa1f03ed // mov\tx13, xzr\n\tWORD $0xcb0c014f // sub\tx15, x10, x12\n\tWORD $0x910040b0 // add\tx16, x5, #16\n\tWORD $0x910040f1 // add\tx17, x7, #16\n\tB    LBB12_65\n\nLBB12_64:\n\tWORD $0x910005ad // add\tx13, x13, #1\n\tWORD $0x8b0e0210 // add\tx16, x16, x14\n\tWORD $0x8b0e00a5 // add\tx5, x5, x14\n\tWORD $0xeb0201bf // cmp\tx13, x2\n\tBEQ  LBB12_101\n\nLBB12_65:\n\tWORD $0x9b097da1 // mul\tx1, x13, x9\n\tWORD $0xaa1f03e0 // mov\tx0, xzr\n\tWORD $0xaa0703e4 // mov\tx4, x7\n\tWORD $0xaa1103e6 // mov\tx6, x17\n\tWORD $0x8b010901 // add\tx1, x8, x1, lsl #2\n\tB    LBB12_67\n\nLBB12_66:\n\tWORD $0xbc207820 // str\ts0, [x1, x0, lsl #2]\n\tWORD $0x91000400 // add\tx0, x0, #1\n\tWORD $0x8b0b00c6 // add\tx6, x6, x11\n\tWORD $0xeb03001f // cmp\tx0, x3\n\tWORD $0x8b0b0084 // add\tx4, x4, x11\n\tBEQ  LBB12_64\n\nLBB12_67:\n\tWORD $0x2f00e400 // movi\td0, #0000000000000000\n\tWORD $0xf100215f // cmp\tx10, #8\n\tBHS  LBB12_69\n\tWORD $0xaa1f03f3 // mov\tx19, xzr\n\tB    LBB12_72\n\nLBB12_69:\n\tWORD $0xaa0f03f3 // mov\tx19, x15\n\tWORD $0xaa0603f4 // mov\tx20, x6\n\tWORD $0xaa1003f5 // mov\tx21, x16\n\nLBB12_70:\n\tWORD $0xad7f9281 // ldp\tq1, q4, [x20, #-16]\n\tWORD $0xf1002273 // subs\tx19, x19, #8\n\tWORD $0xad7f8ea2 // ldp\tq2, q3, [x21, #-16]\n\tWORD $0x910082b5 // add\tx21, x21, #32\n\tWORD $0x91008294 // add\tx20, x20, #32\n\tWORD $0x6e21dc41 // fmul\tv1.4s, v2.4s, v1.4s\n\tWORD $0x5e0c0422 // mov\ts2, v1.s[1]\n\tWORD $0x1e212800 // fadd\ts0, s0, s1\n\tWORD $0x5e140425 // mov\ts5, v1.s[2]\n\tWORD $0x5e1c0421 // mov\ts1, v1.s[3]\n\tWORD $0x1e222800 // fadd\ts0, s0, s2\n\tWORD $0x6e24dc62 // fmul\tv2.4s, v3.4s, v4.4s\n\tWORD $0x1e252800 // fadd\ts0, s0, s5\n\tWORD $0x5e140443 // mov\ts3, v2.s[2]\n\tWORD $0x1e212800 // fadd\ts0, s0, s1\n\tWORD $0x5e0c0441 // mov\ts1, v2.s[1]\n\tWORD $0x1e222800 // fadd\ts0, s0, s2\n\tWORD $0x1e212800 // fadd\ts0, s0, s1\n\tWORD $0x5e1c0441 // mov\ts1, v2.s[3]\n\tWORD $0x1e232800 // fadd\ts0, s0, s3\n\tWORD $0x1e212800 // fadd\ts0, s0, s1\n\tBNE  LBB12_70\n\tWORD $0xaa0f03f3 // mov\tx19, x15\n\tWORD $0xb4fffb6c // cbz\tx12, .LBB12_66\n\nLBB12_72:\n\tWORD $0xd37ef675 // lsl\tx21, x19, #2\n\tWORD $0xcb130153 // sub\tx19, x10, x19\n\tWORD $0x8b150094 // add\tx20, x4, x21\n\tWORD $0x8b1500b5 // add\tx21, x5, x21\n\nLBB12_73:\n\tWORD $0xbc4046a1 // ldr\ts1, [x21], #4\n\tWORD $0xf1000673 // subs\tx19, x19, #1\n\tWORD $0xbc404682 // ldr\ts2, [x20], #4\n\tWORD $0x1f020020 // fmadd\ts0, s1, s2, s0\n\tBNE  LBB12_73\n\tB    LBB12_66\n\nLBB12_74:\n\tWORD $0xb27b7bf0 // mov\tx16, #68719476704\n\tWORD $0x2f00e400 // movi\td0, #0000000000000000\n\tWORD $0xd37ef571 // lsl\tx17, x11, #2\n\tWORD $0x8b0f1210 // add\tx16, x16, x15, lsl #4\n\tWORD $0x9240098f // and\tx15, x12, #0x7\n\tWORD $0xaa1f03ee // mov\tx14, xzr\n\tWORD $0xcb0f0140 // sub\tx0, x10, x15\n\tWORD $0x927c7e01 // and\tx1, x16, #0xffffffff0\n\tWORD $0xd37ef4d0 // lsl\tx16, x6, #2\n\tWORD $0x9100c024 // add\tx4, x1, #48\n\tWORD $0x91008034 // add\tx20, x1, #32\n\tWORD $0x8b0400a1 // add\tx1, x5, x4\n\tWORD $0x8b0400e4 // add\tx4, x7, x4\n\tWORD $0x8b1400f3 // add\tx19, x7, x20\n\tWORD $0x8b1400b4 // add\tx20, x5, x20\n\tB    LBB12_76\n\nLBB12_75:\n\tWORD $0x910005ce // add\tx14, x14, #1\n\tWORD $0x8b100021 // add\tx1, x1, x16\n\tWORD $0x8b100294 // add\tx20, x20, x16\n\tWORD $0xeb0201df // cmp\tx14, x2\n\tBEQ  LBB12_101\n\nLBB12_76:\n\tWORD $0x9b067dd6 // mul\tx22, x14, x6\n\tWORD $0xaa1f03f5 // mov\tx21, xzr\n\tWORD $0xaa1303f9 // mov\tx25, x19\n\tWORD $0xaa0403fa // mov\tx26, x4\n\tWORD $0x9b097dd7 // mul\tx23, x14, x9\n\tWORD $0x8b1608b6 // add\tx22, x5, x22, lsl #2\n\tWORD $0x8b170917 // add\tx23, x8, x23, lsl #2\n\tWORD $0x910042d8 // add\tx24, x22, #16\n\tB    LBB12_78\n\nLBB12_77:\n\tWORD $0xbc357ae1 // str\ts1, [x23, x21, lsl #2]\n\tWORD $0x910006b5 // add\tx21, x21, #1\n\tWORD $0x8b11035a // add\tx26, x26, x17\n\tWORD $0xeb0302bf // cmp\tx21, x3\n\tWORD $0x8b110339 // add\tx25, x25, x17\n\tBEQ  LBB12_75\n\nLBB12_78:\n\tWORD $0x9b0b7ebb // mul\tx27, x21, x11\n\tWORD $0x3dc002c1 // ldr\tq1, [x22]\n\tWORD $0x2a0d03fc // mov\tw28, w13\n\tWORD $0xaa1803fe // mov\tx30, x24\n\tWORD $0x8b1b08fb // add\tx27, x7, x27, lsl #2\n\tWORD $0x3cc10762 // ldr\tq2, [x27], #16\n\tWORD $0x6e22dc21 // fmul\tv1.4s, v1.4s, v2.4s\n\nLBB12_79:\n\tWORD $0x3cc107c2 // ldr\tq2, [x30], #16\n\tWORD $0x7100079c // subs\tw28, w28, #1\n\tWORD $0x3cc10763 // ldr\tq3, [x27], #16\n\tWORD $0x4e22cc61 // fmla\tv1.4s, v3.4s, v2.4s\n\tBNE  LBB12_79\n\tWORD $0x1e202822 // fadd\ts2, s1, s0\n\tWORD $0x5e0c0423 // mov\ts3, v1.s[1]\n\tWORD $0x7100059f // cmp\tw12, #1\n\tWORD $0x1e222862 // fadd\ts2, s3, s2\n\tWORD $0x5e140423 // mov\ts3, v1.s[2]\n\tWORD $0x5e1c0421 // mov\ts1, v1.s[3]\n\tWORD $0x1e222862 // fadd\ts2, s3, s2\n\tWORD $0x1e222821 // fadd\ts1, s1, s2\n\tBLT  LBB12_77\n\tWORD $0xf100215f // cmp\tx10, #8\n\tBHS  LBB12_83\n\tWORD $0xaa1f03fb // mov\tx27, xzr\n\tB    LBB12_86\n\nLBB12_83:\n\tWORD $0xaa1a03fb // mov\tx27, x26\n\tWORD $0xaa0103fc // mov\tx28, x1\n\tWORD $0xaa0003fe // mov\tx30, x0\n\nLBB12_84:\n\tWORD $0xad7f9762 // ldp\tq2, q5, [x27, #-16]\n\tWORD $0xf10023de // subs\tx30, x30, #8\n\tWORD $0xad7f9383 // ldp\tq3, q4, [x28, #-16]\n\tWORD $0x9100839c // add\tx28, x28, #32\n\tWORD $0x9100837b // add\tx27, x27, #32\n\tWORD $0x6e22dc62 // fmul\tv2.4s, v3.4s, v2.4s\n\tWORD $0x5e0c0443 // mov\ts3, v2.s[1]\n\tWORD $0x1e222821 // fadd\ts1, s1, s2\n\tWORD $0x5e140446 // mov\ts6, v2.s[2]\n\tWORD $0x5e1c0442 // mov\ts2, v2.s[3]\n\tWORD $0x1e232821 // fadd\ts1, s1, s3\n\tWORD $0x6e25dc83 // fmul\tv3.4s, v4.4s, v5.4s\n\tWORD $0x1e262821 // fadd\ts1, s1, s6\n\tWORD $0x5e140464 // mov\ts4, v3.s[2]\n\tWORD $0x1e222821 // fadd\ts1, s1, s2\n\tWORD $0x5e0c0462 // mov\ts2, v3.s[1]\n\tWORD $0x1e232821 // fadd\ts1, s1, s3\n\tWORD $0x1e222821 // fadd\ts1, s1, s2\n\tWORD $0x5e1c0462 // mov\ts2, v3.s[3]\n\tWORD $0x1e242821 // fadd\ts1, s1, s4\n\tWORD $0x1e222821 // fadd\ts1, s1, s2\n\tBNE  LBB12_84\n\tWORD $0xaa0003fb // mov\tx27, x0\n\tWORD $0xb4fff8ef // cbz\tx15, .LBB12_77\n\nLBB12_86:\n\tWORD $0xd37ef77e // lsl\tx30, x27, #2\n\tWORD $0xcb1b015b // sub\tx27, x10, x27\n\tWORD $0x8b1e033c // add\tx28, x25, x30\n\tWORD $0x8b1e029e // add\tx30, x20, x30\n\nLBB12_87:\n\tWORD $0xbc4047c2 // ldr\ts2, [x30], #4\n\tWORD $0xf100077b // subs\tx27, x27, #1\n\tWORD $0xbc404783 // ldr\ts3, [x28], #4\n\tWORD $0x1f030441 // fmadd\ts1, s2, s3, s1\n\tBNE  LBB12_87\n\tB    LBB12_77\n\nLBB12_88:\n\tWORD $0x6f00e400 // movi\tv0.2d, #0000000000000000\n\tWORD $0xd37ef529 // lsl\tx9, x9, #2\n\tWORD $0xaa1f03ea // mov\tx10, xzr\n\tWORD $0x927dec6b // and\tx11, x3, #0x7ffffffffffffff8\n\tWORD $0x9100410c // add\tx12, x8, #16\n\tB    LBB12_90\n\nLBB12_89:\n\tWORD $0x9100054a // add\tx10, x10, #1\n\tWORD $0x8b09018c // add\tx12, x12, x9\n\tWORD $0x8b090108 // add\tx8, x8, x9\n\tWORD $0xeb02015f // cmp\tx10, x2\n\tBEQ  LBB12_101\n\nLBB12_90:\n\tWORD $0xf100207f // cmp\tx3, #8\n\tBHS  LBB12_92\n\tWORD $0xaa1f03ee // mov\tx14, xzr\n\tB    LBB12_95\n\nLBB12_92:\n\tWORD $0xaa0b03ed // mov\tx13, x11\n\tWORD $0xaa0c03ee // mov\tx14, x12\n\nLBB12_93:\n\tWORD $0xad3f81c0 // stp\tq0, q0, [x14, #-16]\n\tWORD $0xf10021ad // subs\tx13, x13, #8\n\tWORD $0x910081ce // add\tx14, x14, #32\n\tBNE  LBB12_93\n\tWORD $0xeb03017f // cmp\tx11, x3\n\tWORD $0xaa0b03ee // mov\tx14, x11\n\tBEQ  LBB12_89\n\nLBB12_95:\n\tWORD $0x8b0e090d // add\tx13, x8, x14, lsl #2\n\tWORD $0xcb0e006e // sub\tx14, x3, x14\n\nLBB12_96:\n\tWORD $0xf10005ce // subs\tx14, x14, #1\n\tWORD $0xb80045bf // str\twzr, [x13], #4\n\tBNE  LBB12_96\n\tB    LBB12_89\n\nLBB12_97:\n\tWORD $0x2f00e400 // movi\td0, #0000000000000000\n\tWORD $0xd37ef56a // lsl\tx10, x11, #2\n\tWORD $0xd37ef529 // lsl\tx9, x9, #2\n\tWORD $0xaa1f03eb // mov\tx11, xzr\n\nLBB12_98:\n\tWORD $0x9b067d6c // mul\tx12, x11, x6\n\tWORD $0xaa0303ed // mov\tx13, x3\n\tWORD $0xaa0803ee // mov\tx14, x8\n\tWORD $0xaa0703ef // mov\tx15, x7\n\tWORD $0x8b0c08ac // add\tx12, x5, x12, lsl #2\n\nLBB12_99:\n\tWORD $0x3dc00181 // ldr\tq1, [x12]\n\tWORD $0x3dc001e2 // ldr\tq2, [x15]\n\tWORD $0xf10005ad // subs\tx13, x13, #1\n\tWORD $0x8b0a01ef // add\tx15, x15, x10\n\tWORD $0x6e22dc21 // fmul\tv1.4s, v1.4s, v2.4s\n\tWORD $0x1e202822 // fadd\ts2, s1, s0\n\tWORD $0x5e0c0423 // mov\ts3, v1.s[1]\n\tWORD $0x1e222862 // fadd\ts2, s3, s2\n\tWORD $0x5e140423 // mov\ts3, v1.s[2]\n\tWORD $0x5e1c0421 // mov\ts1, v1.s[3]\n\tWORD $0x1e222862 // fadd\ts2, s3, s2\n\tWORD $0x1e222821 // fadd\ts1, s1, s2\n\tWORD $0xbc0045c1 // str\ts1, [x14], #4\n\tBNE  LBB12_99\n\tWORD $0x9100056b // add\tx11, x11, #1\n\tWORD $0x8b090108 // add\tx8, x8, x9\n\tWORD $0xeb02017f // cmp\tx11, x2\n\tBNE  LBB12_98\n\nLBB12_101:\n\tWORD $0xa9454ff4 // ldp\tx20, x19, [sp, #80]\n\tWORD $0xa94457f6 // ldp\tx22, x21, [sp, #64]\n\tWORD $0xa9435ff8 // ldp\tx24, x23, [sp, #48]\n\tWORD $0xa94267fa // ldp\tx26, x25, [sp, #32]\n\tWORD $0xa9416ffc // ldp\tx28, x27, [sp, #16]\n\tWORD $0xa8c67bfd // ldp\tx29, x30, [sp], #96\n\tRET\n"
  },
  {
    "path": "common/floats/floats_noasm.go",
    "content": "//go:build noasm || (!amd64 && !arm64 && !riscv64)\n\n// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage floats\n\ntype Feature uint64\n\nconst OPENBLAS Feature = 1 << iota\n\nvar feature Feature\n\nfunc (Feature) String() string {\n\treturn \"NOASM\"\n}\n\nfunc (Feature) mulConstAddTo(a []float32, b float32, c, dst []float32) {\n\tmulConstAddTo(a, b, c, dst)\n}\n\nfunc (Feature) mulConstAdd(a []float32, b float32, c []float32) {\n\tmulConstAdd(a, b, c)\n}\n\nfunc (Feature) mulConstTo(a []float32, b float32, c []float32) {\n\tmulConstTo(a, b, c)\n}\n\nfunc (Feature) addConst(a []float32, b float32) {\n\taddConst(a, b)\n}\n\nfunc (Feature) sub(a, b []float32) {\n\tsub(a, b)\n}\n\nfunc (Feature) subTo(a, b, c []float32) {\n\tsubTo(a, b, c)\n}\n\nfunc (Feature) mulTo(a, b, c []float32) {\n\tmulTo(a, b, c)\n}\n\nfunc (Feature) mulConst(a []float32, b float32) {\n\tmulConst(a, b)\n}\n\nfunc (Feature) divTo(a, b, c []float32) {\n\tdivTo(a, b, c)\n}\n\nfunc (Feature) sqrtTo(a, b []float32) {\n\tsqrtTo(a, b)\n}\n\nfunc (Feature) dot(a, b []float32) float32 {\n\treturn dot(a, b)\n}\n\nfunc (Feature) euclidean(a, b []float32) float32 {\n\treturn euclidean(a, b)\n}\n\nfunc (Feature) mm(transA, transB bool, m, n, k int, a []float32, lda int, b []float32, ldb int, c []float32, ldc int) {\n\tmm(transA, transB, m, n, k, a, lda, b, ldb, c, ldc)\n}\n"
  },
  {
    "path": "common/floats/floats_riscv64.go",
    "content": "//go:build !noasm\n\n// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage floats\n\nimport (\n\t\"unsafe\"\n\n\t\"golang.org/x/sys/cpu\"\n)\n\n//go:generate goat src/floats_rvv.c -O3 -march=rv64imafdv\n\ntype Feature uint64\n\nconst (\n\tV Feature = 1 << iota\n\tOPENBLAS\n)\n\nvar feature Feature\n\nfunc init() {\n\tif cpu.RISCV64.HasV {\n\t\tfeature = feature | V\n\t}\n}\n\nfunc (feature Feature) String() string {\n\tif feature == V {\n\t\treturn \"RVV\"\n\t} else {\n\t\treturn \"RV\"\n\t}\n}\n\nfunc (feature Feature) mulConstAddTo(a []float32, b float32, c []float32, dst []float32) {\n\tif feature&V == V {\n\t\tvmul_const_add_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b), unsafe.Pointer(&c[0]), unsafe.Pointer(&dst[0]), int64(len(a)))\n\t} else {\n\t\tmulConstAddTo(a, b, c, dst)\n\t}\n}\n\nfunc (feature Feature) mulConstAdd(a []float32, b float32, c []float32) {\n\tif feature&V == V {\n\t\tvmul_const_add(unsafe.Pointer(&a[0]), unsafe.Pointer(&b), unsafe.Pointer(&c[0]), int64(len(a)))\n\t} else {\n\t\tmulConstAdd(a, b, c)\n\t}\n}\n\nfunc (feature Feature) mulConstTo(a []float32, b float32, c []float32) {\n\tif feature&V == V {\n\t\tvmul_const_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b), unsafe.Pointer(&c[0]), int64(len(a)))\n\t} else {\n\t\tmulConstTo(a, b, c)\n\t}\n}\n\nfunc (feature Feature) addConst(a []float32, b float32) {\n\tif feature&V == V {\n\t\tvadd_const(unsafe.Pointer(&a[0]), unsafe.Pointer(&b), int64(len(a)))\n\t} else {\n\t\taddConst(a, b)\n\t}\n}\n\nfunc (feature Feature) sub(a, b []float32) {\n\tif feature&V == V {\n\t\tvsub(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), int64(len(a)))\n\t} else {\n\t\tsub(a, b)\n\t}\n}\n\nfunc (feature Feature) subTo(a, b, c []float32) {\n\tif feature&V == V {\n\t\tvsub_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), unsafe.Pointer(&c[0]), int64(len(a)))\n\t} else {\n\t\tsubTo(a, b, c)\n\t}\n}\n\nfunc (feature Feature) mulTo(a, b, c []float32) {\n\tif feature&V == V {\n\t\tvmul_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), unsafe.Pointer(&c[0]), int64(len(a)))\n\t} else {\n\t\tmulTo(a, b, c)\n\t}\n}\n\nfunc (feature Feature) mulConst(a []float32, b float32) {\n\tif feature&V == V {\n\t\tvmul_const(unsafe.Pointer(&a[0]), unsafe.Pointer(&b), int64(len(a)))\n\t} else {\n\t\tmulConst(a, b)\n\t}\n}\n\nfunc (feature Feature) divTo(a, b, c []float32) {\n\tif feature&V == V {\n\t\tvdiv_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), unsafe.Pointer(&c[0]), int64(len(a)))\n\t} else {\n\t\tdivTo(a, b, c)\n\t}\n}\n\nfunc (feature Feature) sqrtTo(a, b []float32) {\n\tif feature&V == V {\n\t\tvsqrt_to(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), int64(len(a)))\n\t} else {\n\t\tsqrtTo(a, b)\n\t}\n}\n\nfunc (feature Feature) dot(a, b []float32) float32 {\n\tif feature&V == V {\n\t\treturn vdot(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), int64(len(a)))\n\t} else {\n\t\treturn dot(a, b)\n\t}\n}\n\nfunc (feature Feature) euclidean(a, b []float32) float32 {\n\tif feature&V == V {\n\t\treturn veuclidean(unsafe.Pointer(&a[0]), unsafe.Pointer(&b[0]), int64(len(a)))\n\t} else {\n\t\treturn euclidean(a, b)\n\t}\n}\n\nfunc (feature Feature) mm(transA, transB bool, m, n, k int, a []float32, lda int, b []float32, ldb int, c []float32, ldc int) {\n\tif feature&V == V && feature&OPENBLAS == 0 {\n\t\tvmm(transA, transB, int64(m), int64(n), int64(k), unsafe.Pointer(&a[0]), int64(lda), unsafe.Pointer(&b[0]), int64(ldb), unsafe.Pointer(&c[0]), int64(ldc))\n\t} else {\n\t\tmm(transA, transB, m, n, k, a, lda, b, ldb, c, ldc)\n\t}\n}\n"
  },
  {
    "path": "common/floats/floats_riscv64_test.go",
    "content": "//go:build !noasm\n\n// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage floats\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/suite\"\n)\n\nfunc TestV(t *testing.T) {\n\tsuite.Run(t, &SIMDTestSuite{Feature: V})\n}\n\nfunc initializeFloat32Array(n int) []float32 {\n\tx := make([]float32, n)\n\tfor i := 0; i < n; i++ {\n\t\tx[i] = rand.Float32()\n\t}\n\treturn x\n}\n\nfunc BenchmarkDot(b *testing.B) {\n\tfor _, feat := range []Feature{0, V} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.dot(v1, v2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkEuclidean(b *testing.B) {\n\tfor _, feat := range []Feature{0, V} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.euclidean(v1, v2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkMulConstAddTo(b *testing.B) {\n\tfor _, feat := range []Feature{0, V} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tv3 := make([]float32, i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.mulConstAddTo(v1, 2, v2, v3)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkMulConstAdd(b *testing.B) {\n\tfor _, feat := range []Feature{0, V} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.mulConstAdd(v1, 2, v2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkMulConst(b *testing.B) {\n\tfor _, feat := range []Feature{0, V} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.mulConst(v1, 2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkMulConstTo(b *testing.B) {\n\tfor _, feat := range []Feature{0, V} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.mulConstTo(v1, 2, v2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkAddConst(b *testing.B) {\n\tfor _, feat := range []Feature{0, V} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.addConst(v1, 2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkSub(b *testing.B) {\n\tfor _, feat := range []Feature{0, V} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.sub(v1, v2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkSubTo(b *testing.B) {\n\tfor _, feat := range []Feature{0, V} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tv3 := make([]float32, i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.subTo(v1, v2, v3)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkMulTo(b *testing.B) {\n\tfor _, feat := range []Feature{0, V} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tv3 := make([]float32, i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.mulTo(v1, v2, v3)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkDivTo(b *testing.B) {\n\tfor _, feat := range []Feature{0, V} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := initializeFloat32Array(i)\n\t\t\t\t\tv3 := make([]float32, i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.divTo(v1, v2, v3)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkSqrtTo(b *testing.B) {\n\tfor _, feat := range []Feature{0, V} {\n\t\tb.Run(feat.String(), func(b *testing.B) {\n\t\t\tfor i := 16; i <= 128; i *= 2 {\n\t\t\t\tb.Run(strconv.Itoa(i), func(b *testing.B) {\n\t\t\t\t\tv1 := initializeFloat32Array(i)\n\t\t\t\t\tv2 := make([]float32, i)\n\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\tfeat.sqrtTo(v1, v2)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkMM(b *testing.B) {\n\tfor _, transA := range []bool{false, true} {\n\t\tfor _, transB := range []bool{false, true} {\n\t\t\tfor _, feat := range []Feature{0, V} {\n\t\t\t\tb.Run(fmt.Sprintf(\"(%v,%v,%v)\", transA, transB, feat.String()), func(b *testing.B) {\n\t\t\t\t\tfor n := 16; n <= 128; n *= 2 {\n\t\t\t\t\t\tb.Run(strconv.Itoa(n), func(b *testing.B) {\n\t\t\t\t\t\t\tmatA := initializeFloat32Array(n * n)\n\t\t\t\t\t\t\tmatB := initializeFloat32Array(n * n)\n\t\t\t\t\t\t\tmatC := make([]float32, n*n)\n\t\t\t\t\t\t\tb.ResetTimer()\n\t\t\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\t\t\tfeat.mm(transA, transB, n, n, n, matA, n, matB, n, matC, n)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "common/floats/floats_rvv.go",
    "content": "//go:build !noasm && riscv64\n// Code generated by GoAT. DO NOT EDIT.\n// versions:\n// \tclang   18.1.8 (11bb4)\n// \tobjdump 2.42\n// flags: -march=rv64imafdv -O3\n// source: src/floats_rvv.c\n\npackage floats\n\nimport \"unsafe\"\n\n//go:noescape\nfunc vmul_const_add_to(a, b, c, dst unsafe.Pointer, n int64)\n\n//go:noescape\nfunc vmul_const_add(a, b, c unsafe.Pointer, n int64)\n\n//go:noescape\nfunc vmul_const_to(a, b, c unsafe.Pointer, n int64)\n\n//go:noescape\nfunc vmul_const(a, b unsafe.Pointer, n int64)\n\n//go:noescape\nfunc vadd_const(a, b unsafe.Pointer, n int64)\n\n//go:noescape\nfunc vsub_to(a, b, c unsafe.Pointer, n int64)\n\n//go:noescape\nfunc vsub(a, b unsafe.Pointer, n int64)\n\n//go:noescape\nfunc vmul_to(a, b, c unsafe.Pointer, n int64)\n\n//go:noescape\nfunc vdiv_to(a, b, c unsafe.Pointer, n int64)\n\n//go:noescape\nfunc vsqrt_to(a, b unsafe.Pointer, n int64)\n\n//go:noescape\nfunc vdot(a, b unsafe.Pointer, n int64) (result float32)\n\n//go:noescape\nfunc veuclidean(a, b unsafe.Pointer, n int64) (result float32)\n\n//go:noescape\nfunc vmm(transA, transB bool, m, n, k int64, a unsafe.Pointer, lda int64, b unsafe.Pointer, ldb int64, c unsafe.Pointer, ldc int64)\n"
  },
  {
    "path": "common/floats/floats_rvv.s",
    "content": "//go:build !noasm && riscv64\n// Code generated by GoAT. DO NOT EDIT.\n// versions:\n// \tclang   18.1.8 (11bb4)\n// \tobjdump 2.42\n// flags: -march=rv64imafdv -O3\n// source: src/floats_rvv.c\n\nTEXT ·vmul_const_add_to(SB), $0-40\n\tMOV  a+0(FP), A0\n\tMOV  b+8(FP), A1\n\tMOV  c+16(FP), A2\n\tMOV  dst+24(FP), A3\n\tMOV  n+32(FP), A4\n\tBLEZ A4, LBB0_8\n\tWORD $0xc2202873    // csrr\ta6, vlenb\n\tWORD $0x00185793    // srli\ta5, a6, 1\n\tWORD $0x01800893    // li\ta7, 24\n\tBLTU A7, A5, LBB0_3\n\tWORD $0x01800793    // li\ta5, 24\n\nLBB0_3:\n\tWORD $0xff010113    // addi\tsp, sp, -16\n\tWORD $0x00113423    // sd\tra, 8(sp)                       # 8-byte Folded Spill\n\tWORD $0x00813023    // sd\ts0, 0(sp)                       # 8-byte Folded Spill\n\tWORD $0x01010413    // addi\ts0, sp, 16\n\tWORD $0xff017113    // andi\tsp, sp, -16\n\tBGEU A4, A5, LBB0_9\n\tWORD $0x00000793    // li\ta5, 0\n\nLBB0_5:\n\tWORD $0x00279813 // slli\ta6, a5, 2\n\tWORD $0x010686b3 // add\ta3, a3, a6\n\tWORD $0x01060633 // add\ta2, a2, a6\n\tWORD $0x01050533 // add\ta0, a0, a6\n\tWORD $0x40f70733 // sub\ta4, a4, a5\n\nLBB0_6:\n\tWORD $0x00052787 // flw\tfa5, 0(a0)\n\tWORD $0x0005a707 // flw\tfa4, 0(a1)\n\tWORD $0x00062687 // flw\tfa3, 0(a2)\n\tWORD $0x68e7f7c3 // fmadd.s\tfa5, fa5, fa4, fa3\n\tWORD $0x00f6a027 // fsw\tfa5, 0(a3)\n\tWORD $0x00468693 // addi\ta3, a3, 4\n\tWORD $0x00460613 // addi\ta2, a2, 4\n\tWORD $0xfff70713 // addi\ta4, a4, -1\n\tWORD $0x00450513 // addi\ta0, a0, 4\n\tBNEZ A4, LBB0_6\n\nLBB0_7:\n\tWORD $0xff040113 // addi\tsp, s0, -16\n\tWORD $0x00813083 // ld\tra, 8(sp)                       # 8-byte Folded Reload\n\tWORD $0x00013403 // ld\ts0, 0(sp)                       # 8-byte Folded Reload\n\tWORD $0x01010113 // addi\tsp, sp, 16\n\nLBB0_8:\n\tRET\n\nLBB0_9:\n\tWORD $0x00271293 // slli\tt0, a4, 2\n\tWORD $0x005688b3 // add\ta7, a3, t0\n\tWORD $0x005507b3 // add\ta5, a0, t0\n\tWORD $0x00f6b7b3 // sltu\ta5, a3, a5\n\tWORD $0x01153333 // sltu\tt1, a0, a7\n\tWORD $0x0067f333 // and\tt1, a5, t1\n\tWORD $0x00000793 // li\ta5, 0\n\tBNEZ T1, LBB0_5\n\tWORD $0x00458313 // addi\tt1, a1, 4\n\tWORD $0x0066b333 // sltu\tt1, a3, t1\n\tWORD $0x0115b3b3 // sltu\tt2, a1, a7\n\tWORD $0x00737333 // and\tt1, t1, t2\n\tBNEZ T1, LBB0_5\n\tWORD $0x005602b3 // add\tt0, a2, t0\n\tWORD $0x0056b2b3 // sltu\tt0, a3, t0\n\tWORD $0x011638b3 // sltu\ta7, a2, a7\n\tWORD $0x0112f8b3 // and\ta7, t0, a7\n\tBNEZ A7, LBB0_5\n\tWORD $0x00185893 // srli\ta7, a6, 1\n\tWORD $0x0d1077d7 // vsetvli\ta5, zero, e32, m2, ta, ma\n\tWORD $0x0a05e407 // vlse32.v\tv8, (a1), zero\n\tWORD $0x411007b3 // neg\ta5, a7\n\tWORD $0x00e7f7b3 // and\ta5, a5, a4\n\tWORD $0x00181813 // slli\ta6, a6, 1\n\tWORD $0x00078293 // mv\tt0, a5\n\tWORD $0x00068313 // mv\tt1, a3\n\tWORD $0x00060393 // mv\tt2, a2\n\tWORD $0x00050e13 // mv\tt3, a0\n\nLBB0_13:\n\tWORD $0x228e6507    // vl2re32.v\tv10, (t3)\n\tWORD $0x2283e607    // vl2re32.v\tv12, (t2)\n\tWORD $0xb2a41657    // vfmacc.vv\tv12, v8, v10\n\tWORD $0x22830627    // vs2r.v\tv12, (t1)\n\tWORD $0x010e0e33    // add\tt3, t3, a6\n\tWORD $0x010383b3    // add\tt2, t2, a6\n\tWORD $0x411282b3    // sub\tt0, t0, a7\n\tWORD $0x01030333    // add\tt1, t1, a6\n\tBNEZ T0, LBB0_13\n\tBNE  A5, A4, LBB0_5\n\tJMP  LBB0_7\n\nTEXT ·vmul_const_add(SB), $0-32\n\tMOV  a+0(FP), A0\n\tMOV  b+8(FP), A1\n\tMOV  c+16(FP), A2\n\tMOV  n+24(FP), A3\n\tBLEZ A3, LBB1_8\n\tWORD $0xc22027f3    // csrr\ta5, vlenb\n\tWORD $0x0017d713    // srli\ta4, a5, 1\n\tWORD $0x01000813    // li\ta6, 16\n\tBLTU A6, A4, LBB1_3\n\tWORD $0x01000713    // li\ta4, 16\n\nLBB1_3:\n\tWORD $0xff010113    // addi\tsp, sp, -16\n\tWORD $0x00113423    // sd\tra, 8(sp)                       # 8-byte Folded Spill\n\tWORD $0x00813023    // sd\ts0, 0(sp)                       # 8-byte Folded Spill\n\tWORD $0x01010413    // addi\ts0, sp, 16\n\tWORD $0xff017113    // andi\tsp, sp, -16\n\tBGEU A3, A4, LBB1_9\n\tWORD $0x00000713    // li\ta4, 0\n\nLBB1_5:\n\tWORD $0x00271793 // slli\ta5, a4, 2\n\tWORD $0x00f60633 // add\ta2, a2, a5\n\tWORD $0x00f50533 // add\ta0, a0, a5\n\tWORD $0x40e686b3 // sub\ta3, a3, a4\n\nLBB1_6:\n\tWORD $0x00052787 // flw\tfa5, 0(a0)\n\tWORD $0x0005a707 // flw\tfa4, 0(a1)\n\tWORD $0x00062687 // flw\tfa3, 0(a2)\n\tWORD $0x68e7f7c3 // fmadd.s\tfa5, fa5, fa4, fa3\n\tWORD $0x00f62027 // fsw\tfa5, 0(a2)\n\tWORD $0x00460613 // addi\ta2, a2, 4\n\tWORD $0xfff68693 // addi\ta3, a3, -1\n\tWORD $0x00450513 // addi\ta0, a0, 4\n\tBNEZ A3, LBB1_6\n\nLBB1_7:\n\tWORD $0xff040113 // addi\tsp, s0, -16\n\tWORD $0x00813083 // ld\tra, 8(sp)                       # 8-byte Folded Reload\n\tWORD $0x00013403 // ld\ts0, 0(sp)                       # 8-byte Folded Reload\n\tWORD $0x01010113 // addi\tsp, sp, 16\n\nLBB1_8:\n\tRET\n\nLBB1_9:\n\tWORD $0x00269713 // slli\ta4, a3, 2\n\tWORD $0x00e60833 // add\ta6, a2, a4\n\tWORD $0x00e50733 // add\ta4, a0, a4\n\tWORD $0x00e63733 // sltu\ta4, a2, a4\n\tWORD $0x010538b3 // sltu\ta7, a0, a6\n\tWORD $0x011778b3 // and\ta7, a4, a7\n\tWORD $0x00000713 // li\ta4, 0\n\tBNEZ A7, LBB1_5\n\tWORD $0x00458893 // addi\ta7, a1, 4\n\tWORD $0x011638b3 // sltu\ta7, a2, a7\n\tWORD $0x0105b833 // sltu\ta6, a1, a6\n\tWORD $0x0108f833 // and\ta6, a7, a6\n\tBNEZ A6, LBB1_5\n\tWORD $0x0017d813 // srli\ta6, a5, 1\n\tWORD $0x0d107757 // vsetvli\ta4, zero, e32, m2, ta, ma\n\tWORD $0x0a05e407 // vlse32.v\tv8, (a1), zero\n\tWORD $0x41000733 // neg\ta4, a6\n\tWORD $0x00d77733 // and\ta4, a4, a3\n\tWORD $0x00179793 // slli\ta5, a5, 1\n\tWORD $0x00070893 // mv\ta7, a4\n\tWORD $0x00060293 // mv\tt0, a2\n\tWORD $0x00050313 // mv\tt1, a0\n\nLBB1_12:\n\tWORD $0x22836507    // vl2re32.v\tv10, (t1)\n\tWORD $0x2282e607    // vl2re32.v\tv12, (t0)\n\tWORD $0xb2a41657    // vfmacc.vv\tv12, v8, v10\n\tWORD $0x22828627    // vs2r.v\tv12, (t0)\n\tWORD $0x00f30333    // add\tt1, t1, a5\n\tWORD $0x410888b3    // sub\ta7, a7, a6\n\tWORD $0x00f282b3    // add\tt0, t0, a5\n\tBNEZ A7, LBB1_12\n\tBNE  A4, A3, LBB1_5\n\tJMP  LBB1_7\n\nTEXT ·vmul_const_to(SB), $0-32\n\tMOV  a+0(FP), A0\n\tMOV  b+8(FP), A1\n\tMOV  c+16(FP), A2\n\tMOV  n+24(FP), A3\n\tBLEZ A3, LBB2_8\n\tWORD $0xc22027f3    // csrr\ta5, vlenb\n\tWORD $0x0017d713    // srli\ta4, a5, 1\n\tWORD $0x01000813    // li\ta6, 16\n\tBLTU A6, A4, LBB2_3\n\tWORD $0x01000713    // li\ta4, 16\n\nLBB2_3:\n\tWORD $0xff010113    // addi\tsp, sp, -16\n\tWORD $0x00113423    // sd\tra, 8(sp)                       # 8-byte Folded Spill\n\tWORD $0x00813023    // sd\ts0, 0(sp)                       # 8-byte Folded Spill\n\tWORD $0x01010413    // addi\ts0, sp, 16\n\tWORD $0xff017113    // andi\tsp, sp, -16\n\tBGEU A3, A4, LBB2_9\n\tWORD $0x00000713    // li\ta4, 0\n\nLBB2_5:\n\tWORD $0x00271793 // slli\ta5, a4, 2\n\tWORD $0x00f60633 // add\ta2, a2, a5\n\tWORD $0x00f50533 // add\ta0, a0, a5\n\tWORD $0x40e686b3 // sub\ta3, a3, a4\n\nLBB2_6:\n\tWORD $0x00052787 // flw\tfa5, 0(a0)\n\tWORD $0x0005a707 // flw\tfa4, 0(a1)\n\tWORD $0x10e7f7d3 // fmul.s\tfa5, fa5, fa4\n\tWORD $0x00f62027 // fsw\tfa5, 0(a2)\n\tWORD $0x00460613 // addi\ta2, a2, 4\n\tWORD $0xfff68693 // addi\ta3, a3, -1\n\tWORD $0x00450513 // addi\ta0, a0, 4\n\tBNEZ A3, LBB2_6\n\nLBB2_7:\n\tWORD $0xff040113 // addi\tsp, s0, -16\n\tWORD $0x00813083 // ld\tra, 8(sp)                       # 8-byte Folded Reload\n\tWORD $0x00013403 // ld\ts0, 0(sp)                       # 8-byte Folded Reload\n\tWORD $0x01010113 // addi\tsp, sp, 16\n\nLBB2_8:\n\tRET\n\nLBB2_9:\n\tWORD $0x00269713 // slli\ta4, a3, 2\n\tWORD $0x00e60833 // add\ta6, a2, a4\n\tWORD $0x00e50733 // add\ta4, a0, a4\n\tWORD $0x00e63733 // sltu\ta4, a2, a4\n\tWORD $0x010538b3 // sltu\ta7, a0, a6\n\tWORD $0x011778b3 // and\ta7, a4, a7\n\tWORD $0x00000713 // li\ta4, 0\n\tBNEZ A7, LBB2_5\n\tWORD $0x00458893 // addi\ta7, a1, 4\n\tWORD $0x011638b3 // sltu\ta7, a2, a7\n\tWORD $0x0105b833 // sltu\ta6, a1, a6\n\tWORD $0x0108f833 // and\ta6, a7, a6\n\tBNEZ A6, LBB2_5\n\tWORD $0x0017d813 // srli\ta6, a5, 1\n\tWORD $0x41000733 // neg\ta4, a6\n\tWORD $0x0005a787 // flw\tfa5, 0(a1)\n\tWORD $0x00d77733 // and\ta4, a4, a3\n\tWORD $0x00179793 // slli\ta5, a5, 1\n\tWORD $0x0d1078d7 // vsetvli\ta7, zero, e32, m2, ta, ma\n\tWORD $0x00070893 // mv\ta7, a4\n\tWORD $0x00060293 // mv\tt0, a2\n\tWORD $0x00050313 // mv\tt1, a0\n\nLBB2_12:\n\tWORD $0x22836407    // vl2re32.v\tv8, (t1)\n\tWORD $0x9287d457    // vfmul.vf\tv8, v8, fa5\n\tWORD $0x22828427    // vs2r.v\tv8, (t0)\n\tWORD $0x00f30333    // add\tt1, t1, a5\n\tWORD $0x410888b3    // sub\ta7, a7, a6\n\tWORD $0x00f282b3    // add\tt0, t0, a5\n\tBNEZ A7, LBB2_12\n\tBNE  A4, A3, LBB2_5\n\tJMP  LBB2_7\n\nTEXT ·vmul_const(SB), $0-24\n\tMOV  a+0(FP), A0\n\tMOV  b+8(FP), A1\n\tMOV  n+16(FP), A2\n\tBLEZ A2, LBB3_10\n\tWORD $0xc2202773    // csrr\ta4, vlenb\n\tWORD $0x00175693    // srli\ta3, a4, 1\n\tWORD $0x00800793    // li\ta5, 8\n\tBLTU A5, A3, LBB3_3\n\tWORD $0x00800693    // li\ta3, 8\n\nLBB3_3:\n\tWORD $0xff010113     // addi\tsp, sp, -16\n\tWORD $0x00113423     // sd\tra, 8(sp)                       # 8-byte Folded Spill\n\tWORD $0x00813023     // sd\ts0, 0(sp)                       # 8-byte Folded Spill\n\tWORD $0x01010413     // addi\ts0, sp, 16\n\tWORD $0xff017113     // andi\tsp, sp, -16\n\tBLTU A2, A3, LBB3_6\n\tWORD $0x00458693     // addi\ta3, a1, 4\n\tBGEU A0, A3, LBB3_11\n\tWORD $0x00261693     // slli\ta3, a2, 2\n\tWORD $0x00d506b3     // add\ta3, a0, a3\n\tBGEU A1, A3, LBB3_11\n\nLBB3_6:\n\tWORD $0x00000693 // li\ta3, 0\n\nLBB3_7:\n\tWORD $0x00269713 // slli\ta4, a3, 2\n\tWORD $0x00e50533 // add\ta0, a0, a4\n\tWORD $0x40d60633 // sub\ta2, a2, a3\n\nLBB3_8:\n\tWORD $0x0005a787 // flw\tfa5, 0(a1)\n\tWORD $0x00052707 // flw\tfa4, 0(a0)\n\tWORD $0x10e7f7d3 // fmul.s\tfa5, fa5, fa4\n\tWORD $0x00f52027 // fsw\tfa5, 0(a0)\n\tWORD $0xfff60613 // addi\ta2, a2, -1\n\tWORD $0x00450513 // addi\ta0, a0, 4\n\tBNEZ A2, LBB3_8\n\nLBB3_9:\n\tWORD $0xff040113 // addi\tsp, s0, -16\n\tWORD $0x00813083 // ld\tra, 8(sp)                       # 8-byte Folded Reload\n\tWORD $0x00013403 // ld\ts0, 0(sp)                       # 8-byte Folded Reload\n\tWORD $0x01010113 // addi\tsp, sp, 16\n\nLBB3_10:\n\tRET\n\nLBB3_11:\n\tWORD $0x00175793 // srli\ta5, a4, 1\n\tWORD $0x40f006b3 // neg\ta3, a5\n\tWORD $0x0005a787 // flw\tfa5, 0(a1)\n\tWORD $0x00c6f6b3 // and\ta3, a3, a2\n\tWORD $0x00171713 // slli\ta4, a4, 1\n\tWORD $0x0d107857 // vsetvli\ta6, zero, e32, m2, ta, ma\n\tWORD $0x00068813 // mv\ta6, a3\n\tWORD $0x00050893 // mv\ta7, a0\n\nLBB3_12:\n\tWORD $0x2288e407    // vl2re32.v\tv8, (a7)\n\tWORD $0x9287d457    // vfmul.vf\tv8, v8, fa5\n\tWORD $0x22888427    // vs2r.v\tv8, (a7)\n\tWORD $0x40f80833    // sub\ta6, a6, a5\n\tWORD $0x00e888b3    // add\ta7, a7, a4\n\tBNEZ A6, LBB3_12\n\tBEQ  A3, A2, LBB3_9\n\tJMP  LBB3_7\n\nTEXT ·vadd_const(SB), $0-24\n\tMOV  a+0(FP), A0\n\tMOV  b+8(FP), A1\n\tMOV  n+16(FP), A2\n\tBLEZ A2, LBB4_10\n\tWORD $0xc2202773    // csrr\ta4, vlenb\n\tWORD $0x00175693    // srli\ta3, a4, 1\n\tWORD $0x00800793    // li\ta5, 8\n\tBLTU A5, A3, LBB4_3\n\tWORD $0x00800693    // li\ta3, 8\n\nLBB4_3:\n\tWORD $0xff010113     // addi\tsp, sp, -16\n\tWORD $0x00113423     // sd\tra, 8(sp)                       # 8-byte Folded Spill\n\tWORD $0x00813023     // sd\ts0, 0(sp)                       # 8-byte Folded Spill\n\tWORD $0x01010413     // addi\ts0, sp, 16\n\tWORD $0xff017113     // andi\tsp, sp, -16\n\tBLTU A2, A3, LBB4_6\n\tWORD $0x00458693     // addi\ta3, a1, 4\n\tBGEU A0, A3, LBB4_11\n\tWORD $0x00261693     // slli\ta3, a2, 2\n\tWORD $0x00d506b3     // add\ta3, a0, a3\n\tBGEU A1, A3, LBB4_11\n\nLBB4_6:\n\tWORD $0x00000693 // li\ta3, 0\n\nLBB4_7:\n\tWORD $0x00269713 // slli\ta4, a3, 2\n\tWORD $0x00e50533 // add\ta0, a0, a4\n\tWORD $0x40d60633 // sub\ta2, a2, a3\n\nLBB4_8:\n\tWORD $0x0005a787 // flw\tfa5, 0(a1)\n\tWORD $0x00052707 // flw\tfa4, 0(a0)\n\tWORD $0x00e7f7d3 // fadd.s\tfa5, fa5, fa4\n\tWORD $0x00f52027 // fsw\tfa5, 0(a0)\n\tWORD $0xfff60613 // addi\ta2, a2, -1\n\tWORD $0x00450513 // addi\ta0, a0, 4\n\tBNEZ A2, LBB4_8\n\nLBB4_9:\n\tWORD $0xff040113 // addi\tsp, s0, -16\n\tWORD $0x00813083 // ld\tra, 8(sp)                       # 8-byte Folded Reload\n\tWORD $0x00013403 // ld\ts0, 0(sp)                       # 8-byte Folded Reload\n\tWORD $0x01010113 // addi\tsp, sp, 16\n\nLBB4_10:\n\tRET\n\nLBB4_11:\n\tWORD $0x00175793 // srli\ta5, a4, 1\n\tWORD $0x40f006b3 // neg\ta3, a5\n\tWORD $0x0005a787 // flw\tfa5, 0(a1)\n\tWORD $0x00c6f6b3 // and\ta3, a3, a2\n\tWORD $0x00171713 // slli\ta4, a4, 1\n\tWORD $0x0d107857 // vsetvli\ta6, zero, e32, m2, ta, ma\n\tWORD $0x00068813 // mv\ta6, a3\n\tWORD $0x00050893 // mv\ta7, a0\n\nLBB4_12:\n\tWORD $0x2288e407    // vl2re32.v\tv8, (a7)\n\tWORD $0x0287d457    // vfadd.vf\tv8, v8, fa5\n\tWORD $0x22888427    // vs2r.v\tv8, (a7)\n\tWORD $0x40f80833    // sub\ta6, a6, a5\n\tWORD $0x00e888b3    // add\ta7, a7, a4\n\tBNEZ A6, LBB4_12\n\tBEQ  A3, A2, LBB4_9\n\tJMP  LBB4_7\n\nTEXT ·vsub_to(SB), $0-32\n\tMOV  a+0(FP), A0\n\tMOV  b+8(FP), A1\n\tMOV  c+16(FP), A2\n\tMOV  n+24(FP), A3\n\tBLEZ A3, LBB5_8\n\tWORD $0xc2202873    // csrr\ta6, vlenb\n\tWORD $0x00185713    // srli\ta4, a6, 1\n\tWORD $0x01000793    // li\ta5, 16\n\tBLTU A5, A4, LBB5_3\n\tWORD $0x01000713    // li\ta4, 16\n\nLBB5_3:\n\tWORD $0xff010113    // addi\tsp, sp, -16\n\tWORD $0x00113423    // sd\tra, 8(sp)                       # 8-byte Folded Spill\n\tWORD $0x00813023    // sd\ts0, 0(sp)                       # 8-byte Folded Spill\n\tWORD $0x01010413    // addi\ts0, sp, 16\n\tWORD $0xff017113    // andi\tsp, sp, -16\n\tBGEU A3, A4, LBB5_9\n\tWORD $0x00000713    // li\ta4, 0\n\nLBB5_5:\n\tWORD $0x40e686b3 // sub\ta3, a3, a4\n\tWORD $0x00271713 // slli\ta4, a4, 2\n\tWORD $0x00e60633 // add\ta2, a2, a4\n\tWORD $0x00e585b3 // add\ta1, a1, a4\n\tWORD $0x00e50533 // add\ta0, a0, a4\n\nLBB5_6:\n\tWORD $0x00052787 // flw\tfa5, 0(a0)\n\tWORD $0x0005a707 // flw\tfa4, 0(a1)\n\tWORD $0x08e7f7d3 // fsub.s\tfa5, fa5, fa4\n\tWORD $0x00f62027 // fsw\tfa5, 0(a2)\n\tWORD $0xfff68693 // addi\ta3, a3, -1\n\tWORD $0x00460613 // addi\ta2, a2, 4\n\tWORD $0x00458593 // addi\ta1, a1, 4\n\tWORD $0x00450513 // addi\ta0, a0, 4\n\tBNEZ A3, LBB5_6\n\nLBB5_7:\n\tWORD $0xff040113 // addi\tsp, s0, -16\n\tWORD $0x00813083 // ld\tra, 8(sp)                       # 8-byte Folded Reload\n\tWORD $0x00013403 // ld\ts0, 0(sp)                       # 8-byte Folded Reload\n\tWORD $0x01010113 // addi\tsp, sp, 16\n\nLBB5_8:\n\tRET\n\nLBB5_9:\n\tWORD $0x00181793    // slli\ta5, a6, 1\n\tWORD $0x40a608b3    // sub\ta7, a2, a0\n\tWORD $0x00000713    // li\ta4, 0\n\tBLTU A7, A5, LBB5_5\n\tWORD $0x40b608b3    // sub\ta7, a2, a1\n\tBLTU A7, A5, LBB5_5\n\tWORD $0x00185813    // srli\ta6, a6, 1\n\tWORD $0x41000733    // neg\ta4, a6\n\tWORD $0x00d77733    // and\ta4, a4, a3\n\tWORD $0x0d1078d7    // vsetvli\ta7, zero, e32, m2, ta, ma\n\tWORD $0x00070893    // mv\ta7, a4\n\tWORD $0x00060293    // mv\tt0, a2\n\tWORD $0x00058313    // mv\tt1, a1\n\tWORD $0x00050393    // mv\tt2, a0\n\nLBB5_12:\n\tWORD $0x2283e407    // vl2re32.v\tv8, (t2)\n\tWORD $0x22836507    // vl2re32.v\tv10, (t1)\n\tWORD $0x0a851457    // vfsub.vv\tv8, v8, v10\n\tWORD $0x22828427    // vs2r.v\tv8, (t0)\n\tWORD $0x00f383b3    // add\tt2, t2, a5\n\tWORD $0x00f30333    // add\tt1, t1, a5\n\tWORD $0x410888b3    // sub\ta7, a7, a6\n\tWORD $0x00f282b3    // add\tt0, t0, a5\n\tBNEZ A7, LBB5_12\n\tBNE  A4, A3, LBB5_5\n\tJMP  LBB5_7\n\nTEXT ·vsub(SB), $0-24\n\tMOV  a+0(FP), A0\n\tMOV  b+8(FP), A1\n\tMOV  n+16(FP), A2\n\tBLEZ A2, LBB6_10\n\tWORD $0xc22027f3    // csrr\ta5, vlenb\n\tWORD $0x0017d693    // srli\ta3, a5, 1\n\tWORD $0x01000713    // li\ta4, 16\n\tBLTU A4, A3, LBB6_3\n\tWORD $0x01000693    // li\ta3, 16\n\nLBB6_3:\n\tWORD $0xff010113     // addi\tsp, sp, -16\n\tWORD $0x00113423     // sd\tra, 8(sp)                       # 8-byte Folded Spill\n\tWORD $0x00813023     // sd\ts0, 0(sp)                       # 8-byte Folded Spill\n\tWORD $0x01010413     // addi\ts0, sp, 16\n\tWORD $0xff017113     // andi\tsp, sp, -16\n\tBLTU A2, A3, LBB6_6\n\tWORD $0x00261693     // slli\ta3, a2, 2\n\tWORD $0x00d58733     // add\ta4, a1, a3\n\tBGEU A0, A4, LBB6_11\n\tWORD $0x00d506b3     // add\ta3, a0, a3\n\tBGEU A1, A3, LBB6_11\n\nLBB6_6:\n\tWORD $0x00000693 // li\ta3, 0\n\nLBB6_7:\n\tWORD $0x40d60633 // sub\ta2, a2, a3\n\tWORD $0x00269693 // slli\ta3, a3, 2\n\tWORD $0x00d50533 // add\ta0, a0, a3\n\tWORD $0x00d585b3 // add\ta1, a1, a3\n\nLBB6_8:\n\tWORD $0x0005a787 // flw\tfa5, 0(a1)\n\tWORD $0x00052707 // flw\tfa4, 0(a0)\n\tWORD $0x08f777d3 // fsub.s\tfa5, fa4, fa5\n\tWORD $0x00f52027 // fsw\tfa5, 0(a0)\n\tWORD $0xfff60613 // addi\ta2, a2, -1\n\tWORD $0x00450513 // addi\ta0, a0, 4\n\tWORD $0x00458593 // addi\ta1, a1, 4\n\tBNEZ A2, LBB6_8\n\nLBB6_9:\n\tWORD $0xff040113 // addi\tsp, s0, -16\n\tWORD $0x00813083 // ld\tra, 8(sp)                       # 8-byte Folded Reload\n\tWORD $0x00013403 // ld\ts0, 0(sp)                       # 8-byte Folded Reload\n\tWORD $0x01010113 // addi\tsp, sp, 16\n\nLBB6_10:\n\tRET\n\nLBB6_11:\n\tWORD $0x0017d713 // srli\ta4, a5, 1\n\tWORD $0x40e006b3 // neg\ta3, a4\n\tWORD $0x00c6f6b3 // and\ta3, a3, a2\n\tWORD $0x00179793 // slli\ta5, a5, 1\n\tWORD $0x0d107857 // vsetvli\ta6, zero, e32, m2, ta, ma\n\tWORD $0x00068813 // mv\ta6, a3\n\tWORD $0x00050893 // mv\ta7, a0\n\tWORD $0x00058293 // mv\tt0, a1\n\nLBB6_12:\n\tWORD $0x2282e407    // vl2re32.v\tv8, (t0)\n\tWORD $0x2288e507    // vl2re32.v\tv10, (a7)\n\tWORD $0x0aa41457    // vfsub.vv\tv8, v10, v8\n\tWORD $0x22888427    // vs2r.v\tv8, (a7)\n\tWORD $0x00f282b3    // add\tt0, t0, a5\n\tWORD $0x40e80833    // sub\ta6, a6, a4\n\tWORD $0x00f888b3    // add\ta7, a7, a5\n\tBNEZ A6, LBB6_12\n\tBEQ  A3, A2, LBB6_9\n\tJMP  LBB6_7\n\nTEXT ·vmul_to(SB), $0-32\n\tMOV  a+0(FP), A0\n\tMOV  b+8(FP), A1\n\tMOV  c+16(FP), A2\n\tMOV  n+24(FP), A3\n\tBLEZ A3, LBB7_8\n\tWORD $0xc2202873    // csrr\ta6, vlenb\n\tWORD $0x00185713    // srli\ta4, a6, 1\n\tWORD $0x01000793    // li\ta5, 16\n\tBLTU A5, A4, LBB7_3\n\tWORD $0x01000713    // li\ta4, 16\n\nLBB7_3:\n\tWORD $0xff010113    // addi\tsp, sp, -16\n\tWORD $0x00113423    // sd\tra, 8(sp)                       # 8-byte Folded Spill\n\tWORD $0x00813023    // sd\ts0, 0(sp)                       # 8-byte Folded Spill\n\tWORD $0x01010413    // addi\ts0, sp, 16\n\tWORD $0xff017113    // andi\tsp, sp, -16\n\tBGEU A3, A4, LBB7_9\n\tWORD $0x00000713    // li\ta4, 0\n\nLBB7_5:\n\tWORD $0x40e686b3 // sub\ta3, a3, a4\n\tWORD $0x00271713 // slli\ta4, a4, 2\n\tWORD $0x00e60633 // add\ta2, a2, a4\n\tWORD $0x00e585b3 // add\ta1, a1, a4\n\tWORD $0x00e50533 // add\ta0, a0, a4\n\nLBB7_6:\n\tWORD $0x00052787 // flw\tfa5, 0(a0)\n\tWORD $0x0005a707 // flw\tfa4, 0(a1)\n\tWORD $0x10e7f7d3 // fmul.s\tfa5, fa5, fa4\n\tWORD $0x00f62027 // fsw\tfa5, 0(a2)\n\tWORD $0xfff68693 // addi\ta3, a3, -1\n\tWORD $0x00460613 // addi\ta2, a2, 4\n\tWORD $0x00458593 // addi\ta1, a1, 4\n\tWORD $0x00450513 // addi\ta0, a0, 4\n\tBNEZ A3, LBB7_6\n\nLBB7_7:\n\tWORD $0xff040113 // addi\tsp, s0, -16\n\tWORD $0x00813083 // ld\tra, 8(sp)                       # 8-byte Folded Reload\n\tWORD $0x00013403 // ld\ts0, 0(sp)                       # 8-byte Folded Reload\n\tWORD $0x01010113 // addi\tsp, sp, 16\n\nLBB7_8:\n\tRET\n\nLBB7_9:\n\tWORD $0x00181793    // slli\ta5, a6, 1\n\tWORD $0x40a608b3    // sub\ta7, a2, a0\n\tWORD $0x00000713    // li\ta4, 0\n\tBLTU A7, A5, LBB7_5\n\tWORD $0x40b608b3    // sub\ta7, a2, a1\n\tBLTU A7, A5, LBB7_5\n\tWORD $0x00185813    // srli\ta6, a6, 1\n\tWORD $0x41000733    // neg\ta4, a6\n\tWORD $0x00d77733    // and\ta4, a4, a3\n\tWORD $0x0d1078d7    // vsetvli\ta7, zero, e32, m2, ta, ma\n\tWORD $0x00070893    // mv\ta7, a4\n\tWORD $0x00060293    // mv\tt0, a2\n\tWORD $0x00058313    // mv\tt1, a1\n\tWORD $0x00050393    // mv\tt2, a0\n\nLBB7_12:\n\tWORD $0x2283e407    // vl2re32.v\tv8, (t2)\n\tWORD $0x22836507    // vl2re32.v\tv10, (t1)\n\tWORD $0x92851457    // vfmul.vv\tv8, v8, v10\n\tWORD $0x22828427    // vs2r.v\tv8, (t0)\n\tWORD $0x00f383b3    // add\tt2, t2, a5\n\tWORD $0x00f30333    // add\tt1, t1, a5\n\tWORD $0x410888b3    // sub\ta7, a7, a6\n\tWORD $0x00f282b3    // add\tt0, t0, a5\n\tBNEZ A7, LBB7_12\n\tBNE  A4, A3, LBB7_5\n\tJMP  LBB7_7\n\nTEXT ·vdiv_to(SB), $0-32\n\tMOV  a+0(FP), A0\n\tMOV  b+8(FP), A1\n\tMOV  c+16(FP), A2\n\tMOV  n+24(FP), A3\n\tBLEZ A3, LBB8_8\n\tWORD $0xc2202873    // csrr\ta6, vlenb\n\tWORD $0x00185713    // srli\ta4, a6, 1\n\tWORD $0x01000793    // li\ta5, 16\n\tBLTU A5, A4, LBB8_3\n\tWORD $0x01000713    // li\ta4, 16\n\nLBB8_3:\n\tWORD $0xff010113    // addi\tsp, sp, -16\n\tWORD $0x00113423    // sd\tra, 8(sp)                       # 8-byte Folded Spill\n\tWORD $0x00813023    // sd\ts0, 0(sp)                       # 8-byte Folded Spill\n\tWORD $0x01010413    // addi\ts0, sp, 16\n\tWORD $0xff017113    // andi\tsp, sp, -16\n\tBGEU A3, A4, LBB8_9\n\tWORD $0x00000713    // li\ta4, 0\n\nLBB8_5:\n\tWORD $0x40e686b3 // sub\ta3, a3, a4\n\tWORD $0x00271713 // slli\ta4, a4, 2\n\tWORD $0x00e60633 // add\ta2, a2, a4\n\tWORD $0x00e585b3 // add\ta1, a1, a4\n\tWORD $0x00e50533 // add\ta0, a0, a4\n\nLBB8_6:\n\tWORD $0x00052787 // flw\tfa5, 0(a0)\n\tWORD $0x0005a707 // flw\tfa4, 0(a1)\n\tWORD $0x18e7f7d3 // fdiv.s\tfa5, fa5, fa4\n\tWORD $0x00f62027 // fsw\tfa5, 0(a2)\n\tWORD $0xfff68693 // addi\ta3, a3, -1\n\tWORD $0x00460613 // addi\ta2, a2, 4\n\tWORD $0x00458593 // addi\ta1, a1, 4\n\tWORD $0x00450513 // addi\ta0, a0, 4\n\tBNEZ A3, LBB8_6\n\nLBB8_7:\n\tWORD $0xff040113 // addi\tsp, s0, -16\n\tWORD $0x00813083 // ld\tra, 8(sp)                       # 8-byte Folded Reload\n\tWORD $0x00013403 // ld\ts0, 0(sp)                       # 8-byte Folded Reload\n\tWORD $0x01010113 // addi\tsp, sp, 16\n\nLBB8_8:\n\tRET\n\nLBB8_9:\n\tWORD $0x00181793    // slli\ta5, a6, 1\n\tWORD $0x40a608b3    // sub\ta7, a2, a0\n\tWORD $0x00000713    // li\ta4, 0\n\tBLTU A7, A5, LBB8_5\n\tWORD $0x40b608b3    // sub\ta7, a2, a1\n\tBLTU A7, A5, LBB8_5\n\tWORD $0x00185813    // srli\ta6, a6, 1\n\tWORD $0x41000733    // neg\ta4, a6\n\tWORD $0x00d77733    // and\ta4, a4, a3\n\tWORD $0x0d1078d7    // vsetvli\ta7, zero, e32, m2, ta, ma\n\tWORD $0x00070893    // mv\ta7, a4\n\tWORD $0x00060293    // mv\tt0, a2\n\tWORD $0x00058313    // mv\tt1, a1\n\tWORD $0x00050393    // mv\tt2, a0\n\nLBB8_12:\n\tWORD $0x2283e407    // vl2re32.v\tv8, (t2)\n\tWORD $0x22836507    // vl2re32.v\tv10, (t1)\n\tWORD $0x82851457    // vfdiv.vv\tv8, v8, v10\n\tWORD $0x22828427    // vs2r.v\tv8, (t0)\n\tWORD $0x00f383b3    // add\tt2, t2, a5\n\tWORD $0x00f30333    // add\tt1, t1, a5\n\tWORD $0x410888b3    // sub\ta7, a7, a6\n\tWORD $0x00f282b3    // add\tt0, t0, a5\n\tBNEZ A7, LBB8_12\n\tBNE  A4, A3, LBB8_5\n\tJMP  LBB8_7\n\nTEXT ·vsqrt_to(SB), $0-24\n\tMOV  a+0(FP), A0\n\tMOV  b+8(FP), A1\n\tMOV  n+16(FP), A2\n\tBLEZ A2, LBB9_4\n\tWORD $0xff010113  // addi\tsp, sp, -16\n\tWORD $0x00113423  // sd\tra, 8(sp)                       # 8-byte Folded Spill\n\tWORD $0x00813023  // sd\ts0, 0(sp)                       # 8-byte Folded Spill\n\tWORD $0x01010413  // addi\ts0, sp, 16\n\tWORD $0xff017113  // andi\tsp, sp, -16\n\nLBB9_2:\n\tWORD $0x0d0676d7 // vsetvli\ta3, a2, e32, m1, ta, ma\n\tWORD $0x02056407 // vle32.v\tv8, (a0)\n\tWORD $0x4e801457 // vfsqrt.v\tv8, v8\n\tWORD $0x0205e427 // vse32.v\tv8, (a1)\n\tWORD $0x00269713 // slli\ta4, a3, 2\n\tWORD $0x00e50533 // add\ta0, a0, a4\n\tWORD $0x40d60633 // sub\ta2, a2, a3\n\tWORD $0x00e585b3 // add\ta1, a1, a4\n\tBGTZ A2, LBB9_2\n\tWORD $0xff040113 // addi\tsp, s0, -16\n\tWORD $0x00813083 // ld\tra, 8(sp)                       # 8-byte Folded Reload\n\tWORD $0x00013403 // ld\ts0, 0(sp)                       # 8-byte Folded Reload\n\tWORD $0x01010113 // addi\tsp, sp, 16\n\nLBB9_4:\n\tRET\n\nTEXT ·vdot(SB), $8-24\n\tMOV  a+0(FP), A0\n\tMOV  b+8(FP), A1\n\tMOV  n+16(FP), A2\n\tWORD $0xff010113  // addi\tsp, sp, -16\n\tWORD $0x00113423  // sd\tra, 8(sp)                       # 8-byte Folded Spill\n\tWORD $0x00813023  // sd\ts0, 0(sp)                       # 8-byte Folded Spill\n\tWORD $0x01010413  // addi\ts0, sp, 16\n\tWORD $0xff017113  // andi\tsp, sp, -16\n\tWORD $0x0d0076d7  // vsetvli\ta3, zero, e32, m1, ta, ma\n\tWORD $0x02d65733  // divu\ta4, a2, a3\n\tWORD $0x0007079b  // sext.w\ta5, a4\n\tWORD $0x5e003457  // vmv.v.i\tv8, 0\n\tBLEZ A5, LBB10_3\n\tWORD $0x00000813  // li\ta6, 0\n\tWORD $0x00269893  // slli\ta7, a3, 2\n\tWORD $0x9e8034d7  // vmv1r.v\tv9, v8\n\nLBB10_2:\n\tWORD $0x02056507     // vle32.v\tv10, (a0)\n\tWORD $0x0205e587     // vle32.v\tv11, (a1)\n\tWORD $0xb2a594d7     // vfmacc.vv\tv9, v11, v10\n\tWORD $0x0018081b     // addiw\ta6, a6, 1\n\tWORD $0x01150533     // add\ta0, a0, a7\n\tWORD $0x011585b3     // add\ta1, a1, a7\n\tBLT  A6, A5, LBB10_2\n\tJMP  LBB10_4\n\nLBB10_3:\n\tWORD $0x9e8034d7 // vmv1r.v\tv9, v8\n\nLBB10_4:\n\tWORD $0x02d706b3        // mul\ta3, a4, a3\n\tWORD $0x40d60633        // sub\ta2, a2, a3\n\tWORD $0x0e941457        // vfredosum.vs\tv8, v9, v8\n\tWORD $0x0d067057        // vsetvli\tzero, a2, e32, m1, ta, ma\n\tWORD $0x02056487        // vle32.v\tv9, (a0)\n\tWORD $0x0205e507        // vle32.v\tv10, (a1)\n\tWORD $0x929514d7        // vfmul.vv\tv9, v9, v10\n\tWORD $0x0e941457        // vfredosum.vs\tv8, v9, v8\n\tWORD $0x42801557        // vfmv.f.s\tfa0, v8\n\tWORD $0xff040113        // addi\tsp, s0, -16\n\tWORD $0x00813083        // ld\tra, 8(sp)                       # 8-byte Folded Reload\n\tWORD $0x00013403        // ld\ts0, 0(sp)                       # 8-byte Folded Reload\n\tWORD $0x01010113        // addi\tsp, sp, 16\n\tMOVF FA0, result+24(FP)\n\tRET\n\nTEXT ·veuclidean(SB), $8-24\n\tMOV  a+0(FP), A0\n\tMOV  b+8(FP), A1\n\tMOV  n+16(FP), A2\n\tWORD $0xff010113  // addi\tsp, sp, -16\n\tWORD $0x00113423  // sd\tra, 8(sp)                       # 8-byte Folded Spill\n\tWORD $0x00813023  // sd\ts0, 0(sp)                       # 8-byte Folded Spill\n\tWORD $0x01010413  // addi\ts0, sp, 16\n\tWORD $0xff017113  // andi\tsp, sp, -16\n\tWORD $0x0d0076d7  // vsetvli\ta3, zero, e32, m1, ta, ma\n\tWORD $0x02d65733  // divu\ta4, a2, a3\n\tWORD $0x0007079b  // sext.w\ta5, a4\n\tWORD $0x5e003457  // vmv.v.i\tv8, 0\n\tBLEZ A5, LBB11_3\n\tWORD $0x00000813  // li\ta6, 0\n\tWORD $0x00269893  // slli\ta7, a3, 2\n\tWORD $0x9e8034d7  // vmv1r.v\tv9, v8\n\nLBB11_2:\n\tWORD $0x02056507     // vle32.v\tv10, (a0)\n\tWORD $0x0205e587     // vle32.v\tv11, (a1)\n\tWORD $0x0aa59557     // vfsub.vv\tv10, v10, v11\n\tWORD $0xb2a514d7     // vfmacc.vv\tv9, v10, v10\n\tWORD $0x0018081b     // addiw\ta6, a6, 1\n\tWORD $0x01150533     // add\ta0, a0, a7\n\tWORD $0x011585b3     // add\ta1, a1, a7\n\tBLT  A6, A5, LBB11_2\n\tJMP  LBB11_4\n\nLBB11_3:\n\tWORD $0x9e8034d7 // vmv1r.v\tv9, v8\n\nLBB11_4:\n\tWORD $0x02d706b3        // mul\ta3, a4, a3\n\tWORD $0x40d60633        // sub\ta2, a2, a3\n\tWORD $0x0e941457        // vfredosum.vs\tv8, v9, v8\n\tWORD $0x0d067657        // vsetvli\ta2, a2, e32, m1, ta, ma\n\tWORD $0x02056487        // vle32.v\tv9, (a0)\n\tWORD $0x0205e507        // vle32.v\tv10, (a1)\n\tWORD $0x0d007557        // vsetvli\ta0, zero, e32, m1, ta, ma\n\tWORD $0x0a9514d7        // vfsub.vv\tv9, v9, v10\n\tWORD $0x0d067057        // vsetvli\tzero, a2, e32, m1, ta, ma\n\tWORD $0x929494d7        // vfmul.vv\tv9, v9, v9\n\tWORD $0x0e941457        // vfredosum.vs\tv8, v9, v8\n\tWORD $0x4e801457        // vfsqrt.v\tv8, v8\n\tWORD $0x42801557        // vfmv.f.s\tfa0, v8\n\tWORD $0xff040113        // addi\tsp, s0, -16\n\tWORD $0x00813083        // ld\tra, 8(sp)                       # 8-byte Folded Reload\n\tWORD $0x00013403        // ld\ts0, 0(sp)                       # 8-byte Folded Reload\n\tWORD $0x01010113        // addi\tsp, sp, 16\n\tMOVF FA0, result+24(FP)\n\tRET\n\nTEXT ·vmm(SB), $0-88\n\tMOVB transA+0(FP), A0\n\tMOVB transB+1(FP), A1\n\tMOV  m+8(FP), A2\n\tMOV  n+16(FP), A3\n\tMOV  k+24(FP), A4\n\tMOV  a+32(FP), A5\n\tMOV  lda+40(FP), A6\n\tMOV  b+48(FP), A7\n\tADDI -24, SP, SP\n\tMOV  ldb+80(FP), T0\n\tMOV  T0, 0(SP)\n\tMOV  c+88(FP), T0\n\tMOV  T0, 8(SP)\n\tMOV  ldc+96(FP), T0\n\tMOV  T0, 16(SP)\n\tWORD $0xf8010113      // addi\tsp, sp, -128\n\tWORD $0x06113c23      // sd\tra, 120(sp)                     # 8-byte Folded Spill\n\tWORD $0x06813823      // sd\ts0, 112(sp)                     # 8-byte Folded Spill\n\tWORD $0x07213423      // sd\ts2, 104(sp)                     # 8-byte Folded Spill\n\tWORD $0x07313023      // sd\ts3, 96(sp)                      # 8-byte Folded Spill\n\tWORD $0x05413c23      // sd\ts4, 88(sp)                      # 8-byte Folded Spill\n\tWORD $0x05513823      // sd\ts5, 80(sp)                      # 8-byte Folded Spill\n\tWORD $0x05613423      // sd\ts6, 72(sp)                      # 8-byte Folded Spill\n\tWORD $0x05713023      // sd\ts7, 64(sp)                      # 8-byte Folded Spill\n\tWORD $0x03813c23      // sd\ts8, 56(sp)                      # 8-byte Folded Spill\n\tWORD $0x03913823      // sd\ts9, 48(sp)                      # 8-byte Folded Spill\n\tWORD $0x03a13423      // sd\ts10, 40(sp)                     # 8-byte Folded Spill\n\tWORD $0x08010413      // addi\ts0, sp, 128\n\tWORD $0xff017113      // andi\tsp, sp, -16\n\tWORD $0x01043303      // ld\tt1, 16(s0)\n\tWORD $0x00843283      // ld\tt0, 8(s0)\n\tWORD $0x02513023      // sd\tt0, 32(sp)                      # 8-byte Folded Spill\n\tWORD $0x00043383      // ld\tt2, 0(s0)\n\tWORD $0x01113823      // sd\ta7, 16(sp)                      # 8-byte Folded Spill\n\tWORD $0x00c13c23      // sd\ta2, 24(sp)                      # 8-byte Folded Spill\n\tBNEZ A0, LBB12_18\n\tBNEZ A1, LBB12_18\n\tWORD $0x01813503      // ld\ta0, 24(sp)                      # 8-byte Folded Reload\n\tBLEZ A0, LBB12_65\n\tBLEZ A4, LBB12_65\n\tBLEZ A3, LBB12_65\n\tWORD $0x00269f13      // slli\tt5, a3, 2\n\tWORD $0x00271f93      // slli\tt6, a4, 2\n\tWORD $0xfff70513      // addi\ta0, a4, -1\n\tWORD $0x02750533      // mul\ta0, a0, t2\n\tWORD $0x00d50eb3      // add\tt4, a0, a3\n\tWORD $0xc2202973      // csrr\ts2, vlenb\n\tWORD $0x00195513      // srli\ta0, s2, 1\n\tWORD $0x00800593      // li\ta1, 8\n\tWORD $0x002e9e93      // slli\tt4, t4, 2\n\tWORD $0x00050a13      // mv\ts4, a0\n\tBLTU A1, A0, LBB12_7\n\tWORD $0x00800a13      // li\ts4, 8\n\nLBB12_7:\n\tWORD $0x00000593 // li\ta1, 0\n\tWORD $0x00231293 // slli\tt0, t1, 2\n\tWORD $0x00281613 // slli\ta2, a6, 2\n\tWORD $0x01013883 // ld\ta7, 16(sp)                      # 8-byte Folded Reload\n\tWORD $0x01d88eb3 // add\tt4, a7, t4\n\tWORD $0x02013303 // ld\tt1, 32(sp)                      # 8-byte Folded Reload\n\tWORD $0x01e30f33 // add\tt5, t1, t5\n\tWORD $0x01f78fb3 // add\tt6, a5, t6\n\tWORD $0x00100893 // li\ta7, 1\n\tWORD $0x03d89893 // slli\ta7, a7, 61\n\tWORD $0x0113f8b3 // and\ta7, t2, a7\n\tWORD $0x00239e13 // slli\tt3, t2, 2\n\tWORD $0x00191913 // slli\ts2, s2, 1\n\tWORD $0x011039b3 // snez\ts3, a7\n\tWORD $0x0146ba33 // sltu\ts4, a3, s4\n\tJMP  LBB12_9\n\nLBB12_8:\n\tWORD $0x00158593      // addi\ta1, a1, 1\n\tWORD $0x00530333      // add\tt1, t1, t0\n\tWORD $0x01813883      // ld\ta7, 24(sp)                      # 8-byte Folded Reload\n\tBEQ  A1, A7, LBB12_65\n\nLBB12_9:\n\tWORD $0x00000b13 // li\ts6, 0\n\tWORD $0x02b288b3 // mul\ta7, t0, a1\n\tWORD $0x02013383 // ld\tt2, 32(sp)                      # 8-byte Folded Reload\n\tWORD $0x011383b3 // add\tt2, t2, a7\n\tWORD $0x011f08b3 // add\ta7, t5, a7\n\tWORD $0x02b60ab3 // mul\ts5, a2, a1\n\tWORD $0x01578bb3 // add\ts7, a5, s5\n\tWORD $0x015f8ab3 // add\ts5, t6, s5\n\tWORD $0x0153bab3 // sltu\ts5, t2, s5\n\tWORD $0x011bbbb3 // sltu\ts7, s7, a7\n\tWORD $0x017afab3 // and\ts5, s5, s7\n\tWORD $0x03058bb3 // mul\ts7, a1, a6\n\tWORD $0x002b9b93 // slli\ts7, s7, 2\n\tWORD $0x01778bb3 // add\ts7, a5, s7\n\tWORD $0x01d3b3b3 // sltu\tt2, t2, t4\n\tWORD $0x01013c83 // ld\ts9, 16(sp)                      # 8-byte Folded Reload\n\tWORD $0x011cb8b3 // sltu\ta7, s9, a7\n\tWORD $0x0113f8b3 // and\ta7, t2, a7\n\tWORD $0x0138e8b3 // or\ta7, a7, s3\n\tWORD $0x015a63b3 // or\tt2, s4, s5\n\tWORD $0x0113ec33 // or\ts8, t2, a7\n\tWORD $0x000c8393 // mv\tt2, s9\n\tJMP  LBB12_11\n\nLBB12_10:\n\tWORD $0x001b0b13     // addi\ts6, s6, 1\n\tWORD $0x01c383b3     // add\tt2, t2, t3\n\tBEQ  S6, A4, LBB12_8\n\nLBB12_11:\n\tWORD $0x002b1d13  // slli\ts10, s6, 2\n\tWORD $0x01ab8d33  // add\ts10, s7, s10\n\tBEQZ S8, LBB12_13\n\tWORD $0x00000093  // li\tra, 0\n\tJMP  LBB12_16\n\nLBB12_13:\n\tWORD $0x0d1078d7 // vsetvli\ta7, zero, e32, m2, ta, ma\n\tWORD $0x0a0d6407 // vlse32.v\tv8, (s10), zero\n\tWORD $0x40a008b3 // neg\ta7, a0\n\tWORD $0x00d8f0b3 // and\tra, a7, a3\n\tWORD $0x00008a93 // mv\ts5, ra\n\tWORD $0x00030c93 // mv\ts9, t1\n\tWORD $0x00038893 // mv\ta7, t2\n\nLBB12_14:\n\tWORD $0x2288e507      // vl2re32.v\tv10, (a7)\n\tWORD $0x228ce607      // vl2re32.v\tv12, (s9)\n\tWORD $0xb2a41657      // vfmacc.vv\tv12, v8, v10\n\tWORD $0x228c8627      // vs2r.v\tv12, (s9)\n\tWORD $0x012888b3      // add\ta7, a7, s2\n\tWORD $0x40aa8ab3      // sub\ts5, s5, a0\n\tWORD $0x012c8cb3      // add\ts9, s9, s2\n\tBNEZ S5, LBB12_14\n\tBEQ  RA, A3, LBB12_10\n\nLBB12_16:\n\tWORD $0x00209a93 // slli\ts5, ra, 2\n\tWORD $0x01538cb3 // add\ts9, t2, s5\n\tWORD $0x01530ab3 // add\ts5, t1, s5\n\tWORD $0x401680b3 // sub\tra, a3, ra\n\nLBB12_17:\n\tWORD $0x000d2787  // flw\tfa5, 0(s10)\n\tWORD $0x000ca707  // flw\tfa4, 0(s9)\n\tWORD $0x000aa687  // flw\tfa3, 0(s5)\n\tWORD $0x68e7f7c3  // fmadd.s\tfa5, fa5, fa4, fa3\n\tWORD $0x00faa027  // fsw\tfa5, 0(s5)\n\tWORD $0x004c8c93  // addi\ts9, s9, 4\n\tWORD $0xfff08093  // addi\tra, ra, -1\n\tWORD $0x004a8a93  // addi\ts5, s5, 4\n\tBNEZ RA, LBB12_17\n\tJMP  LBB12_10\n\nLBB12_18:\n\tBEQZ A1, LBB12_29\n\tBNEZ A0, LBB12_29\n\tWORD $0x01813503  // ld\ta0, 24(sp)                      # 8-byte Folded Reload\n\tBLEZ A0, LBB12_65\n\tBLEZ A3, LBB12_65\n\tWORD $0x0d007e57  // vsetvli\tt3, zero, e32, m1, ta, ma\n\tWORD $0x03c75533  // divu\ta0, a4, t3\n\tWORD $0x0005059b  // sext.w\ta1, a0\n\tWORD $0x5e003457  // vmv.v.i\tv8, 0\n\tWORD $0x03c50533  // mul\ta0, a0, t3\n\tWORD $0x40a70733  // sub\ta4, a4, a0\n\tWORD $0x0d077557  // vsetvli\ta0, a4, e32, m1, ta, ma\n\tBLEZ A1, LBB12_61\n\tWORD $0x00000713  // li\ta4, 0\n\tWORD $0x00281813  // slli\ta6, a6, 2\n\tWORD $0x002e1e13  // slli\tt3, t3, 2\n\tWORD $0x00239393  // slli\tt2, t2, 2\n\nLBB12_24:\n\tWORD $0x00000e93 // li\tt4, 0\n\tWORD $0x02670633 // mul\ta2, a4, t1\n\tWORD $0x00261613 // slli\ta2, a2, 2\n\tWORD $0x02013f03 // ld\tt5, 32(sp)                      # 8-byte Folded Reload\n\tWORD $0x00cf0f33 // add\tt5, t5, a2\n\tWORD $0x01013f83 // ld\tt6, 16(sp)                      # 8-byte Folded Reload\n\nLBB12_25:\n\tWORD $0x00000293 // li\tt0, 0\n\tWORD $0x0d007657 // vsetvli\ta2, zero, e32, m1, ta, ma\n\tWORD $0x000f8913 // mv\ts2, t6\n\tWORD $0x00078893 // mv\ta7, a5\n\tWORD $0x9e8034d7 // vmv1r.v\tv9, v8\n\nLBB12_26:\n\tWORD $0x0208e507      // vle32.v\tv10, (a7)\n\tWORD $0x02096587      // vle32.v\tv11, (s2)\n\tWORD $0xb2a594d7      // vfmacc.vv\tv9, v11, v10\n\tWORD $0x0012829b      // addiw\tt0, t0, 1\n\tWORD $0x01c888b3      // add\ta7, a7, t3\n\tWORD $0x01c90933      // add\ts2, s2, t3\n\tBLT  T0, A1, LBB12_26\n\tWORD $0x0e9414d7      // vfredosum.vs\tv9, v9, v8\n\tWORD $0x0d057057      // vsetvli\tzero, a0, e32, m1, ta, ma\n\tWORD $0x0208e507      // vle32.v\tv10, (a7)\n\tWORD $0x02096587      // vle32.v\tv11, (s2)\n\tWORD $0x92a59557      // vfmul.vv\tv10, v10, v11\n\tWORD $0x0ea494d7      // vfredosum.vs\tv9, v10, v9\n\tWORD $0x002e9613      // slli\ta2, t4, 2\n\tWORD $0x00cf0633      // add\ta2, t5, a2\n\tWORD $0xcd00f057      // vsetivli\tzero, 1, e32, m1, ta, ma\n\tWORD $0x020664a7      // vse32.v\tv9, (a2)\n\tWORD $0x001e8e93      // addi\tt4, t4, 1\n\tWORD $0x007f8fb3      // add\tt6, t6, t2\n\tBNE  T4, A3, LBB12_25\n\tWORD $0x00170713      // addi\ta4, a4, 1\n\tWORD $0x010787b3      // add\ta5, a5, a6\n\tWORD $0x01813603      // ld\ta2, 24(sp)                      # 8-byte Folded Reload\n\tBNE  A4, A2, LBB12_24\n\tJMP  LBB12_65\n\nLBB12_29:\n\tWORD $0x00e02633      // sgtz\ta2, a4\n\tWORD $0x00d028b3      // sgtz\ta7, a3\n\tWORD $0x01167633      // and\ta2, a2, a7\n\tBEQZ A0, LBB12_45\n\tBNEZ A1, LBB12_45\n\tWORD $0x01813503      // ld\ta0, 24(sp)                      # 8-byte Folded Reload\n\tWORD $0x00a02533      // sgtz\ta0, a0\n\tWORD $0x00c57533      // and\ta0, a0, a2\n\tBEQZ A0, LBB12_65\n\tWORD $0x00269f13      // slli\tt5, a3, 2\n\tWORD $0xfff70513      // addi\ta0, a4, -1\n\tWORD $0x030505b3      // mul\ta1, a0, a6\n\tWORD $0x00259f93      // slli\tt6, a1, 2\n\tWORD $0x02750533      // mul\ta0, a0, t2\n\tWORD $0x00d50533      // add\ta0, a0, a3\n\tWORD $0x00251e93      // slli\tt4, a0, 2\n\tWORD $0xc2202573      // csrr\ta0, vlenb\n\tWORD $0x00155593      // srli\ta1, a0, 1\n\tWORD $0x00800613      // li\ta2, 8\n\tWORD $0x01f78fb3      // add\tt6, a5, t6\n\tWORD $0x00058a93      // mv\ts5, a1\n\tBLTU A2, A1, LBB12_34\n\tWORD $0x00800a93      // li\ts5, 8\n\nLBB12_34:\n\tWORD $0x00000e13 // li\tt3, 0\n\tWORD $0x00231313 // slli\tt1, t1, 2\n\tWORD $0x01013603 // ld\ta2, 16(sp)                      # 8-byte Folded Reload\n\tWORD $0x01d60633 // add\ta2, a2, t4\n\tWORD $0x00c13423 // sd\ta2, 8(sp)                       # 8-byte Folded Spill\n\tWORD $0x02013b03 // ld\ts6, 32(sp)                      # 8-byte Folded Reload\n\tWORD $0x01eb0f33 // add\tt5, s6, t5\n\tWORD $0x004f8f93 // addi\tt6, t6, 4\n\tWORD $0x00100613 // li\ta2, 1\n\tWORD $0x03d61613 // slli\ta2, a2, 61\n\tWORD $0x00c878b3 // and\ta7, a6, a2\n\tWORD $0x00c3f633 // and\ta2, t2, a2\n\tWORD $0x00239393 // slli\tt2, t2, 2\n\tWORD $0x00151913 // slli\ts2, a0, 1\n\tWORD $0x00c039b3 // snez\ts3, a2\n\tWORD $0x01103a33 // snez\ts4, a7\n\tWORD $0x0156bab3 // sltu\ts5, a3, s5\n\tJMP  LBB12_36\n\nLBB12_35:\n\tWORD $0x001e0e13      // addi\tt3, t3, 1\n\tWORD $0x006b0b33      // add\ts6, s6, t1\n\tWORD $0x01813603      // ld\ta2, 24(sp)                      # 8-byte Folded Reload\n\tBEQ  T3, A2, LBB12_65\n\nLBB12_36:\n\tWORD $0x00000b93 // li\ts7, 0\n\tWORD $0x03c30633 // mul\ta2, t1, t3\n\tWORD $0x02013883 // ld\ta7, 32(sp)                      # 8-byte Folded Reload\n\tWORD $0x00c888b3 // add\ta7, a7, a2\n\tWORD $0x00cf0633 // add\ta2, t5, a2\n\tWORD $0x002e1293 // slli\tt0, t3, 2\n\tWORD $0x00578c33 // add\ts8, a5, t0\n\tWORD $0x005f82b3 // add\tt0, t6, t0\n\tWORD $0x0058b2b3 // sltu\tt0, a7, t0\n\tWORD $0x00cc3eb3 // sltu\tt4, s8, a2\n\tWORD $0x01d2f2b3 // and\tt0, t0, t4\n\tWORD $0x0142e2b3 // or\tt0, t0, s4\n\tWORD $0x00813e83 // ld\tt4, 8(sp)                       # 8-byte Folded Reload\n\tWORD $0x01d8b8b3 // sltu\ta7, a7, t4\n\tWORD $0x01013d03 // ld\ts10, 16(sp)                     # 8-byte Folded Reload\n\tWORD $0x00cd3633 // sltu\ta2, s10, a2\n\tWORD $0x00c8f633 // and\ta2, a7, a2\n\tWORD $0x01366633 // or\ta2, a2, s3\n\tWORD $0x00c2e633 // or\ta2, t0, a2\n\tWORD $0x00caecb3 // or\ts9, s5, a2\n\tJMP  LBB12_38\n\nLBB12_37:\n\tWORD $0x001b8b93      // addi\ts7, s7, 1\n\tWORD $0x007d0d33      // add\ts10, s10, t2\n\tBEQ  S7, A4, LBB12_35\n\nLBB12_38:\n\tWORD $0x030b8633  // mul\ta2, s7, a6\n\tWORD $0x00261613  // slli\ta2, a2, 2\n\tWORD $0x00cc00b3  // add\tra, s8, a2\n\tBEQZ S9, LBB12_40\n\tWORD $0x00000e93  // li\tt4, 0\n\tJMP  LBB12_43\n\nLBB12_40:\n\tWORD $0x00155613 // srli\ta2, a0, 1\n\tWORD $0x0d1078d7 // vsetvli\ta7, zero, e32, m2, ta, ma\n\tWORD $0x0a00e407 // vlse32.v\tv8, (ra), zero\n\tWORD $0x40c00633 // neg\ta2, a2\n\tWORD $0x00d67eb3 // and\tt4, a2, a3\n\tWORD $0x000e8893 // mv\ta7, t4\n\tWORD $0x000b0293 // mv\tt0, s6\n\tWORD $0x000d0613 // mv\ta2, s10\n\nLBB12_41:\n\tWORD $0x22866507      // vl2re32.v\tv10, (a2)\n\tWORD $0x2282e607      // vl2re32.v\tv12, (t0)\n\tWORD $0xb2a41657      // vfmacc.vv\tv12, v8, v10\n\tWORD $0x22828627      // vs2r.v\tv12, (t0)\n\tWORD $0x01260633      // add\ta2, a2, s2\n\tWORD $0x40b888b3      // sub\ta7, a7, a1\n\tWORD $0x012282b3      // add\tt0, t0, s2\n\tBNEZ A7, LBB12_41\n\tBEQ  T4, A3, LBB12_37\n\nLBB12_43:\n\tWORD $0x002e9293 // slli\tt0, t4, 2\n\tWORD $0x005d08b3 // add\ta7, s10, t0\n\tWORD $0x005b02b3 // add\tt0, s6, t0\n\tWORD $0x41d68eb3 // sub\tt4, a3, t4\n\nLBB12_44:\n\tWORD $0x0000a787  // flw\tfa5, 0(ra)\n\tWORD $0x0008a707  // flw\tfa4, 0(a7)\n\tWORD $0x0002a687  // flw\tfa3, 0(t0)\n\tWORD $0x68e7f7c3  // fmadd.s\tfa5, fa5, fa4, fa3\n\tWORD $0x00f2a027  // fsw\tfa5, 0(t0)\n\tWORD $0x00488893  // addi\ta7, a7, 4\n\tWORD $0xfffe8e93  // addi\tt4, t4, -1\n\tWORD $0x00428293  // addi\tt0, t0, 4\n\tBNEZ T4, LBB12_44\n\tJMP  LBB12_37\n\nLBB12_45:\n\tWORD $0x00b675b3      // and\ta1, a2, a1\n\tBEQZ A1, LBB12_65\n\tWORD $0x01813583      // ld\ta1, 24(sp)                      # 8-byte Folded Reload\n\tBLEZ A1, LBB12_65\n\tBEQZ A0, LBB12_65\n\tWORD $0x00269f13      // slli\tt5, a3, 2\n\tWORD $0xfff70513      // addi\ta0, a4, -1\n\tWORD $0x03050533      // mul\ta0, a0, a6\n\tWORD $0x00251f93      // slli\tt6, a0, 2\n\tWORD $0x00d70533      // add\ta0, a4, a3\n\tWORD $0x00251513      // slli\ta0, a0, 2\n\tWORD $0x01013e83      // ld\tt4, 16(sp)                      # 8-byte Folded Reload\n\tWORD $0x00ae8eb3      // add\tt4, t4, a0\n\tWORD $0xc2202573      // csrr\ta0, vlenb\n\tWORD $0x00155593      // srli\ta1, a0, 1\n\tWORD $0x00800613      // li\ta2, 8\n\tWORD $0x01f78fb3      // add\tt6, a5, t6\n\tWORD $0x00058913      // mv\ts2, a1\n\tBLTU A2, A1, LBB12_50\n\tWORD $0x00800913      // li\ts2, 8\n\nLBB12_50:\n\tWORD $0x00000e13 // li\tt3, 0\n\tWORD $0x00231313 // slli\tt1, t1, 2\n\tWORD $0xffce8e93 // addi\tt4, t4, -4\n\tWORD $0x01d13423 // sd\tt4, 8(sp)                       # 8-byte Folded Spill\n\tWORD $0x02013a83 // ld\ts5, 32(sp)                      # 8-byte Folded Reload\n\tWORD $0x01ea8f33 // add\tt5, s5, t5\n\tWORD $0x004f8f93 // addi\tt6, t6, 4\n\tWORD $0x0126b633 // sltu\ta2, a3, s2\n\tWORD $0xfff64613 // not\ta2, a2\n\tWORD $0xfff38893 // addi\ta7, t2, -1\n\tWORD $0x0018b893 // seqz\ta7, a7\n\tWORD $0x01167633 // and\ta2, a2, a7\n\tWORD $0x00100893 // li\ta7, 1\n\tWORD $0x03d89893 // slli\ta7, a7, 61\n\tWORD $0x011878b3 // and\ta7, a6, a7\n\tWORD $0x00151913 // slli\ts2, a0, 1\n\tWORD $0x00239e93 // slli\tt4, t2, 2\n\tWORD $0x011039b3 // snez\ts3, a7\n\tWORD $0x00164a13 // xori\ts4, a2, 1\n\tJMP  LBB12_52\n\nLBB12_51:\n\tWORD $0x001e0e13      // addi\tt3, t3, 1\n\tWORD $0x006a8ab3      // add\ts5, s5, t1\n\tWORD $0x01813603      // ld\ta2, 24(sp)                      # 8-byte Folded Reload\n\tBEQ  T3, A2, LBB12_65\n\nLBB12_52:\n\tWORD $0x00000b13 // li\ts6, 0\n\tWORD $0x03c30633 // mul\ta2, t1, t3\n\tWORD $0x02013883 // ld\ta7, 32(sp)                      # 8-byte Folded Reload\n\tWORD $0x00c888b3 // add\ta7, a7, a2\n\tWORD $0x00cf0633 // add\ta2, t5, a2\n\tWORD $0x002e1293 // slli\tt0, t3, 2\n\tWORD $0x00578bb3 // add\ts7, a5, t0\n\tWORD $0x005f82b3 // add\tt0, t6, t0\n\tWORD $0x0058b2b3 // sltu\tt0, a7, t0\n\tWORD $0x00cbb3b3 // sltu\tt2, s7, a2\n\tWORD $0x0072f2b3 // and\tt0, t0, t2\n\tWORD $0x0132e2b3 // or\tt0, t0, s3\n\tWORD $0x00813383 // ld\tt2, 8(sp)                       # 8-byte Folded Reload\n\tWORD $0x0078b8b3 // sltu\ta7, a7, t2\n\tWORD $0x01013c83 // ld\ts9, 16(sp)                      # 8-byte Folded Reload\n\tWORD $0x00ccb633 // sltu\ta2, s9, a2\n\tWORD $0x00c8f633 // and\ta2, a7, a2\n\tWORD $0x00ca6633 // or\ta2, s4, a2\n\tWORD $0x00566c33 // or\ts8, a2, t0\n\tJMP  LBB12_54\n\nLBB12_53:\n\tWORD $0x001b0b13      // addi\ts6, s6, 1\n\tWORD $0x004c8c93      // addi\ts9, s9, 4\n\tBEQ  S6, A4, LBB12_51\n\nLBB12_54:\n\tWORD $0x030b0633  // mul\ta2, s6, a6\n\tWORD $0x00261613  // slli\ta2, a2, 2\n\tWORD $0x00cb8d33  // add\ts10, s7, a2\n\tBEQZ S8, LBB12_56\n\tWORD $0x00000093  // li\tra, 0\n\tJMP  LBB12_59\n\nLBB12_56:\n\tWORD $0x00155613 // srli\ta2, a0, 1\n\tWORD $0x0d1078d7 // vsetvli\ta7, zero, e32, m2, ta, ma\n\tWORD $0x0a0d6407 // vlse32.v\tv8, (s10), zero\n\tWORD $0x40c00633 // neg\ta2, a2\n\tWORD $0x00d670b3 // and\tra, a2, a3\n\tWORD $0x00008893 // mv\ta7, ra\n\tWORD $0x000a8293 // mv\tt0, s5\n\tWORD $0x000c8613 // mv\ta2, s9\n\nLBB12_57:\n\tWORD $0x22866507      // vl2re32.v\tv10, (a2)\n\tWORD $0x2282e607      // vl2re32.v\tv12, (t0)\n\tWORD $0xb2a41657      // vfmacc.vv\tv12, v8, v10\n\tWORD $0x22828627      // vs2r.v\tv12, (t0)\n\tWORD $0x01260633      // add\ta2, a2, s2\n\tWORD $0x40b888b3      // sub\ta7, a7, a1\n\tWORD $0x012282b3      // add\tt0, t0, s2\n\tBNEZ A7, LBB12_57\n\tBEQ  RA, A3, LBB12_53\n\nLBB12_59:\n\tWORD $0x021e83b3 // mul\tt2, t4, ra\n\tWORD $0x007c83b3 // add\tt2, s9, t2\n\tWORD $0x00209893 // slli\ta7, ra, 2\n\tWORD $0x011a88b3 // add\ta7, s5, a7\n\tWORD $0x401682b3 // sub\tt0, a3, ra\n\nLBB12_60:\n\tWORD $0x000d2787  // flw\tfa5, 0(s10)\n\tWORD $0x0003a707  // flw\tfa4, 0(t2)\n\tWORD $0x0008a687  // flw\tfa3, 0(a7)\n\tWORD $0x68e7f7c3  // fmadd.s\tfa5, fa5, fa4, fa3\n\tWORD $0x00f8a027  // fsw\tfa5, 0(a7)\n\tWORD $0x01d383b3  // add\tt2, t2, t4\n\tWORD $0xfff28293  // addi\tt0, t0, -1\n\tWORD $0x00488893  // addi\ta7, a7, 4\n\tBNEZ T0, LBB12_60\n\tJMP  LBB12_53\n\nLBB12_61:\n\tWORD $0x00000593 // li\ta1, 0\n\tWORD $0x0d007657 // vsetvli\ta2, zero, e32, m1, ta, ma\n\tWORD $0x0e841457 // vfredosum.vs\tv8, v8, v8\n\tWORD $0x00239393 // slli\tt2, t2, 2\n\tWORD $0x00231313 // slli\tt1, t1, 2\n\nLBB12_62:\n\tWORD $0x03058633 // mul\ta2, a1, a6\n\tWORD $0x00261613 // slli\ta2, a2, 2\n\tWORD $0x00c78733 // add\ta4, a5, a2\n\tWORD $0x00068893 // mv\ta7, a3\n\tWORD $0x02013283 // ld\tt0, 32(sp)                      # 8-byte Folded Reload\n\tWORD $0x01013e03 // ld\tt3, 16(sp)                      # 8-byte Folded Reload\n\nLBB12_63:\n\tWORD $0x0d057057      // vsetvli\tzero, a0, e32, m1, ta, ma\n\tWORD $0x02076487      // vle32.v\tv9, (a4)\n\tWORD $0x020e6507      // vle32.v\tv10, (t3)\n\tWORD $0x929514d7      // vfmul.vv\tv9, v9, v10\n\tWORD $0x0e9414d7      // vfredosum.vs\tv9, v9, v8\n\tWORD $0xcd00f057      // vsetivli\tzero, 1, e32, m1, ta, ma\n\tWORD $0x0202e4a7      // vse32.v\tv9, (t0)\n\tWORD $0x007e0e33      // add\tt3, t3, t2\n\tWORD $0xfff88893      // addi\ta7, a7, -1\n\tWORD $0x00428293      // addi\tt0, t0, 4\n\tBNEZ A7, LBB12_63\n\tWORD $0x00158593      // addi\ta1, a1, 1\n\tWORD $0x02013603      // ld\ta2, 32(sp)                      # 8-byte Folded Reload\n\tWORD $0x00660633      // add\ta2, a2, t1\n\tWORD $0x02c13023      // sd\ta2, 32(sp)                      # 8-byte Folded Spill\n\tWORD $0x01813603      // ld\ta2, 24(sp)                      # 8-byte Folded Reload\n\tBNE  A1, A2, LBB12_62\n\nLBB12_65:\n\tWORD $0xf8040113 // addi\tsp, s0, -128\n\tWORD $0x07813083 // ld\tra, 120(sp)                     # 8-byte Folded Reload\n\tWORD $0x07013403 // ld\ts0, 112(sp)                     # 8-byte Folded Reload\n\tWORD $0x06813903 // ld\ts2, 104(sp)                     # 8-byte Folded Reload\n\tWORD $0x06013983 // ld\ts3, 96(sp)                      # 8-byte Folded Reload\n\tWORD $0x05813a03 // ld\ts4, 88(sp)                      # 8-byte Folded Reload\n\tWORD $0x05013a83 // ld\ts5, 80(sp)                      # 8-byte Folded Reload\n\tWORD $0x04813b03 // ld\ts6, 72(sp)                      # 8-byte Folded Reload\n\tWORD $0x04013b83 // ld\ts7, 64(sp)                      # 8-byte Folded Reload\n\tWORD $0x03813c03 // ld\ts8, 56(sp)                      # 8-byte Folded Reload\n\tWORD $0x03013c83 // ld\ts9, 48(sp)                      # 8-byte Folded Reload\n\tWORD $0x02813d03 // ld\ts10, 40(sp)                     # 8-byte Folded Reload\n\tWORD $0x08010113 // addi\tsp, sp, 128\n\tADDI 24, SP, SP\n\tRET\n"
  },
  {
    "path": "common/floats/floats_test.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage floats\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\nfunc TestMatZero(t *testing.T) {\n\ta := [][]float32{\n\t\t{3, 2, 5, 6, 0, 0},\n\t\t{1, 2, 3, 4, 5, 6},\n\t}\n\tMatZero(a)\n\tassert.Equal(t, [][]float32{\n\t\t{0, 0, 0, 0, 0, 0},\n\t\t{0, 0, 0, 0, 0, 0},\n\t}, a)\n}\n\nfunc TestZero(t *testing.T) {\n\ta := []float32{3, 2, 5, 6, 0, 0}\n\tZero(a)\n\tassert.Equal(t, []float32{0, 0, 0, 0, 0, 0}, a)\n}\n\nfunc TestAdd(t *testing.T) {\n\ta := []float32{1, 2, 3, 4}\n\tb := []float32{5, 6, 7, 8}\n\tAdd(a, b)\n\tassert.Equal(t, []float32{6, 8, 10, 12}, a)\n\tassert.Panics(t, func() { Add([]float32{1}, nil) })\n}\n\nfunc TestSub(t *testing.T) {\n\ta := []float32{1, 2, 3, 4}\n\tb := []float32{5, 6, 7, 8}\n\tSub(a, b)\n\tassert.Equal(t, []float32{-4, -4, -4, -4}, a)\n\tassert.Panics(t, func() { Sub([]float32{1}, nil) })\n}\n\nfunc TestSubTo(t *testing.T) {\n\ta := []float32{1, 2, 3, 4}\n\tb := []float32{5, 6, 7, 8}\n\tc := make([]float32, 4)\n\tSubTo(a, b, c)\n\tassert.Equal(t, []float32{-4, -4, -4, -4}, c)\n\tassert.Panics(t, func() { SubTo([]float32{1}, nil, nil) })\n}\n\nfunc TestMulTo(t *testing.T) {\n\ta := []float32{1, 2, 3, 4}\n\tb := []float32{5, 6, 7, 8}\n\tc := make([]float32, 4)\n\tMulTo(a, b, c)\n\tassert.Equal(t, []float32{5, 12, 21, 32}, c)\n\tassert.Panics(t, func() { MulTo([]float32{1}, nil, nil) })\n}\n\nfunc TestMulConst(t *testing.T) {\n\ta := []float32{1, 2, 3, 4}\n\tMulConst(a, 2)\n\tassert.Equal(t, []float32{2, 4, 6, 8}, a)\n}\n\nfunc TestDiv(t *testing.T) {\n\ta := []float32{1, 4, 9, 16}\n\tb := []float32{1, 2, 3, 4}\n\tDiv(a, b)\n\tassert.Equal(t, []float32{1, 2, 3, 4}, a)\n\tassert.Panics(t, func() { Div([]float32{1}, nil) })\n}\n\nfunc TestMulConstTo(t *testing.T) {\n\ta := []float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n\tdst := make([]float32, 11)\n\ttarget := []float32{0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20}\n\tMulConstTo(a, 2, dst)\n\tassert.Equal(t, target, dst)\n\tassert.Panics(t, func() { MulConstTo(nil, 2, dst) })\n}\n\nfunc TestMulConstAdd(t *testing.T) {\n\ta := []float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n\tdst := []float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n\ttarget := []float32{0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30}\n\tMulConstAdd(a, 2, dst)\n\tassert.Equal(t, target, dst)\n\tassert.Panics(t, func() { MulConstAdd(nil, 1, dst) })\n}\n\nfunc TestMulConstAddTo(t *testing.T) {\n\ta := []float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n\tb := []float32{0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20}\n\tdst := make([]float32, 11)\n\ttarget := []float32{0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50}\n\tMulConstAddTo(a, 3, b, dst)\n\tassert.Equal(t, target, dst)\n}\n\nfunc TestMulAddTo(t *testing.T) {\n\ta := []float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n\tb := []float32{0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20}\n\tc := []float32{0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30}\n\ttarget := []float32{0, 5, 14, 27, 44, 65, 90, 119, 152, 189, 230}\n\tMulAddTo(a, b, c)\n\tassert.Equal(t, target, c)\n\tassert.Panics(t, func() { MulAddTo(nil, nil, c) })\n}\n\nfunc TestAddTo(t *testing.T) {\n\ta := []float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n\tb := []float32{0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20}\n\tdst := make([]float32, 11)\n\ttarget := []float32{0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30}\n\tAddTo(a, b, dst)\n\tassert.Equal(t, target, dst)\n\tassert.Panics(t, func() { AddTo(nil, nil, dst) })\n}\n\nfunc TestAddConst(t *testing.T) {\n\ta := []float32{1, 2, 3, 4}\n\tAddConst(a, 2)\n\tassert.Equal(t, []float32{3, 4, 5, 6}, a)\n}\n\nfunc TestDivTo(t *testing.T) {\n\ta := []float32{1, 4, 9, 16}\n\tb := []float32{1, 2, 3, 4}\n\tc := make([]float32, 4)\n\tDivTo(a, b, c)\n\tassert.Equal(t, []float32{1, 2, 3, 4}, c)\n\tassert.Panics(t, func() { DivTo([]float32{1}, nil, nil) })\n}\n\nfunc TestSqrtTo(t *testing.T) {\n\ta := []float32{1, 4, 9, 16}\n\tb := make([]float32, 4)\n\tSqrtTo(a, b)\n\tassert.Equal(t, []float32{1, 2, 3, 4}, b)\n\tassert.Panics(t, func() { SqrtTo([]float32{1}, nil) })\n}\n\nfunc TestSqrt(t *testing.T) {\n\ta := []float32{1, 4, 9, 16}\n\tSqrt(a)\n\tassert.Equal(t, []float32{1, 2, 3, 4}, a)\n}\n\nfunc TestDot(t *testing.T) {\n\ta := []float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n\tb := []float32{0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20}\n\tassert.Equal(t, float32(770), Dot(a, b))\n\tassert.Panics(t, func() { Dot([]float32{1}, nil) })\n}\n\nfunc TestEuclidean(t *testing.T) {\n\ta := []float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n\tb := []float32{0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20}\n\tassert.Equal(t, float32(19.621416), Euclidean(a, b))\n\tassert.Panics(t, func() { Euclidean([]float32{1}, nil) })\n}\n\ntype NativeTestSuite struct {\n\tsuite.Suite\n}\n\nfunc (suite *NativeTestSuite) TestDot() {\n\ta := []float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n\tb := []float32{0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20}\n\tsuite.Equal(float32(770), dot(a, b))\n}\n\nfunc (suite *NativeTestSuite) TestEuclidean() {\n\ta := []float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n\tb := []float32{0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20}\n\tsuite.InDelta(float32(19.621416), euclidean(a, b), 1e-6)\n}\n\nfunc (suite *NativeTestSuite) TestMulConstAddTo() {\n\ta := []float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n\tb := []float32{0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20}\n\tdst := make([]float32, 11)\n\tmulConstAddTo(a, 3, b, dst)\n\tsuite.Equal([]float32{0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50}, dst)\n}\n\nfunc (suite *NativeTestSuite) TestMulConstAdd() {\n\ta := []float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n\tdst := []float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n\ttarget := []float32{0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30}\n\tmulConstAdd(a, 2, dst)\n\tsuite.Equal(target, dst)\n}\n\nfunc (suite *NativeTestSuite) TestMulConstTo() {\n\ta := []float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n\tdst := make([]float32, 11)\n\ttarget := []float32{0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20}\n\tmulConstTo(a, 2, dst)\n\tsuite.Equal(target, dst)\n}\n\nfunc (suite *NativeTestSuite) TestAddConst() {\n\ta := []float32{1, 2, 3, 4}\n\taddConst(a, 2)\n\tsuite.Equal([]float32{3, 4, 5, 6}, a)\n}\n\nfunc (suite *NativeTestSuite) TestSub() {\n\ta := []float32{1, 2, 3, 4}\n\tb := []float32{5, 6, 7, 8}\n\tsub(a, b)\n\tsuite.Equal([]float32{-4, -4, -4, -4}, a)\n}\n\nfunc (suite *NativeTestSuite) TestSubTo() {\n\ta := []float32{1, 2, 3, 4}\n\tb := []float32{5, 6, 7, 8}\n\tc := make([]float32, 4)\n\tsubTo(a, b, c)\n\tsuite.Equal([]float32{-4, -4, -4, -4}, c)\n}\n\nfunc (suite *NativeTestSuite) TestMulTo() {\n\ta := []float32{1, 2, 3, 4}\n\tb := []float32{5, 6, 7, 8}\n\tc := make([]float32, 4)\n\tmulTo(a, b, c)\n\tsuite.Equal([]float32{5, 12, 21, 32}, c)\n}\n\nfunc (suite *NativeTestSuite) TestDivTo() {\n\ta := []float32{1, 4, 9, 16}\n\tb := []float32{1, 2, 3, 4}\n\tc := make([]float32, 4)\n\tdivTo(a, b, c)\n\tsuite.Equal([]float32{1, 2, 3, 4}, c)\n}\n\nfunc (suite *NativeTestSuite) TestSqrtTo() {\n\ta := []float32{1, 4, 9, 16}\n\tb := make([]float32, 4)\n\tsqrtTo(a, b)\n\tsuite.Equal([]float32{1, 2, 3, 4}, b)\n}\n\nfunc (suite *NativeTestSuite) TestMulConst() {\n\ta := []float32{1, 2, 3, 4}\n\tmulConst(a, 2)\n\tsuite.Equal([]float32{2, 4, 6, 8}, a)\n}\n\nfunc (suite *NativeTestSuite) TestMM() {\n\ta := []float32{1, 2, 3, 4, 5, 6}\n\tb := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}\n\tc := make([]float32, 8)\n\ttarget := []float32{38, 44, 50, 56, 83, 98, 113, 128}\n\tmm(false, false, 2, 4, 3, a, 3, b, 4, c, 4)\n\tsuite.Equal(target, c)\n\n\tc = make([]float32, 8)\n\ttarget = []float32{14, 32, 50, 68, 32, 77, 122, 167}\n\tmm(false, true, 2, 4, 3, a, 3, b, 3, c, 4)\n\tsuite.Equal(target, c)\n\n\tc = make([]float32, 8)\n\ttarget = []float32{61, 70, 79, 88, 76, 88, 100, 112}\n\tmm(true, false, 2, 4, 3, a, 2, b, 4, c, 4)\n\tsuite.Equal(target, c)\n\n\tc = make([]float32, 8)\n\ttarget = []float32{22, 49, 76, 103, 28, 64, 100, 136}\n\tmm(true, true, 2, 4, 3, a, 2, b, 3, c, 4)\n\tsuite.Equal(target, c)\n}\n\nfunc TestNativeTestSuite(t *testing.T) {\n\tsuite.Run(t, new(NativeTestSuite))\n}\n\ntype SIMDTestSuite struct {\n\tsuite.Suite\n\tFeature\n}\n\nfunc (suite *SIMDTestSuite) SetupSuite() {\n\tif feature&suite.Feature != suite.Feature {\n\t\tsuite.T().Skipf(\"%s is not supported\", (suite.Feature - (feature & suite.Feature)).String())\n\t}\n}\n\nfunc (suite *SIMDTestSuite) TestMulConstAddTo() {\n\ta := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}\n\tb := []float32{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200}\n\tdst := make([]float32, len(a))\n\tsuite.mulConstAddTo(a, 2, b, dst)\n\tc := make([]float32, len(a))\n\tmulConstAddTo(a, 2, b, c)\n\tassert.Equal(suite.T(), c, dst)\n}\n\nfunc (suite *SIMDTestSuite) TestMulConstAdd() {\n\ta := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}\n\tb := []float32{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200}\n\tsuite.mulConstAdd(a, 2, b)\n\tc := []float32{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200}\n\tmulConstAdd(a, 2, c)\n\tassert.Equal(suite.T(), c, b)\n}\n\nfunc (suite *SIMDTestSuite) TestMulConstTo() {\n\ta := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}\n\tb := []float32{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200}\n\tsuite.mulConstTo(a, 2, b)\n\tc := []float32{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200}\n\tmulConstTo(a, 2, c)\n\tassert.Equal(suite.T(), c, b)\n}\n\nfunc (suite *SIMDTestSuite) TestAddConst() {\n\ta := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}\n\tsuite.addConst(a, 2)\n\tc := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}\n\taddConst(c, 2)\n\tassert.Equal(suite.T(), c, a)\n}\n\nfunc (suite *SIMDTestSuite) TestSub() {\n\ta := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}\n\tb := []float32{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200}\n\tsuite.sub(a, b)\n\tc := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}\n\tsub(c, b)\n\tsuite.Equal(c, a)\n}\n\nfunc (suite *SIMDTestSuite) TestSubTo() {\n\ta := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}\n\tb := []float32{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200}\n\tc := make([]float32, len(a))\n\tsuite.subTo(a, b, c)\n\td := make([]float32, len(a))\n\tsubTo(a, b, d)\n\tassert.Equal(suite.T(), c, d)\n}\n\nfunc (suite *SIMDTestSuite) TestMulTo() {\n\ta := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}\n\tb := []float32{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200}\n\texpected, actual := make([]float32, len(a)), make([]float32, len(a))\n\tsuite.mulTo(a, b, actual)\n\tmulTo(a, b, expected)\n\tassert.Equal(suite.T(), expected, actual)\n}\n\nfunc (suite *SIMDTestSuite) TestMulConst() {\n\tb := []float32{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200}\n\tsuite.mulConst(b, 2)\n\tc := []float32{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200}\n\tmulConst(c, 2)\n\tassert.Equal(suite.T(), c, b)\n}\n\nfunc (suite *SIMDTestSuite) TestDivTo() {\n\ta := []float32{1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400}\n\tb := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}\n\tc := make([]float32, len(a))\n\tsuite.divTo(a, b, c)\n\td := make([]float32, len(a))\n\tdivTo(a, b, d)\n\tassert.Equal(suite.T(), c, d)\n}\n\nfunc (suite *SIMDTestSuite) TestSqrtTo() {\n\ta := []float32{1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400}\n\tb := make([]float32, len(a))\n\tsuite.sqrtTo(a, b)\n\tc := make([]float32, len(a))\n\tsqrtTo(a, c)\n\tassert.Equal(suite.T(), b, c)\n}\n\nfunc (suite *SIMDTestSuite) TestDot() {\n\ta := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}\n\tb := []float32{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200}\n\tactual := suite.dot(a, b)\n\texpected := dot(a, b)\n\tassert.Equal(suite.T(), expected, actual)\n}\n\nfunc (suite *SIMDTestSuite) TestEuclidean() {\n\ta := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}\n\tb := []float32{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200}\n\tactual := suite.euclidean(a, b)\n\texpected := euclidean(a, b)\n\tassert.Equal(suite.T(), expected, actual)\n}\n\nfunc (suite *SIMDTestSuite) TestMM() {\n\ta := []float32{1, 2, 3, 4, 5, 6}\n\tb := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}\n\tc := make([]float32, 8)\n\ttarget := []float32{38, 44, 50, 56, 83, 98, 113, 128}\n\tsuite.mm(false, false, 2, 4, 3, a, 3, b, 4, c, 4)\n\tsuite.Equal(target, c)\n\n\tc = make([]float32, 8)\n\ttarget = []float32{14, 32, 50, 68, 32, 77, 122, 167}\n\tsuite.mm(false, true, 2, 4, 3, a, 3, b, 3, c, 4)\n\tsuite.Equal(target, c)\n\n\tc = make([]float32, 8)\n\ttarget = []float32{61, 70, 79, 88, 76, 88, 100, 112}\n\tsuite.mm(true, false, 2, 4, 3, a, 2, b, 4, c, 4)\n\tsuite.Equal(target, c)\n\n\tc = make([]float32, 8)\n\ttarget = []float32{22, 49, 76, 103, 28, 64, 100, 136}\n\tsuite.mm(true, true, 2, 4, 3, a, 2, b, 3, c, 4)\n\tsuite.Equal(target, c)\n}\n"
  },
  {
    "path": "common/floats/mm.go",
    "content": "//go:build !cgo || (!(darwin && arm64) && !mkl && !openblas)\n\n// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage floats\n\nfunc mm(transA, transB bool, m, n, k int, a []float32, lda int, b []float32, ldb int, c []float32, ldc int) {\n\tif !transA && !transB {\n\t\tfor i := 0; i < m; i++ {\n\t\t\tfor l := 0; l < k; l++ {\n\t\t\t\t// C_l += A_{il} * B_i\n\t\t\t\tMulConstAdd(b[l*ldb:(l+1)*ldb], a[i*lda+l], c[i*ldc:(i+1)*ldc])\n\t\t\t}\n\t\t}\n\t} else if !transA && transB {\n\t\tfor i := 0; i < m; i++ {\n\t\t\tfor j := 0; j < n; j++ {\n\t\t\t\tc[i*ldc+j] = Dot(a[i*lda:(i+1)*lda], b[j*ldb:(j+1)*ldb])\n\t\t\t}\n\t\t}\n\t} else if transA && !transB {\n\t\tfor i := 0; i < m; i++ {\n\t\t\tfor l := 0; l < k; l++ {\n\t\t\t\t// C_j += A_{ji} * B_i\n\t\t\t\tMulConstAdd(b[l*ldb:(l+1)*ldb], a[l*lda+i], c[i*ldc:(i+1)*ldc])\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor i := 0; i < m; i++ {\n\t\t\tfor j := 0; j < n; j++ {\n\t\t\t\tfor l := 0; l < k; l++ {\n\t\t\t\t\tc[i*ldc+j] += a[l*lda+i] * b[j*ldb+l]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "common/floats/mm_darwin_arm64.go",
    "content": "//go:build cgo\n\n// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage floats\n\nimport \"github.com/gorse-io/gorse/common/blas\"\n\nfunc init() {\n\tfeature = feature | AMX\n}\n\nfunc mm(transA, transB bool, m, n, k int, a []float32, lda int, b []float32, ldb int, c []float32, ldc int) {\n\tblas.SGEMM(blas.RowMajor, blas.NewTranspose(transA), blas.NewTranspose(transB),\n\t\tm, n, k, 1.0, a, lda, b, ldb, 0, c, ldc)\n}\n"
  },
  {
    "path": "common/floats/mm_mkl.go",
    "content": "//go:build cgo && mkl\n\n// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage floats\n\nimport \"github.com/gorse-io/gorse/common/blas\"\n\nfunc init() {\n\tfeature = feature | MKL\n}\n\nfunc mm(transA, transB bool, m, n, k int, a []float32, lda int, b []float32, ldb int, c []float32, ldc int) {\n\tblas.SGEMM(blas.RowMajor, blas.NewTranspose(transA), blas.NewTranspose(transB),\n\t\tm, n, k, 1.0, a, lda, b, ldb, 0, c, ldc)\n}\n"
  },
  {
    "path": "common/floats/mm_openblas.go",
    "content": "//go:build cgo && openblas\n\n// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage floats\n\nimport \"github.com/gorse-io/gorse/common/blas\"\n\nfunc init() {\n\tfeature = feature | OPENBLAS\n}\n\nfunc mm(transA, transB bool, m, n, k int, a []float32, lda int, b []float32, ldb int, c []float32, ldc int) {\n\tblas.SGEMM(blas.RowMajor, blas.NewTranspose(transA), blas.NewTranspose(transB),\n\t\tm, n, k, 1.0, a, lda, b, ldb, 0, c, ldc)\n}\n"
  },
  {
    "path": "common/floats/src/.gitignore",
    "content": "*\n!.gitignore\n!*.c\n!*.h\n!Makefile\n"
  },
  {
    "path": "common/floats/src/Makefile",
    "content": "SOURCES = munit.c floats_test.c\n\nifeq ($(shell uname -m),x86_64)\n\tSOURCES += floats_avx.c floats_avx512.c\n\tCFLAGS = -O3 -mavx -mavx512f -mavx512dq\nelse ifeq ($(shell uname -m),aarch64)\n\tSOURCES += floats_neon.c floats_sve2.c\n\tCFLAGS = -O3 -march=armv8-a+sve\nelse ifeq ($(shell uname -m),riscv64)\n\tSOURCES += floats_rvv.c\n\tCFLAGS = -O3 -march=rv64imafdv\nendif\n\nOBJECTS\t\t= $(SOURCES:.c=.o)\nDEPENDENCES\t= $(SOURCES:.c=.d)\nEXECUTE\t\t= floats_test\n\n$(EXECUTE): $(OBJECTS) \n\t$(CC) $(OBJECTS) -lm -o $(EXECUTE)\n\ntest: $(EXECUTE)\n\t./${EXECUTE}\n\nclean:\n\trm $(OBJECTS) $(DEPENDENCES) $(EXECUTE)\n\n-include $(DEPENDENCES)\n\n%.d: %.c\n\t@set -e; \\\n\trm -f $@; \\\n\t$(CC) $(CFLAGS) -MM -MT $(@:.d=.o) $< > $@.$$$$; \\\n\tsed 's,\\($*\\)\\.o[ :]*,\\1.o $@: ,g' $@.$$$$ > $@; \\\n\trm -f $@.$$$$\n"
  },
  {
    "path": "common/floats/src/floats_avx.c",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n#include <immintrin.h>\n#include <stdint.h>\n\nvoid _mm256_mul_const_add_to(float *a, float *b, float *c, float *dst, int64_t n)\n{\n    int epoch = n / 8;\n    int remain = n % 8;\n    for (int i = 0; i < epoch; i++)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_broadcast_ss(b);\n        __m256 v3 = _mm256_loadu_ps(c);\n        __m256 v = _mm256_add_ps(_mm256_mul_ps(v1, v2), v3);\n        _mm256_storeu_ps(dst, v);\n        a += 8;\n        c += 8;\n        dst += 8;\n    }\n    for (int i = 0; i < remain; i++)\n    {\n        dst[i] = c[i] + a[i] * b[0];\n    }\n}\n\nvoid _mm256_mul_const_add(float *a, float *b, float *c, int64_t n)\n{\n    int epoch = n / 8;\n    int remain = n % 8;\n    for (int i = 0; i < epoch; i++)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_broadcast_ss(b);\n        __m256 v3 = _mm256_loadu_ps(c);\n        __m256 v = _mm256_add_ps(_mm256_mul_ps(v1, v2), v3);\n        _mm256_storeu_ps(c, v);\n        a += 8;\n        c += 8;\n    }\n    for (int i = 0; i < remain; i++)\n    {\n        c[i] += a[i] * b[0];\n    }\n}\n\nvoid _mm256_mul_const_to(float *a, float *b, float *c, int64_t n)\n{\n    int epoch = n / 8;\n    int remain = n % 8;\n    for (int i = 0; i < epoch; i++)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_broadcast_ss(b);\n        __m256 v = _mm256_mul_ps(v1, v2);\n        _mm256_storeu_ps(c, v);\n        a += 8;\n        c += 8;\n    }\n    for (int i = 0; i < remain; i++)\n    {\n        c[i] = a[i] * b[0];\n    }\n}\n\nvoid _mm256_mul_const(float *a, float *b, int64_t n)\n{\n    int epoch = n / 8;\n    int remain = n % 8;\n    for (int i = 0; i < epoch; i++)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_broadcast_ss(b);\n        __m256 v = _mm256_mul_ps(v1, v2);\n        _mm256_storeu_ps(a, v);\n        a += 8;\n    }\n    for (int i = 0; i < remain; i++)\n    {\n        a[i] *= b[0];\n    }\n}\n\nvoid _mm256_add_const(float *a, float *b, int64_t n)\n{\n    int epoch = n / 8;\n    int remain = n % 8;\n    for (int i = 0; i < epoch; i++)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_broadcast_ss(b);\n        __m256 v = _mm256_add_ps(v1, v2);\n        _mm256_storeu_ps(a, v);\n        a += 8;\n    }\n    for (int i = 0; i < remain; i++)\n    {\n        a[i] += b[0];\n    }\n}\n\nvoid _mm256_sub_to(float *a, float *b, float *c, int64_t n)\n{\n    int epoch = n / 8;\n    int remain = n % 8;\n    for (int i = 0; i < epoch; i++)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_loadu_ps(b);\n        __m256 v = _mm256_sub_ps(v1, v2);\n        _mm256_storeu_ps(c, v);\n        a += 8;\n        b += 8;\n        c += 8;\n    }\n    for (int i = 0; i < remain; i++)\n    {\n        c[i] = a[i] - b[i];\n    }\n}\n\nvoid _mm256_sub(float *a, float *b, int64_t n)\n{\n    int epoch = n / 8;\n    int remain = n % 8;\n    for (int i = 0; i < epoch; i++)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_loadu_ps(b);\n        __m256 v = _mm256_sub_ps(v1, v2);\n        _mm256_storeu_ps(a, v);\n        a += 8;\n        b += 8;\n    }\n    for (int i = 0; i < remain; i++)\n    {\n        a[i] -= b[i];\n    }\n}\n\nvoid _mm256_mul_to(float *a, float *b, float *c, int64_t n)\n{\n    int epoch = n / 8;\n    int remain = n % 8;\n    for (int i = 0; i < epoch; i++)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_loadu_ps(b);\n        __m256 v = _mm256_mul_ps(v1, v2);\n        _mm256_storeu_ps(c, v);\n        a += 8;\n        b += 8;\n        c += 8;\n    }\n    for (int i = 0; i < remain; i++)\n    {\n        c[i] = a[i] * b[i];\n    }\n}\n\nvoid _mm256_div_to(float *a, float *b, float *c, int64_t n)\n{\n    int epoch = n / 8;\n    int remain = n % 8;\n    for (int i = 0; i < epoch; i++)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_loadu_ps(b);\n        __m256 v = _mm256_div_ps(v1, v2);\n        _mm256_storeu_ps(c, v);\n        a += 8;\n        b += 8;\n        c += 8;\n    }\n    for (int i = 0; i < remain; i++)\n    {\n        c[i] = a[i] / b[i];\n    }\n}\n\nvoid _mm256_sqrt_to(float *a, float *b, int64_t n)\n{\n    int epoch = n / 8;\n    int remain = n % 8;\n    for (int i = 0; i < epoch; i++)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_sqrt_ps(v1);\n        _mm256_storeu_ps(b, v2);\n        a += 8;\n        b += 8;\n    }\n    for (int i = 0; i < remain; i++)\n    {\n        __m128 v = _mm_set1_ps(a[i]);\n        __m128 r = _mm_sqrt_ss(v);\n        b[i] = _mm_cvtss_f32(r);\n    }\n}\n\ninline __attribute__((always_inline)) float dot(float *a, float *b, int64_t n)\n{\n    int epoch = n / 8;\n    int remain = n % 8;\n    __m256 s = _mm256_setzero_ps();\n    if (epoch > 0)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_loadu_ps(b);\n        s = _mm256_mul_ps(v1, v2);\n        a += 8;\n        b += 8;\n    }\n    for (int i = 1; i < epoch; i++)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_loadu_ps(b);\n        s = _mm256_add_ps(_mm256_mul_ps(v1, v2), s);\n        a += 8;\n        b += 8;\n    }\n    __m128 s7_6_5_4 = _mm256_extractf128_ps(s, 1);\n    __m128 s3_2_1_0 = _mm256_castps256_ps128(s);\n    __m128 s37_26_15_04 = _mm_add_ps(s7_6_5_4, s3_2_1_0);\n    __m128 sxx_15_04 = s37_26_15_04;\n    __m128 sxx_37_26 = _mm_movehl_ps(s37_26_15_04, s37_26_15_04);\n    const __m128 sxx_1357_0246 = _mm_add_ps(sxx_15_04, sxx_37_26);\n    const __m128 sxxx_0246 = sxx_1357_0246;\n    const __m128 sxxx_1357 = _mm_shuffle_ps(sxx_1357_0246, sxx_1357_0246, 0x1);\n    __m128 sxxx_01234567 = _mm_add_ss(sxxx_0246, sxxx_1357);\n    float sum = _mm_cvtss_f32(sxxx_01234567);\n    for (int i = 0; i < remain; i++)\n    {\n        sum += a[i] * b[i];\n    }\n    return sum;\n}\n\nfloat _mm256_dot(float *a, float *b, int64_t n)\n{\n    return dot(a, b, n);\n}\n\nfloat _mm256_euclidean(float *a, float *b, int64_t n)\n{\n    int epoch = n / 8;\n    int remain = n % 8;\n    __m256 sum = _mm256_setzero_ps();\n    if (epoch > 0)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_loadu_ps(b);\n        __m256 v = _mm256_sub_ps(v1, v2);\n        sum = _mm256_mul_ps(v, v);\n        a += 8;\n        b += 8;\n    }\n    for (int i = 1; i < epoch; i++)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_loadu_ps(b);\n        __m256 v = _mm256_sub_ps(v1, v2);\n        v = _mm256_mul_ps(v, v);\n        sum = _mm256_add_ps(v, sum);\n        a += 8;\n        b += 8;\n    }\n    __m128 s7_6_5_4 = _mm256_extractf128_ps(sum, 1);\n    __m128 s3_2_1_0 = _mm256_castps256_ps128(sum);\n    __m128 s37_26_15_04 = _mm_add_ps(s7_6_5_4, s3_2_1_0);\n    __m128 sxx_15_04 = s37_26_15_04;\n    __m128 sxx_37_26 = _mm_movehl_ps(s37_26_15_04, s37_26_15_04);\n    const __m128 sxx_1357_0246 = _mm_add_ps(sxx_15_04, sxx_37_26);\n    const __m128 sxxx_0246 = sxx_1357_0246;\n    const __m128 sxxx_1357 = _mm_shuffle_ps(sxx_1357_0246, sxx_1357_0246, 0x1);\n    __m128 sxxx_01234567 = _mm_add_ss(sxxx_0246, sxxx_1357);\n    float ret = _mm_cvtss_f32(sxxx_01234567);\n    for (int i = 0; i < remain; i++)\n    {\n        ret += (a[i] - b[i]) * (a[i] - b[i]);\n    }\n    __m128 v = _mm_set1_ps(ret);\n    __m128 r = _mm_sqrt_ss(v);\n    return _mm_cvtss_f32(r);\n}\n\nvoid _mm256_mm(_Bool transA, _Bool transB, int64_t m, int64_t n, int64_t k, float *a, int64_t lda, float *b, int64_t ldb, float *c, int64_t ldc)\n{\n    if (!transA && !transB)\n    {\n        for (int i = 0; i < m; i++) {\n            for (int l = 0; l < k; l++) {\n                for (int j = 0; j < n; j++) {\n                    c[i * ldc + j] += a[i * lda + l] * b[l * ldb + j];\n                }\n            }\n        }\n    } else if (!transA && transB)\n    {\n        for (int i = 0; i < m; i++) {\n            for (int j = 0; j < n; j++) {\n                c[i * ldc + j] = dot(a + i * lda, b + j * ldb, k);\n            }\n        }\n    } else if (transA && !transB)\n    {\n        for (int i = 0; i < m; i++) {\n            for (int l = 0; l < k; l++) {\n                for (int j = 0; j < n; j++) {\n                    c[i * ldc + j] += a[l * lda + i] * b[l * ldb + j];\n                }\n            }\n        }\n    } else if (transA && transB)\n    {\n        for (int i = 0; i < m; i++) {\n            for (int l = 0; l < k; l++) {\n                for (int j = 0; j < n; j++) {\n                    c[i * ldc + j] += a[l * lda + i] * b[j * ldb + l];\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "common/floats/src/floats_avx512.c",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n#include <immintrin.h>\n#include <stdint.h>\n\nvoid _mm512_mul_const_add_to(float *a, float *b, float *c, float *dst, int64_t n)\n{\n    int epoch = n / 16;\n    int remain = n % 16;\n    for (int i = 0; i < epoch; i++)\n    {\n        __m512 v1 = _mm512_loadu_ps(a);\n        __m512 v2 = _mm512_set1_ps(*b);\n        __m512 v3 = _mm512_loadu_ps(c);\n        __m512 v = _mm512_fmadd_ps(v1, v2, v3);\n        _mm512_storeu_ps(dst, v);\n        a += 16;\n        c += 16;\n        dst += 16;\n    }\n    if (remain >= 8)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_broadcast_ss(b);\n        __m256 v3 = _mm256_loadu_ps(c);\n        __m256 v = _mm256_add_ps(_mm256_mul_ps(v1, v2), v3);\n        _mm256_storeu_ps(dst, v);\n        a += 8;\n        c += 8;\n        dst += 8;\n        remain -= 8;\n    }\n    for (int i = 0; i < remain; i++)\n    {\n        dst[i] = c[i] + a[i] * b[0];\n    }\n}\n\nvoid _mm512_mul_const_add(float *a, float *b, float *c, int64_t n)\n{\n    int epoch = n / 16;\n    int remain = n % 16;\n    for (int i = 0; i < epoch; i++)\n    {\n        __m512 v1 = _mm512_loadu_ps(a);\n        __m512 v2 = _mm512_set1_ps(*b);\n        __m512 v3 = _mm512_loadu_ps(c);\n        __m512 v = _mm512_fmadd_ps(v1, v2, v3);\n        _mm512_storeu_ps(c, v);\n        a += 16;\n        c += 16;\n    }\n    if (remain >= 8)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_broadcast_ss(b);\n        __m256 v3 = _mm256_loadu_ps(c);\n        __m256 v = _mm256_add_ps(_mm256_mul_ps(v1, v2), v3);\n        _mm256_storeu_ps(c, v);\n        a += 8;\n        c += 8;\n        remain -= 8;\n    }\n    for (int i = 0; i < remain; i++)\n    {\n        c[i] += a[i] * b[0];\n    }\n}\n\nvoid _mm512_mul_const_to(float *a, float *b, float *c, int64_t n)\n{\n    int epoch = n / 16;\n    int remain = n % 16;\n    for (int i = 0; i < epoch; i++)\n    {\n        __m512 v1 = _mm512_loadu_ps(a);\n        __m512 v2 = _mm512_set1_ps(*b);\n        __m512 v = _mm512_mul_ps(v1, v2);\n        _mm512_storeu_ps(c, v);\n        a += 16;\n        c += 16;\n    }\n    if (remain >= 8)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_broadcast_ss(b);\n        __m256 v = _mm256_mul_ps(v1, v2);\n        _mm256_storeu_ps(c, v);\n        a += 8;\n        c += 8;\n        remain -= 8;\n    }\n    for (int i = 0; i < remain; i++)\n    {\n        c[i] = a[i] * b[0];\n    }\n}\n\nvoid _mm512_mul_const(float *a, float *b, int64_t n)\n{\n    int epoch = n / 16;\n    int remain = n % 16;\n    for (int i = 0; i < epoch; i++)\n    {\n        __m512 v1 = _mm512_loadu_ps(a);\n        __m512 v2 = _mm512_set1_ps(*b);\n        __m512 v = _mm512_mul_ps(v1, v2);\n        _mm512_storeu_ps(a, v);\n        a += 16;\n    }\n    if (remain >= 8)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_broadcast_ss(b);\n        __m256 v = _mm256_mul_ps(v1, v2);\n        _mm256_storeu_ps(a, v);\n        a += 8;\n        remain -= 8;\n    }\n    for (int i = 0; i < remain; i++)\n    {\n        a[i] *= b[0];\n    }\n}\n\nvoid _mm512_add_const(float *a, float *b, int64_t n)\n{\n    int epoch = n / 16;\n    int remain = n % 16;\n    for (int i = 0; i < epoch; i++)\n    {\n        __m512 v1 = _mm512_loadu_ps(a);\n        __m512 v2 = _mm512_set1_ps(*b);\n        __m512 v = _mm512_add_ps(v1, v2);\n        _mm512_storeu_ps(a, v);\n        a += 16;\n    }\n    if (remain >= 8)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_broadcast_ss(b);\n        __m256 v = _mm256_add_ps(v1, v2);\n        _mm256_storeu_ps(a, v);\n        a += 8;\n        remain -= 8;\n    }\n    for (int i = 0; i < remain; i++)\n    {\n        a[i] += b[0];\n    }\n}\n\nvoid _mm512_sub_to(float *a, float *b, float *c, int64_t n)\n{\n    int epoch = n / 16;\n    int remain = n % 16;\n    for (int i = 0; i < epoch; i++)\n    {\n        __m512 v1 = _mm512_loadu_ps(a);\n        __m512 v2 = _mm512_loadu_ps(b);\n        __m512 v = _mm512_sub_ps(v1, v2);\n        _mm512_storeu_ps(c, v);\n        a += 16;\n        b += 16;\n        c += 16;\n    }\n    if (remain >= 8)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_loadu_ps(b);\n        __m256 v = _mm256_sub_ps(v1, v2);\n        _mm256_storeu_ps(c, v);\n        a += 8;\n        b += 8;\n        c += 8;\n        remain -= 8;\n    }\n    for (int i = 0; i < remain; i++)\n    {\n        c[i] = a[i] - b[i];\n    }\n}\n\nvoid _mm512_sub(float *a, float *b, int64_t n)\n{\n    int epoch = n / 16;\n    int remain = n % 16;\n    for (int i = 0; i < epoch; i++)\n    {\n        __m512 v1 = _mm512_loadu_ps(a);\n        __m512 v2 = _mm512_loadu_ps(b);\n        __m512 v = _mm512_sub_ps(v1, v2);\n        _mm512_storeu_ps(a, v);\n        a += 16;\n        b += 16;\n    }\n    for (int i = 0; i < remain; i++)\n    {\n        a[i] -= b[i];\n    }\n}\n\nvoid _mm512_mul_to(float *a, float *b, float *c, int64_t n)\n{\n    int epoch = n / 16;\n    int remain = n % 16;\n    for (int i = 0; i < epoch; i++)\n    {\n        __m512 v1 = _mm512_loadu_ps(a);\n        __m512 v2 = _mm512_loadu_ps(b);\n        __m512 v = _mm512_mul_ps(v1, v2);\n        _mm512_storeu_ps(c, v);\n        a += 16;\n        b += 16;\n        c += 16;\n    }\n    if (remain >= 8)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_loadu_ps(b);\n        __m256 v = _mm256_mul_ps(v1, v2);\n        _mm256_storeu_ps(c, v);\n        a += 8;\n        b += 8;\n        c += 8;\n        remain -= 8;\n    }\n    for (int i = 0; i < remain; i++)\n    {\n        c[i] = a[i] * b[i];\n    }\n}\n\nvoid _mm512_div_to(float *a, float *b, float *c, int64_t n)\n{\n    int epoch = n / 16;\n    int remain = n % 16;\n    for (int i = 0; i < epoch; i++)\n    {\n        __m512 v1 = _mm512_loadu_ps(a);\n        __m512 v2 = _mm512_loadu_ps(b);\n        __m512 v = _mm512_div_ps(v1, v2);\n        _mm512_storeu_ps(c, v);\n        a += 16;\n        b += 16;\n        c += 16;\n    }\n    if (remain >= 8)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_loadu_ps(b);\n        __m256 v = _mm256_div_ps(v1, v2);\n        _mm256_storeu_ps(c, v);\n        a += 8;\n        b += 8;\n        c += 8;\n        remain -= 8;\n    }\n    for (int i = 0; i < remain; i++)\n    {\n        c[i] = a[i] / b[i];\n    }\n}\n\nvoid _mm512_sqrt_to(float *a, float *b, int64_t n)\n{\n    int epoch = n / 16;\n    int remain = n % 16;\n    for (int i = 0; i < epoch; i++)\n    {\n        __m512 v1 = _mm512_loadu_ps(a);\n        __m512 v2 = _mm512_sqrt_ps(v1);\n        _mm512_storeu_ps(b, v2);\n        a += 16;\n        b += 16;\n    }\n    if (remain >= 8)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_sqrt_ps(v1);\n        _mm256_storeu_ps(b, v2);\n        a += 8;\n        b += 8;\n        remain -= 8;\n    }\n    for (int i = 0; i < remain; i++)\n    {\n        __m128 v = _mm_set1_ps(a[i]);\n        __m128 r = _mm_sqrt_ss(v);\n        b[i] = _mm_cvtss_f32(r);\n    }\n}\n\ninline __attribute__((always_inline)) float dot(float *a, float *b, int64_t n)\n{\n    int epoch = n / 16;\n    int remain = n % 16;\n    __m512 s = _mm512_setzero_ps();\n    if (epoch > 0)\n    {\n        __m512 v1 = _mm512_loadu_ps(a);\n        __m512 v2 = _mm512_loadu_ps(b);\n        s = _mm512_mul_ps(v1, v2);\n        a += 16;\n        b += 16;\n    }\n    for (int i = 1; i < epoch; i++)\n    {\n        __m512 v1 = _mm512_loadu_ps(a);\n        __m512 v2 = _mm512_loadu_ps(b);\n        s = _mm512_fmadd_ps(v1, v2, s);\n        a += 16;\n        b += 16;\n    }\n    __m256 sf_e_d_c_b_a_9_8 = _mm256_castpd_ps(_mm512_extractf64x4_pd(_mm512_castps_pd(s), 1));\n    __m256 s7_6_5_4_3_2_1_0 = _mm512_castps512_ps256(s);\n    __m256 s7f_6e_5d_4c_3b_2a_19_08 = _mm256_add_ps(sf_e_d_c_b_a_9_8, s7_6_5_4_3_2_1_0);\n    __m128 s7f_6e_5d_4c = _mm_castsi128_ps(_mm256_extracti128_si256(_mm256_castps_si256(s7f_6e_5d_4c_3b_2a_19_08), 1));\n    __m128 s3b_2a_19_08 = _mm256_castps256_ps128(s7f_6e_5d_4c_3b_2a_19_08);\n    __m128 s37bf_26ae_159d_048c = _mm_add_ps(s7f_6e_5d_4c, s3b_2a_19_08);\n    __m128 sxx_159d_048c = s37bf_26ae_159d_048c;\n    __m128 sxx_37bf_26ae = _mm_movehl_ps(sxx_159d_048c, s37bf_26ae_159d_048c);\n    const __m128 sxx_13579bdf_02468ace = _mm_add_ps(sxx_159d_048c, sxx_37bf_26ae);\n    const __m128 sxxx_02468ace = sxx_13579bdf_02468ace;\n    const __m128 sxxx_13579bdf = _mm_shuffle_ps(sxx_13579bdf_02468ace, sxx_13579bdf_02468ace, 0x1);\n    __m128 sxxx_0123456789abcdef = _mm_add_ss(sxxx_02468ace, sxxx_13579bdf);\n    float sum = _mm_cvtss_f32(sxxx_0123456789abcdef);\n\n    if (remain >= 8)\n    {\n        __m256 s;\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_loadu_ps(b);\n        s = _mm256_mul_ps(v1, v2);\n        a += 8;\n        b += 8;\n        __m128 s7_6_5_4 = _mm256_extractf128_ps(s, 1);\n        __m128 s3_2_1_0 = _mm256_castps256_ps128(s);\n        __m128 s37_26_15_04 = _mm_add_ps(s7_6_5_4, s3_2_1_0);\n        __m128 sxx_15_04 = s37_26_15_04;\n        __m128 sxx_37_26 = _mm_movehl_ps(s37_26_15_04, s37_26_15_04);\n        const __m128 sxx_1357_0246 = _mm_add_ps(sxx_15_04, sxx_37_26);\n        const __m128 sxxx_0246 = sxx_1357_0246;\n        const __m128 sxxx_1357 = _mm_shuffle_ps(sxx_1357_0246, sxx_1357_0246, 0x1);\n        __m128 sxxx_01234567 = _mm_add_ss(sxxx_0246, sxxx_1357);\n        sum += _mm_cvtss_f32(sxxx_01234567);\n        remain -= 8;\n    }\n\n    for (int i = 0; i < remain; i++)\n    {\n        sum += a[i] * b[i];\n    }\n    return sum;\n}\n\nfloat _mm512_dot(float *a, float *b, int64_t n)\n{\n    return dot(a, b, n);\n}\n\nfloat _mm512_euclidean(float *a, float *b, int64_t n)\n{\n    int epoch = n / 16;\n    int remain = n % 16;\n    __m512 s = _mm512_setzero_ps();\n    if (epoch > 0)\n    {\n        __m512 v1 = _mm512_loadu_ps(a);\n        __m512 v2 = _mm512_loadu_ps(b);\n        __m512 v = _mm512_sub_ps(v1, v2);\n        s = _mm512_mul_ps(v, v);\n        a += 16;\n        b += 16;\n    }\n    for (int i = 1; i < epoch; i++)\n    {\n        __m512 v1 = _mm512_loadu_ps(a);\n        __m512 v2 = _mm512_loadu_ps(b);\n        __m512 v = _mm512_sub_ps(v1, v2);\n        v = _mm512_mul_ps(v, v);\n        s = _mm512_add_ps(v, s);\n        a += 16;\n        b += 16;\n    }\n    __m256 sf_e_d_c_b_a_9_8 = _mm256_castpd_ps(_mm512_extractf64x4_pd(_mm512_castps_pd(s), 1));\n    __m256 s7_6_5_4_3_2_1_0 = _mm512_castps512_ps256(s);\n    __m256 s7f_6e_5d_4c_3b_2a_19_08 = _mm256_add_ps(sf_e_d_c_b_a_9_8, s7_6_5_4_3_2_1_0);\n    __m128 s7f_6e_5d_4c = _mm_castsi128_ps(_mm256_extracti128_si256(_mm256_castps_si256(s7f_6e_5d_4c_3b_2a_19_08), 1));\n    __m128 s3b_2a_19_08 = _mm256_castps256_ps128(s7f_6e_5d_4c_3b_2a_19_08);\n    __m128 s37bf_26ae_159d_048c = _mm_add_ps(s7f_6e_5d_4c, s3b_2a_19_08);\n    __m128 sxx_159d_048c = s37bf_26ae_159d_048c;\n    __m128 sxx_37bf_26ae = _mm_movehl_ps(sxx_159d_048c, s37bf_26ae_159d_048c);\n    const __m128 sxx_13579bdf_02468ace = _mm_add_ps(sxx_159d_048c, sxx_37bf_26ae);\n    const __m128 sxxx_02468ace = sxx_13579bdf_02468ace;\n    const __m128 sxxx_13579bdf = _mm_shuffle_ps(sxx_13579bdf_02468ace, sxx_13579bdf_02468ace, 0x1);\n    __m128 sxxx_0123456789abcdef = _mm_add_ps(sxxx_02468ace, sxxx_13579bdf);\n    float sum = _mm_cvtss_f32(sxxx_0123456789abcdef);\n\n    if (remain >= 8)\n    {\n        __m256 v1 = _mm256_loadu_ps(a);\n        __m256 v2 = _mm256_loadu_ps(b);\n        __m256 v = _mm256_sub_ps(v1, v2);\n        v = _mm256_mul_ps(v, v);\n        a += 8;\n        b += 8;\n        __m128 s7_6_5_4 = _mm256_extractf128_ps(v, 1);\n        __m128 s3_2_1_0 = _mm256_castps256_ps128(v);\n        __m128 s37_26_15_04 = _mm_add_ps(s7_6_5_4, s3_2_1_0);\n        __m128 sxx_15_04 = s37_26_15_04;\n        __m128 sxx_37_26 = _mm_movehl_ps(s37_26_15_04, s37_26_15_04);\n        const __m128 sxx_1357_0246 = _mm_add_ps(sxx_15_04, sxx_37_26);\n        const __m128 sxxx_0246 = sxx_1357_0246;\n        const __m128 sxxx_1357 = _mm_shuffle_ps(sxx_1357_0246, sxx_1357_0246, 0x1);\n        __m128 sxxx_01234567 = _mm_add_ss(sxxx_0246, sxxx_1357);\n        sum += _mm_cvtss_f32(sxxx_01234567);\n        remain -= 8;\n    }\n\n    for (int i = 0; i < remain; i++)\n    {\n        sum += (a[i] - b[i]) * (a[i] - b[i]);\n    }\n\n    __m128 v = _mm_set1_ps(sum);\n    __m128 r = _mm_sqrt_ss(v);\n    return _mm_cvtss_f32(r);\n}\n\nvoid _mm512_mm(_Bool transA, _Bool transB, int64_t m, int64_t n, int64_t k, float *a, int64_t lda, float *b, int64_t ldb, float *c, int64_t ldc)\n{\n    if (!transA && !transB)\n    {\n        for (int i = 0; i < m; i++) {\n            for (int l = 0; l < k; l++) {\n                for (int j = 0; j < n; j++) {\n                    c[i * ldc + j] += a[i * lda + l] * b[l * ldb + j];\n                }\n            }\n        }\n    } else if (!transA && transB)\n    {\n        for (int i = 0; i < m; i++) {\n            for (int j = 0; j < n; j++) {\n                c[i * ldc + j] = dot(a + i * lda, b + j * ldb, k);\n            }\n        }\n    } else if (transA && !transB)\n    {\n        for (int i = 0; i < m; i++) {\n            for (int l = 0; l < k; l++) {\n                for (int j = 0; j < n; j++) {\n                    c[i * ldc + j] += a[l * lda + i] * b[l * ldb + j];\n                }\n            }\n        }\n    } else if (transA && transB)\n    {\n        for (int i = 0; i < m; i++) {\n            for (int l = 0; l < k; l++) {\n                for (int j = 0; j < n; j++) {\n                    c[i * ldc + j] += a[l * lda + i] * b[j * ldb + l];\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "common/floats/src/floats_neon.c",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n#include <arm_neon.h>\n#include <stdint.h>\n\nvoid vmul_const_add_to(float *a, float *b, float *c, float *dst, long n) {\n    for (int i = 0; i < n; i++) {\n        dst[i] = a[i] * (*b) + c[i];\n    }\n}\n\nvoid vmul_const_add(float *a, float *b, float *c, long n) {\n    for (int i = 0; i < n; i++) {\n        c[i] += a[i] * b[0];\n    }\n}\n\nvoid vmul_const_to(float *a, float *b, float *c, long n) {\n    for (int i = 0; i < n; i++) {\n        c[i] = a[i] * b[0];\n    }\n}\n\nvoid vmul_const(float *a, float *b, long n) {\n    for (int i = 0; i < n; i++) {\n        a[i] *= b[0];\n    }\n}\n\nvoid vadd_const(float *a, float *b, long n) {\n    for (int i = 0; i < n; i++) {\n        a[i] += b[0];\n    }\n}\n\nvoid vsub_to(float *a, float *b, float *c, long n) {\n    for (long i = 0; i < n; i++) {\n        c[i] = a[i] - b[i];\n    }\n}\n\nvoid vsub(float *a, float *b, long n) {\n    for (long i = 0; i < n; i++) {\n        a[i] -= b[i];\n    }\n}\n\nvoid vmul_to(float *a, float *b, float *c, long n) {\n    for (long i = 0; i < n; i++) {\n        c[i] = a[i] * b[i];\n    }\n}\n\nvoid vdiv_to(float *a, float *b, float *c, long n) {\n    for (int64_t i = 0; i < n; i++) {\n        c[i] = a[i] / b[i];\n    }\n}\n\nvoid vsqrt_to(float *a, float *b, long n) {\n    int epoch = n / 4;\n    int remain = n % 4;\n    for (int i = 0; i < epoch; i++) {\n        float32x4_t v1 = vld1q_f32(a);\n        float32x4_t v2 = vsqrtq_f32(v1);\n        vst1q_f32(b, v2);\n        a += 4;\n        b += 4;\n    }\n    for (int i = 0; i < remain; i++) {\n        float32x2_t v = vdup_n_f32(a[i]);\n        float32x2_t r = vsqrt_f32(v);\n        b[i] = vget_lane_f32(r, 0);\n    }\n}\n\ninline float dot(float *a, float *b, long n) {\n    int epoch = n / 4;\n    int remain = n % 4;\n    float32x4_t s = vdupq_n_f32(0);\n    if (epoch > 0) {\n        float32x4_t v1 = vld1q_f32(a);\n        float32x4_t v2 = vld1q_f32(b);\n        s = vmulq_f32(v1, v2);\n        a += 4;\n        b += 4;\n    }\n    for (int i = 1; i < epoch; i++) {\n        float32x4_t v1 = vld1q_f32(a);\n        float32x4_t v2 = vld1q_f32(b);\n        s = vmlaq_f32(s, v1, v2);\n        a += 4;\n        b += 4;\n    }\n    float partial[4];\n    vst1q_f32(partial, s);\n    float sum = 0;\n    for (int i = 0; i < 4; i++) {\n        sum += partial[i];\n    }\n    for (int i = 0; i < remain; i++) {\n        sum += a[i] * b[i];\n    }\n    return sum;\n}\n\nfloat vdot(float *a, float *b, long n) {\n    return dot(a, b, n);\n}\n\nfloat veuclidean(float *a, float *b, long n) {\n    int epoch = n / 4;\n    int remain = n % 4;\n    float32x4_t s = vdupq_n_f32(0);\n    if (epoch > 0) {\n        float32x4_t v1 = vld1q_f32(a);\n        float32x4_t v2 = vld1q_f32(b);\n        float32x4_t v = vsubq_f32(v1, v2);\n        s = vmulq_f32(v, v);\n        a += 4;\n        b += 4;\n    }\n    for (int i = 1; i < epoch; i++) {\n        float32x4_t v1 = vld1q_f32(a);\n        float32x4_t v2 = vld1q_f32(b);\n        float32x4_t v = vsubq_f32(v1, v2);\n        s = vmlaq_f32(s, v, v);\n        a += 4;\n        b += 4;\n    }\n    float partial[4];\n    vst1q_f32(partial, s);\n    float sum = 0;\n    for (int i = 0; i < 4; i++) {\n        sum += partial[i];\n    }\n    for (int i = 0; i < remain; i++) {\n        sum += (a[i] - b[i]) * (a[i] - b[i]);\n    }\n    float32x2_t v = vld1_f32(&sum);\n    float32x2_t r = vsqrt_f32(v);\n    return vget_lane_f32(r, 0);\n}\n\nvoid vmm(_Bool transA, _Bool transB, long m, long n, long k, float *a, long lda, float *b, long ldb, float *c, long ldc) {\n    if (!transA && !transB)\n    {\n        for (int i = 0; i < m; i++) {\n            for (int l = 0; l < k; l++) {\n                for (int j = 0; j < n; j++) {\n                    c[i * ldc + j] += a[i * lda + l] * b[l * ldb + j];\n                }\n            }\n        }\n    } else if (!transA && transB)\n    {\n        for (int i = 0; i < m; i++) {\n            for (int j = 0; j < n; j++) {\n                c[i * ldc + j] = dot(a + i * lda, b + j * ldb, k);\n            }\n        }\n    } else if (transA && !transB)\n    {\n        for (int i = 0; i < m; i++) {\n            for (int l = 0; l < k; l++) {\n                for (int j = 0; j < n; j++) {\n                    c[i * ldc + j] += a[l * lda + i] * b[l * ldb + j];\n                }\n            }\n        }\n    } else if (transA && transB)\n    {\n        for (int i = 0; i < m; i++) {\n            for (int l = 0; l < k; l++) {\n                for (int j = 0; j < n; j++) {\n                    c[i * ldc + j] += a[l * lda + i] * b[j * ldb + l];\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "common/floats/src/floats_rvv.c",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n#include <riscv_vector.h>\n\nvoid vmul_const_add_to(float *a, float *b, float *c, float *dst, long n) {\n    for (int i = 0; i < n; i++) {\n        dst[i] = a[i] * (*b) + c[i];\n    }\n}\n\nvoid vmul_const_add(float *a, float *b, float *c, long n) {\n    for (int i = 0; i < n; i++) {\n        c[i] += a[i] * b[0];\n    }\n}\n\nvoid vmul_const_to(float *a, float *b, float *c, long n) {\n    for (int i = 0; i < n; i++) {\n        c[i] = a[i] * b[0];\n    }\n}\n\nvoid vmul_const(float *a, float *b, long n) {\n    for (int i = 0; i < n; i++) {\n        a[i] *= b[0];\n    }\n}\n\nvoid vadd_const(float *a, float *b, long n) {\n    for (int i = 0; i < n; i++) {\n        a[i] += b[0];\n    }\n}\n\nvoid vsub_to(float *a, float *b, float *c, long n) {\n    for (long i = 0; i < n; i++) {\n        c[i] = a[i] - b[i];\n    }\n}\n\nvoid vsub(float *a, float *b, long n) {\n    for (long i = 0; i < n; i++) {\n        a[i] -= b[i];\n    }\n}\n\nvoid vmul_to(float *a, float *b, float *c, long n) {\n    for (long i = 0; i < n; i++) {\n        c[i] = a[i] * b[i];\n    }\n}\n\nvoid vdiv_to(float *a, float *b, float *c, long n) {\n    for (long i = 0; i < n; i++) {\n        c[i] = a[i] / b[i];\n    }\n}\n\nvoid vsqrt_to(float *a, float *b, long n) {\n    for (size_t vl; n > 0; a += vl, b += vl, n -= vl) {\n        vl = __riscv_vsetvl_e32m1(n);\n        vfloat32m1_t v1 = __riscv_vle32_v_f32m1(a, vl);\n        vfloat32m1_t v2 = __riscv_vfsqrt_v_f32m1(v1, vl);\n        __riscv_vse32_v_f32m1(b, v2, vl);\n    }\n}\n\ninline float dot(float *a, float *b, long n) {\n    size_t vlmax = __riscv_vsetvlmax_e32m1();\n    int epoch = n / vlmax;\n    int remain = n % vlmax;\n    vfloat32m1_t s1 = __riscv_vfmv_v_f_f32m1(0, vlmax);\n    for (int i = 0; i < epoch; i++) {\n        vfloat32m1_t v1 = __riscv_vle32_v_f32m1(a, vlmax);\n        vfloat32m1_t v2 = __riscv_vle32_v_f32m1(b, vlmax);\n        s1 = __riscv_vfmacc_vv_f32m1(s1, v1, v2, vlmax);\n        a += vlmax;\n        b += vlmax;\n    }\n    vfloat32m1_t s = __riscv_vfmv_v_f_f32m1(0, vlmax);\n    s = __riscv_vfredosum_vs_f32m1_f32m1(s1, s, vlmax);\n    size_t vl = __riscv_vsetvl_e32m1(remain);\n    vfloat32m1_t v1 = __riscv_vle32_v_f32m1(a, vl);\n    vfloat32m1_t v2 = __riscv_vle32_v_f32m1(b, vl);\n    vfloat32m1_t s2 = __riscv_vfmul_vv_f32m1(v1, v2, vl);\n    s = __riscv_vfredosum_vs_f32m1_f32m1(s2, s, vl);\n    return __riscv_vfmv_f_s_f32m1_f32(s);\n}\n\nfloat vdot(float *a, float *b, long n) {\n    return dot(a, b, n);\n}\n\nfloat veuclidean(float *a, float *b, long n) {\n    size_t vlmax = __riscv_vsetvlmax_e32m1();\n    int epoch = n / vlmax;\n    int remain = n % vlmax;\n    vfloat32m1_t s1 = __riscv_vfmv_v_f_f32m1(0, vlmax);\n    for (int i = 0; i < epoch; i++) {\n        vfloat32m1_t v1 = __riscv_vle32_v_f32m1(a, vlmax);\n        vfloat32m1_t v2 = __riscv_vle32_v_f32m1(b, vlmax);\n        vfloat32m1_t v = __riscv_vfsub_vv_f32m1(v1, v2, vlmax);\n        s1 = __riscv_vfmacc_vv_f32m1(s1, v, v, vlmax);\n        a += vlmax;\n        b += vlmax;\n    }\n    vfloat32m1_t s = __riscv_vfmv_v_f_f32m1(0, vlmax);\n    s = __riscv_vfredosum_vs_f32m1_f32m1(s1, s, vlmax);\n    size_t vl = __riscv_vsetvl_e32m1(remain);\n    vfloat32m1_t v1 = __riscv_vle32_v_f32m1(a, vl);\n    vfloat32m1_t v2 = __riscv_vle32_v_f32m1(b, vl);\n    vfloat32m1_t v = __riscv_vfsub_vv_f32m1(v1, v2, vlmax);\n    vfloat32m1_t s2 = __riscv_vfmul_vv_f32m1(v, v, vl);\n    s = __riscv_vfredosum_vs_f32m1_f32m1(s2, s, vl);\n    s = __riscv_vfsqrt_v_f32m1(s, vl);\n    return __riscv_vfmv_f_s_f32m1_f32(s);\n}\n\nvoid vmm(_Bool transA, _Bool transB, long m, long n, long k, float *a, long lda, float *b, long ldb, float *c, long ldc) {\n    if (!transA && !transB)\n    {\n        for (int i = 0; i < m; i++) {\n            for (int l = 0; l < k; l++) {\n                for (int j = 0; j < n; j++) {\n                    c[i * ldc + j] += a[i * lda + l] * b[l * ldb + j];\n                }\n            }\n        }\n    } else if (!transA && transB)\n    {\n        for (int i = 0; i < m; i++) {\n            for (int j = 0; j < n; j++) {\n                c[i * ldc + j] = dot(a + i * lda, b + j * ldb, k);\n            }\n        }\n    } else if (transA && !transB)\n    {\n        for (int i = 0; i < m; i++) {\n            for (int l = 0; l < k; l++) {\n                for (int j = 0; j < n; j++) {\n                    c[i * ldc + j] += a[l * lda + i] * b[l * ldb + j];\n                }\n            }\n        }\n    } else if (transA && transB)\n    {\n        for (int i = 0; i < m; i++) {\n            for (int l = 0; l < k; l++) {\n                for (int j = 0; j < n; j++) {\n                    c[i * ldc + j] += a[l * lda + i] * b[j * ldb + l];\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "common/floats/src/floats_sve2.c",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n#include <arm_sve.h>\n#include <stdint.h>\n\nvoid svmul_const_add_to(float *a, float *b, float *c, long n)\n{\n    for (long i = 0; i < n; i += svcntw())\n    {\n        svbool_t pg = svwhilelt_b32(i, n);\n        svfloat32_t a_seg = svld1(pg, a + i);\n        svfloat32_t c_seg = svld1(pg, c + i);\n        svst1(pg, c + i, svmla_x(pg, c_seg, a_seg, *b));\n    }\n}\n\nvoid svmul_const_to(float *a, float *b, float *c, long n)\n{\n    for (long i = 0; i < n; i += svcntw())\n    {\n        svbool_t pg = svwhilelt_b32(i, n);\n        svfloat32_t a_seg = svld1(pg, a + i);\n        svst1(pg, c + i, svmul_x(pg, a_seg, *b));\n    }\n}\n\nvoid svmul_const(float *a, float *b, long n)\n{\n    for (long i = 0; i < n; i += svcntw())\n    {\n        svbool_t pg = svwhilelt_b32(i, n);\n        svfloat32_t a_seg = svld1(pg, a + i);\n        svst1(pg, a + i, svmul_x(pg, a_seg, *b));\n    }\n}\n\nvoid svmul_to(float *a, float *b, float *c, long n)\n{\n    for (long i = 0; i < n; i += svcntw())\n    {\n        svbool_t pg = svwhilelt_b32(i, n);\n        svfloat32_t a_seg = svld1(pg, a + i);\n        svfloat32_t b_seg = svld1(pg, b + i);\n        svst1(pg, c + i, svmul_x(pg, a_seg, b_seg));\n    }\n}\n"
  },
  {
    "path": "common/floats/src/floats_test.c",
    "content": "#include \"munit.h\"\n#include \"math.h\"\n\nconst size_t kVectorLength = 63;\nconst size_t kIteration = 1;\n\n/* no simd */\n\nvoid mul_const_add_to(float *a, float *b, float *c, float *dst, int64_t n)\n{\n  for (int64_t i = 0; i < n; i++)\n  {\n    dst[i] = a[i] * (*b) + c[i];\n  }\n}\n\nvoid mul_const_add(float *a, float *b, float *c, int64_t n)\n{\n  for (int64_t i = 0; i < n; i++)\n  {\n    c[i] += a[i] * (*b);\n  }\n}\n\nvoid mul_const_to(float *a, float *b, float *c, int64_t n)\n{\n  for (int64_t i = 0; i < n; i++)\n  {\n    c[i] = a[i] * (*b);\n  }\n}\n\nvoid mul_const(float *a, float *b, int64_t n)\n{\n  for (int64_t i = 0; i < n; i++)\n  {\n    a[i] *= *b;\n  }\n}\n\nvoid sub_to(float *a, float *b, float *c, int64_t n)\n{\n  for (int64_t i = 0; i < n; i++)\n  {\n    c[i] = a[i] - b[i];\n  }\n}\n\nvoid sub(float *a, float *b, int64_t n)\n{\n  for (int64_t i = 0; i < n; i++)\n  {\n    a[i] -= b[i];\n  }\n}\n\nvoid mul_to(float *a, float *b, float *c, int64_t n)\n{\n  for (int64_t i = 0; i < n; i++)\n  {\n    c[i] = a[i] * b[i];\n  }\n}\n\nvoid div_to(float *a, float *b, float *c, int64_t n)\n{\n  for (int64_t i = 0; i < n; i++)\n  {\n    c[i] = a[i] / b[i];\n  }\n}\n\nvoid sqrt_to(float *a, float *b, int64_t n)\n{\n  for (int64_t i = 0; i < n; i++)\n  {\n    b[i] = sqrtf(a[i]);\n  }\n}\n\nfloat dot(float *a, float *b, int64_t n)\n{\n  float sum = 0;\n  for (int64_t i = 0; i < n; i++)\n  {\n    sum += a[i] * b[i];\n  }\n  return sum;\n}\n\nfloat euclidean(float *a, float *b, int64_t n)\n{\n  float sum = 0;\n  for (int64_t i = 0; i < n; i++)\n  {\n    sum += powf(a[i] - b[i], 2);\n  }\n  return sqrtf(sum);\n}\n\nint rand_float(float *a, int64_t n)\n{\n  for (int i = 0; i < n; i++)\n  {\n    a[i] = munit_rand_double();\n  }\n}\n\n#if defined(__x86_64__)\n\nvoid _mm256_mul_const_add_to(float *a, float *b, float *c, float *dst, int64_t n);\nvoid _mm256_mul_const_add(float *a, float *b, float *c, int64_t n);\nvoid _mm256_mul_const_to(float *a, float *b, float *c, int64_t n);\nvoid _mm256_mul_const(float *a, float *b, int64_t n);\nvoid _mm256_sub_to(float *a, float *b, float *c, int64_t n);\nvoid _mm256_sub(float *a, float *b, int64_t n);\nvoid _mm256_mul_to(float *a, float *b, float *c, int64_t n);\nvoid _mm256_div_to(float *a, float *b, float *c, int64_t n);\nvoid _mm256_sqrt_to(float *a, float *b, int64_t n);\nfloat _mm256_dot(float *a, float *b, int64_t n);\nfloat _mm256_euclidean(float *a, float *b, int64_t n);\n\nvoid _mm512_mul_const_add_to(float *a, float *b, float *c, float *dst, int64_t n);\nvoid _mm512_mul_const_add(float *a, float *b, float *c, int64_t n);\nvoid _mm512_mul_const_to(float *a, float *b, float *c, int64_t n);\nvoid _mm512_mul_const(float *a, float *b, int64_t n);\nvoid _mm512_sub_to(float *a, float *b, float *c, int64_t n);\nvoid _mm512_sub(float *a, float *b, int64_t n);\nvoid _mm512_mul_to(float *a, float *b, float *c, int64_t n);\nvoid _mm512_div_to(float *a, float *b, float *c, int64_t n);\nvoid _mm512_sqrt_to(float *a, float *b, int64_t n);\nfloat _mm512_dot(float *a, float *b, int64_t n);\nfloat _mm512_euclidean(float *a, float *b, int64_t n);\n\nMunitResult mm256_mul_const_add_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n  float d = munit_rand_double();\n\n  mul_const_add_to(a, &d, b, expect, kVectorLength);\n  _mm256_mul_const_add_to(a, &d, b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult mm256_mul_const_add_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(expect, kVectorLength);\n  memcpy(expect, actual, sizeof(float) * kVectorLength);\n  float b = munit_rand_double();\n\n  mul_const_add(a, &b, expect, kVectorLength);\n  _mm256_mul_const_add(a, &b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult mm256_mul_const_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  float b = munit_rand_double();\n\n  mul_const_to(a, &b, expect, kVectorLength);\n  _mm256_mul_const_to(a, &b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult mm256_mul_const_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float expect[kVectorLength], actual[kVectorLength];\n  rand_float(expect, kVectorLength);\n  memcpy(expect, actual, sizeof(float) * kVectorLength);\n  float b = munit_rand_double();\n\n  mul_const(expect, &b, kVectorLength);\n  _mm256_mul_const(actual, &b, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult mm256_sub_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n\n  sub_to(a, b, expect, kVectorLength);\n  _mm256_sub_to(a, b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult mm256_sub_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float expected[kVectorLength], actual[kVectorLength], b[kVectorLength];\n  rand_float(b, kVectorLength);\n  rand_float(expected, kVectorLength);\n  memcpy(expected, actual, sizeof(float) * kVectorLength);\n\n  sub(expected, b, kVectorLength);\n  _mm256_sub(actual, b, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expected, actual);\n  return MUNIT_OK;\n}\n\nMunitResult mm256_mul_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n\n  mul_to(a, b, expect, kVectorLength);\n  _mm256_mul_to(a, b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult mm256_div_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n\n  div_to(a, b, expect, kVectorLength);\n  _mm256_div_to(a, b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult mm256_sqrt_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  memcpy(expect, actual, sizeof(float) * kVectorLength);\n\n  sqrt_to(a, expect, kVectorLength);\n  _mm256_sqrt_to(a, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult mm256_dot_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n\n  float expect = dot(a, b, kVectorLength);\n  float actual = _mm256_dot(a, b, kVectorLength);\n  munit_assert_float_equal(expect, actual, 5);\n  return MUNIT_OK;\n}\n\nMunitResult mm256_euclidean_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n\n  float expect = euclidean(a, b, kVectorLength);\n  float actual = _mm256_euclidean(a, b, kVectorLength);\n  munit_assert_float_equal(expect, actual, 5);\n  return MUNIT_OK;\n}\n\nMunitTest mm256_tests[] = {\n    {\"mul_const_add_to\", mm256_mul_const_add_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"mul_const_add\", mm256_mul_const_add_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"mul_const_to\", mm256_mul_const_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"mul_const\", mm256_mul_const_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"sub\", mm256_sub_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"sub_to\", mm256_sub_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"mul_to\", mm256_mul_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"div_to\", mm256_div_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"sqrt_to\", mm256_sqrt_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"dot\", mm256_dot_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"euclidean\", mm256_euclidean_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}};\n\nstatic const MunitSuite mm256_suite = {\n    \"mm256_\", mm256_tests, NULL, kIteration, MUNIT_SUITE_OPTION_NONE};\n\nMunitResult mm512_mul_const_add_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength], c[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n  rand_float(c, kVectorLength);\n  memcpy(expect, actual, sizeof(float) * kVectorLength);\n  float d = munit_rand_double();\n\n  mul_const_add_to(a, &d, b, expect, kVectorLength);\n  _mm512_mul_const_add_to(a, &d, b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult mm512_mul_const_add_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(expect, kVectorLength);\n  memcpy(expect, actual, sizeof(float) * kVectorLength);\n  float b = munit_rand_double();\n\n  mul_const_add(a, &b, expect, kVectorLength);\n  _mm512_mul_const_add(a, &b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult mm512_mul_const_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  float b = munit_rand_double();\n\n  mul_const_to(a, &b, expect, kVectorLength);\n  _mm512_mul_const_to(a, &b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult mm512_mul_const_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float expect[kVectorLength], actual[kVectorLength];\n  rand_float(expect, kVectorLength);\n  memcpy(expect, actual, sizeof(float) * kVectorLength);\n  float b = munit_rand_double();\n\n  mul_const(expect, &b, kVectorLength);\n  _mm512_mul_const(actual, &b, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult mm512_sub_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float expected[kVectorLength], actual[kVectorLength], b[kVectorLength];\n  rand_float(b, kVectorLength);\n  rand_float(expected, kVectorLength);\n  memcpy(expected, actual, sizeof(float) * kVectorLength);\n\n  sub(expected, b, kVectorLength);\n  _mm512_sub(actual, b, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expected, actual);\n  return MUNIT_OK;\n}\n\nMunitResult mm512_sub_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n\n  sub_to(a, b, expect, kVectorLength);\n  _mm512_sub_to(a, b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult mm512_mul_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n\n  mul_to(a, b, expect, kVectorLength);\n  _mm512_mul_to(a, b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult mm512_div_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n\n  div_to(a, b, expect, kVectorLength);\n  _mm512_div_to(a, b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult mm512_sqrt_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  memcpy(expect, actual, sizeof(float) * kVectorLength);\n\n  sqrt_to(a, expect, kVectorLength);\n  _mm512_sqrt_to(a, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult mm512_dot_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n\n  float expect = dot(a, b, kVectorLength);\n  float actual = _mm512_dot(a, b, kVectorLength);\n  munit_assert_float_equal(expect, actual, 5);\n  return MUNIT_OK;\n}\n\nMunitResult mm512_euclidean_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n\n  float expect = euclidean(a, b, kVectorLength);\n  float actual = _mm512_euclidean(a, b, kVectorLength);\n  munit_assert_float_equal(expect, actual, 5);\n  return MUNIT_OK;\n}\n\nMunitTest mm512_tests[] = {\n    {\"mul_const_add_to\", mm512_mul_const_add_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"mul_const_add\", mm512_mul_const_add_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"mul_const_to\", mm512_mul_const_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"mul_const\", mm512_mul_const_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"sub_to\", mm512_sub_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"sub\", mm512_sub_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"mul_to\", mm512_mul_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"div_to\", mm512_div_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"sqrt_to\", mm512_sqrt_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"dot\", mm512_dot_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"euclidean\", mm512_euclidean_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}};\n\nstatic const MunitSuite mm512_suite = {\n    \"mm512_\", mm512_tests, NULL, kIteration, MUNIT_SUITE_OPTION_NONE};\n\nint main(int argc, char *const argv[MUNIT_ARRAY_PARAM(argc + 1)])\n{\n  munit_suite_main(&mm256_suite, NULL, argc, argv);\n  munit_suite_main(&mm512_suite, NULL, argc, argv);\n  return 0;\n}\n\n#elif defined(__aarch64__)\n\nvoid vmul_const_add_to(float *a, float *b, float *c, float *dst, int64_t n);\nvoid vmul_const_add(float *a, float *b, float *c, int64_t n);\nvoid vmul_const_to(float *a, float *b, float *c, int64_t n);\nvoid vmul_const(float *a, float *b, int64_t n);\nvoid vsub_to(float *a, float *b, float *c, int64_t n);\nvoid vsub(float *a, float *b, int64_t n);\nvoid vmul_to(float *a, float *b, float *c, int64_t n);\nvoid vdiv_to(float *a, float *b, float *c, int64_t n);\nvoid vsqrt_to(float *a, float *b, int64_t n);\nfloat vdot(float *a, float *b, int64_t n);\nfloat veuclidean(float *a, float *b, int64_t n);\n\nMunitResult vmul_const_add_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n  float d = munit_rand_double();\n\n  mul_const_add_to(a, &d, b, expect, kVectorLength);\n  vmul_const_add_to(a, &d, b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult vmul_const_add_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(expect, kVectorLength);\n  memcpy(expect, actual, sizeof(float) * kVectorLength);\n  float b = munit_rand_double();\n\n  mul_const_add(a, &b, expect, kVectorLength);\n  vmul_const_add(a, &b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult vmul_const_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  float b = munit_rand_double();\n\n  mul_const_to(a, &b, expect, kVectorLength);\n  vmul_const_to(a, &b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult vmul_const_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float expect[kVectorLength], actual[kVectorLength];\n  rand_float(expect, kVectorLength);\n  memcpy(expect, actual, sizeof(float) * kVectorLength);\n  float b = munit_rand_double();\n\n  mul_const(expect, &b, kVectorLength);\n  vmul_const(actual, &b, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult vmul_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n\n  mul_to(a, b, expect, kVectorLength);\n  vmul_to(a, b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult vdot_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n\n  float expect = dot(a, b, kVectorLength);\n  float actual = vdot(a, b, kVectorLength);\n  munit_assert_float_equal(expect, actual, 5);\n  return MUNIT_OK;\n}\n\nMunitResult veuclidean_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n\n  float expect = euclidean(a, b, kVectorLength);\n  float actual = veuclidean(a, b, kVectorLength);\n  munit_assert_float_equal(expect, actual, 5);\n  return MUNIT_OK;\n}\n\nMunitTest vtests[] = {\n    {\"mul_const_add_to\", vmul_const_add_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"mul_const_to\", vmul_const_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"mul_const\", vmul_const_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"mul_to\", vmul_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"dot\", vdot_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"euclidean\", veuclidean_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}};\n\nstatic const MunitSuite vsuite = {\n    \"v\", vtests, NULL, kIteration, MUNIT_SUITE_OPTION_NONE};\n\nvoid svmul_const_add_to(float *a, float *b, float *c, float *dst, long n);\nvoid svmul_const_to(float *a, float *b, float *c, long n);\nvoid svmul_const(float *a, float *b, long n);\nvoid svmul_to(float *a, float *b, float *c, long n);\n\nMunitResult svmul_const_add_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n  float d = munit_rand_double();\n\n  mul_const_add_to(a, &d, b, expect, kVectorLength);\n  svmul_const_add_to(a, &d, b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult svmul_const_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  float b = munit_rand_double();\n\n  mul_const_to(a, &b, expect, kVectorLength);\n  svmul_const_to(a, &b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult svmul_const_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float expect[kVectorLength], actual[kVectorLength];\n  rand_float(expect, kVectorLength);\n  memcpy(expect, actual, sizeof(float) * kVectorLength);\n  float b = munit_rand_double();\n\n  mul_const(expect, &b, kVectorLength);\n  svmul_const(actual, &b, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult svmul_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n\n  mul_to(a, b, expect, kVectorLength);\n  svmul_to(a, b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitTest svtests[] = {\n    {\"mul_const_add_to\", svmul_const_add_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"mul_const_to\", svmul_const_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"mul_const\", svmul_const_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"mul_to\", svmul_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}};\n\nstatic const MunitSuite svsuite = {\n    \"sv\", svtests, NULL, kIteration, MUNIT_SUITE_OPTION_NONE};\n\nint main(int argc, char *const argv[MUNIT_ARRAY_PARAM(argc + 1)])\n{\n  munit_suite_main(&vsuite, NULL, argc, argv);\n  munit_suite_main(&svsuite, NULL, argc, argv);\n  return 0;\n}\n\n#elif defined(__riscv) && (__riscv_xlen == 64)\n\nvoid vmul_const_add_to(float *a, float *b, float *c, float *dst, int64_t n);\nvoid vmul_const_add(float *a, float *b, float *c, int64_t n);\nvoid vmul_const_to(float *a, float *b, float *c, int64_t n);\nvoid vmul_const(float *a, float *b, int64_t n);\nvoid vsub_to(float *a, float *b, float *c, int64_t n);\nvoid vsub(float *a, float *b, int64_t n);\nvoid vmul_to(float *a, float *b, float *c, int64_t n);\nvoid vdiv_to(float *a, float *b, float *c, int64_t n);\nvoid vsqrt_to(float *a, float *b, int64_t n);\nfloat vdot(float *a, float *b, int64_t n);\nfloat veuclidean(float *a, float *b, int64_t n);\n\nMunitResult vmul_const_add_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n  float d = munit_rand_double();\n\n  mul_const_add_to(a, &d, b, expect, kVectorLength);\n  vmul_const_add_to(a, &d, b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult vmul_const_add_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(expect, kVectorLength);\n  memcpy(expect, actual, sizeof(float) * kVectorLength);\n  float b = munit_rand_double();\n\n  mul_const_add(a, &b, expect, kVectorLength);\n  vmul_const_add(a, &b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult vmul_const_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  float b = munit_rand_double();\n\n  mul_const_to(a, &b, expect, kVectorLength);\n  vmul_const_to(a, &b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult vmul_const_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float expect[kVectorLength], actual[kVectorLength];\n  rand_float(expect, kVectorLength);\n  memcpy(expect, actual, sizeof(float) * kVectorLength);\n  float b = munit_rand_double();\n\n  mul_const(expect, &b, kVectorLength);\n  vmul_const(actual, &b, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult vmul_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n\n  mul_to(a, b, expect, kVectorLength);\n  vmul_to(a, b, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult vsqrt_to_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], expect[kVectorLength], actual[kVectorLength];\n  rand_float(a, kVectorLength);\n  memcpy(expect, actual, sizeof(float) * kVectorLength);\n\n  sqrt_to(a, expect, kVectorLength);\n  vsqrt_to(a, actual, kVectorLength);\n  munit_assert_floats_equal(kVectorLength, expect, actual);\n  return MUNIT_OK;\n}\n\nMunitResult vdot_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n\n  float expect = dot(a, b, kVectorLength);\n  float actual = vdot(a, b, kVectorLength);\n  munit_assert_float_equal(expect, actual, 5);\n  return MUNIT_OK;\n}\n\nMunitResult veuclidean_test(const MunitParameter params[], void *user_data_or_fixture)\n{\n  float a[kVectorLength], b[kVectorLength];\n  rand_float(a, kVectorLength);\n  rand_float(b, kVectorLength);\n\n  float expect = euclidean(a, b, kVectorLength);\n  float actual = veuclidean(a, b, kVectorLength);\n  munit_assert_float_equal(expect, actual, 5);\n  return MUNIT_OK;\n}\n\nMunitTest vtests[] = {\n    {\"mul_const_add_to\", vmul_const_add_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"mul_const_to\", vmul_const_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"mul_const\", vmul_const_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"mul_to\", vmul_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"sqrt_to\", vsqrt_to_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"dot\", vdot_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {\"euclidean\", veuclidean_test, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL},\n    {NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}};\n\nstatic const MunitSuite vsuite = {\n    \"v\", vtests, NULL, kIteration, MUNIT_SUITE_OPTION_NONE};\n\nint main(int argc, char *const argv[MUNIT_ARRAY_PARAM(argc + 1)])\n{\n  munit_suite_main(&vsuite, NULL, argc, argv);\n  return 0;\n}\n\n#endif\n"
  },
  {
    "path": "common/floats/src/munit.c",
    "content": "/* Copyright (c) 2013-2018 Evan Nemerson <evan@nemerson.com>\n *\n * Permission is hereby granted, free of charge, to any person\n * obtaining a copy of this software and associated documentation\n * files (the \"Software\"), to deal in the Software without\n * restriction, including without limitation the rights to use, copy,\n * modify, merge, publish, distribute, sublicense, and/or sell copies\n * of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS\n * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/*** Configuration ***/\n\n/* This is just where the output from the test goes.  It's really just\n * meant to let you choose stdout or stderr, but if anyone really want\n * to direct it to a file let me know, it would be fairly easy to\n * support. */\n#if !defined(MUNIT_OUTPUT_FILE)\n#  define MUNIT_OUTPUT_FILE stdout\n#endif\n\n/* This is a bit more useful; it tells µnit how to format the seconds in\n * timed tests.  If your tests run for longer you might want to reduce\n * it, and if your computer is really fast and your tests are tiny you\n * can increase it. */\n#if !defined(MUNIT_TEST_TIME_FORMAT)\n#  define MUNIT_TEST_TIME_FORMAT \"0.8f\"\n#endif\n\n/* If you have long test names you might want to consider bumping\n * this.  The result information takes 43 characters. */\n#if !defined(MUNIT_TEST_NAME_LEN)\n#  define MUNIT_TEST_NAME_LEN 37\n#endif\n\n/* If you don't like the timing information, you can disable it by\n * defining MUNIT_DISABLE_TIMING. */\n#if !defined(MUNIT_DISABLE_TIMING)\n#  define MUNIT_ENABLE_TIMING\n#endif\n\n/*** End configuration ***/\n\n#if defined(_POSIX_C_SOURCE) && (_POSIX_C_SOURCE < 200809L)\n#  undef _POSIX_C_SOURCE\n#endif\n#if !defined(_POSIX_C_SOURCE)\n#  define _POSIX_C_SOURCE 200809L\n#endif\n\n/* Solaris freaks out if you try to use a POSIX or SUS standard without\n * the \"right\" C standard. */\n#if defined(_XOPEN_SOURCE)\n#  undef _XOPEN_SOURCE\n#endif\n\n#if defined(__STDC_VERSION__)\n#  if __STDC_VERSION__ >= 201112L\n#    define _XOPEN_SOURCE 700\n#  elif __STDC_VERSION__ >= 199901L\n#    define _XOPEN_SOURCE 600\n#  endif\n#endif\n\n/* Because, according to Microsoft, POSIX is deprecated.  You've got\n * to appreciate the chutzpah. */\n#if defined(_MSC_VER) && !defined(_CRT_NONSTDC_NO_DEPRECATE)\n#  define _CRT_NONSTDC_NO_DEPRECATE\n#endif\n\n#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)\n#  include <stdbool.h>\n#elif defined(_WIN32)\n/* https://msdn.microsoft.com/en-us/library/tf4dy80a.aspx */\n#endif\n\n#include <limits.h>\n#include <time.h>\n#include <errno.h>\n#include <string.h>\n#include <stdlib.h>\n#include <stdio.h>\n#include <stdarg.h>\n#include <setjmp.h>\n\n#if !defined(MUNIT_NO_NL_LANGINFO) && !defined(_WIN32)\n#define MUNIT_NL_LANGINFO\n#include <locale.h>\n#include <langinfo.h>\n#include <strings.h>\n#endif\n\n#if !defined(_WIN32)\n#  include <unistd.h>\n#  include <sys/types.h>\n#  include <sys/wait.h>\n#else\n#  include <windows.h>\n#  include <io.h>\n#  include <fcntl.h>\n#  if !defined(STDERR_FILENO)\n#    define STDERR_FILENO _fileno(stderr)\n#  endif\n#endif\n\n#include \"munit.h\"\n\n#define MUNIT_STRINGIFY(x) #x\n#define MUNIT_XSTRINGIFY(x) MUNIT_STRINGIFY(x)\n\n#if defined(__GNUC__) || defined(__INTEL_COMPILER) || defined(__SUNPRO_CC) || defined(__IBMCPP__)\n#  define MUNIT_THREAD_LOCAL __thread\n#elif (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201102L)) || defined(_Thread_local)\n#  define MUNIT_THREAD_LOCAL _Thread_local\n#elif defined(_WIN32)\n#  define MUNIT_THREAD_LOCAL __declspec(thread)\n#endif\n\n/* MSVC 12.0 will emit a warning at /W4 for code like 'do { ... }\n * while (0)', or 'do { ... } while (1)'.  I'm pretty sure nobody\n * at Microsoft compiles with /W4. */\n#if defined(_MSC_VER) && (_MSC_VER <= 1800)\n#pragma warning(disable: 4127)\n#endif\n\n#if defined(_WIN32) || defined(__EMSCRIPTEN__)\n#  define MUNIT_NO_FORK\n#endif\n\n#if defined(__EMSCRIPTEN__)\n#  define MUNIT_NO_BUFFER\n#endif\n\n/*** Logging ***/\n\nstatic MunitLogLevel munit_log_level_visible = MUNIT_LOG_INFO;\nstatic MunitLogLevel munit_log_level_fatal = MUNIT_LOG_ERROR;\n\n#if defined(MUNIT_THREAD_LOCAL)\nstatic MUNIT_THREAD_LOCAL munit_bool munit_error_jmp_buf_valid = 0;\nstatic MUNIT_THREAD_LOCAL jmp_buf munit_error_jmp_buf;\n#endif\n\n/* At certain warning levels, mingw will trigger warnings about\n * suggesting the format attribute, which we've explicitly *not* set\n * because it will then choke on our attempts to use the MS-specific\n * I64 modifier for size_t (which we have to use since MSVC doesn't\n * support the C99 z modifier). */\n\n#if defined(__MINGW32__) || defined(__MINGW64__)\n#  pragma GCC diagnostic push\n#  pragma GCC diagnostic ignored \"-Wsuggest-attribute=format\"\n#endif\n\nMUNIT_PRINTF(5,0)\nstatic void\nmunit_logf_exv(MunitLogLevel level, FILE* fp, const char* filename, int line, const char* format, va_list ap) {\n  if (level < munit_log_level_visible)\n    return;\n\n  switch (level) {\n    case MUNIT_LOG_DEBUG:\n      fputs(\"Debug\", fp);\n      break;\n    case MUNIT_LOG_INFO:\n      fputs(\"Info\", fp);\n      break;\n    case MUNIT_LOG_WARNING:\n      fputs(\"Warning\", fp);\n      break;\n    case MUNIT_LOG_ERROR:\n      fputs(\"Error\", fp);\n      break;\n    default:\n      munit_logf_ex(MUNIT_LOG_ERROR, filename, line, \"Invalid log level (%d)\", level);\n      return;\n  }\n\n  fputs(\": \", fp);\n  if (filename != NULL)\n    fprintf(fp, \"%s:%d: \", filename, line);\n  vfprintf(fp, format, ap);\n  fputc('\\n', fp);\n}\n\nMUNIT_PRINTF(3,4)\nstatic void\nmunit_logf_internal(MunitLogLevel level, FILE* fp, const char* format, ...) {\n  va_list ap;\n\n  va_start(ap, format);\n  munit_logf_exv(level, fp, NULL, 0, format, ap);\n  va_end(ap);\n}\n\nstatic void\nmunit_log_internal(MunitLogLevel level, FILE* fp, const char* message) {\n  munit_logf_internal(level, fp, \"%s\", message);\n}\n\nvoid\nmunit_logf_ex(MunitLogLevel level, const char* filename, int line, const char* format, ...) {\n  va_list ap;\n\n  va_start(ap, format);\n  munit_logf_exv(level, stderr, filename, line, format, ap);\n  va_end(ap);\n\n  if (level >= munit_log_level_fatal) {\n#if defined(MUNIT_THREAD_LOCAL)\n    if (munit_error_jmp_buf_valid)\n      longjmp(munit_error_jmp_buf, 1);\n#endif\n    abort();\n  }\n}\n\nvoid\nmunit_errorf_ex(const char* filename, int line, const char* format, ...) {\n  va_list ap;\n\n  va_start(ap, format);\n  munit_logf_exv(MUNIT_LOG_ERROR, stderr, filename, line, format, ap);\n  va_end(ap);\n\n#if defined(MUNIT_THREAD_LOCAL)\n  if (munit_error_jmp_buf_valid)\n    longjmp(munit_error_jmp_buf, 1);\n#endif\n  abort();\n}\n\n#if defined(__MINGW32__) || defined(__MINGW64__)\n#pragma GCC diagnostic pop\n#endif\n\n#if !defined(MUNIT_STRERROR_LEN)\n#  define MUNIT_STRERROR_LEN 80\n#endif\n\nstatic void\nmunit_log_errno(MunitLogLevel level, FILE* fp, const char* msg) {\n#if defined(MUNIT_NO_STRERROR_R) || (defined(__MINGW32__) && !defined(MINGW_HAS_SECURE_API))\n  munit_logf_internal(level, fp, \"%s: %s (%d)\", msg, strerror(errno), errno);\n#else\n  char munit_error_str[MUNIT_STRERROR_LEN];\n  munit_error_str[0] = '\\0';\n\n#if !defined(_WIN32)\n  strerror_r(errno, munit_error_str, MUNIT_STRERROR_LEN);\n#else\n  strerror_s(munit_error_str, MUNIT_STRERROR_LEN, errno);\n#endif\n\n  munit_logf_internal(level, fp, \"%s: %s (%d)\", msg, munit_error_str, errno);\n#endif\n}\n\n/*** Memory allocation ***/\n\nvoid*\nmunit_malloc_ex(const char* filename, int line, size_t size) {\n  void* ptr;\n\n  if (size == 0)\n    return NULL;\n\n  ptr = calloc(1, size);\n  if (MUNIT_UNLIKELY(ptr == NULL)) {\n    munit_logf_ex(MUNIT_LOG_ERROR, filename, line, \"Failed to allocate %\" MUNIT_SIZE_MODIFIER \"u bytes.\", size);\n  }\n\n  return ptr;\n}\n\n/*** Timer code ***/\n\n#if defined(MUNIT_ENABLE_TIMING)\n\n#define psnip_uint64_t munit_uint64_t\n#define psnip_uint32_t munit_uint32_t\n\n/* Code copied from portable-snippets\n * <https://github.com/nemequ/portable-snippets/>.  If you need to\n * change something, please do it there so we can keep the code in\n * sync. */\n\n/* Clocks (v1)\n * Portable Snippets - https://gitub.com/nemequ/portable-snippets\n * Created by Evan Nemerson <evan@nemerson.com>\n *\n *   To the extent possible under law, the authors have waived all\n *   copyright and related or neighboring rights to this code.  For\n *   details, see the Creative Commons Zero 1.0 Universal license at\n *   https://creativecommons.org/publicdomain/zero/1.0/\n */\n\n#if !defined(PSNIP_CLOCK_H)\n#define PSNIP_CLOCK_H\n\n#if !defined(psnip_uint64_t)\n#  include \"../exact-int/exact-int.h\"\n#endif\n\n#if !defined(PSNIP_CLOCK_STATIC_INLINE)\n#  if defined(__GNUC__)\n#    define PSNIP_CLOCK__COMPILER_ATTRIBUTES __attribute__((__unused__))\n#  else\n#    define PSNIP_CLOCK__COMPILER_ATTRIBUTES\n#  endif\n\n#  define PSNIP_CLOCK__FUNCTION PSNIP_CLOCK__COMPILER_ATTRIBUTES static\n#endif\n\nenum PsnipClockType {\n  /* This clock provides the current time, in units since 1970-01-01\n   * 00:00:00 UTC not including leap seconds.  In other words, UNIX\n   * time.  Keep in mind that this clock doesn't account for leap\n   * seconds, and can go backwards (think NTP adjustments). */\n  PSNIP_CLOCK_TYPE_WALL = 1,\n  /* The CPU time is a clock which increases only when the current\n   * process is active (i.e., it doesn't increment while blocking on\n   * I/O). */\n  PSNIP_CLOCK_TYPE_CPU = 2,\n  /* Monotonic time is always running (unlike CPU time), but it only\n     ever moves forward unless you reboot the system.  Things like NTP\n     adjustments have no effect on this clock. */\n  PSNIP_CLOCK_TYPE_MONOTONIC = 3\n};\n\nstruct PsnipClockTimespec {\n  psnip_uint64_t seconds;\n  psnip_uint64_t nanoseconds;\n};\n\n/* Methods we support: */\n\n#define PSNIP_CLOCK_METHOD_CLOCK_GETTIME                   1\n#define PSNIP_CLOCK_METHOD_TIME                            2\n#define PSNIP_CLOCK_METHOD_GETTIMEOFDAY                    3\n#define PSNIP_CLOCK_METHOD_QUERYPERFORMANCECOUNTER         4\n#define PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME              5\n#define PSNIP_CLOCK_METHOD_CLOCK                           6\n#define PSNIP_CLOCK_METHOD_GETPROCESSTIMES                 7\n#define PSNIP_CLOCK_METHOD_GETRUSAGE                       8\n#define PSNIP_CLOCK_METHOD_GETSYSTEMTIMEPRECISEASFILETIME  9\n#define PSNIP_CLOCK_METHOD_GETTICKCOUNT64                 10\n\n#include <assert.h>\n\n#if defined(HEDLEY_UNREACHABLE)\n#  define PSNIP_CLOCK_UNREACHABLE() HEDLEY_UNREACHABLE()\n#else\n#  define PSNIP_CLOCK_UNREACHABLE() assert(0)\n#endif\n\n/* Choose an implementation */\n\n/* #undef PSNIP_CLOCK_WALL_METHOD */\n/* #undef PSNIP_CLOCK_CPU_METHOD */\n/* #undef PSNIP_CLOCK_MONOTONIC_METHOD */\n\n/* We want to be able to detect the libc implementation, so we include\n   <limits.h> (<features.h> isn't available everywhere). */\n\n#if defined(__unix__) || defined(__unix) || defined(__linux__)\n#  include <limits.h>\n#  include <unistd.h>\n#endif\n\n#if defined(_POSIX_TIMERS) && (_POSIX_TIMERS > 0)\n/* These are known to work without librt.  If you know of others\n * please let us know so we can add them. */\n#  if \\\n  (defined(__GLIBC__) && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 17))) || \\\n  (defined(__FreeBSD__))\n#    define PSNIP_CLOCK_HAVE_CLOCK_GETTIME\n#  elif !defined(PSNIP_CLOCK_NO_LIBRT)\n#    define PSNIP_CLOCK_HAVE_CLOCK_GETTIME\n#  endif\n#endif\n\n#if defined(_WIN32)\n#  if !defined(PSNIP_CLOCK_CPU_METHOD)\n#    define PSNIP_CLOCK_CPU_METHOD PSNIP_CLOCK_METHOD_GETPROCESSTIMES\n#  endif\n#  if !defined(PSNIP_CLOCK_MONOTONIC_METHOD)\n#    define PSNIP_CLOCK_MONOTONIC_METHOD PSNIP_CLOCK_METHOD_QUERYPERFORMANCECOUNTER\n#  endif\n#endif\n\n#if defined(__MACH__) && !defined(__gnu_hurd__)\n#  if !defined(PSNIP_CLOCK_MONOTONIC_METHOD)\n#    define PSNIP_CLOCK_MONOTONIC_METHOD PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME\n#  endif\n#endif\n\n#if defined(PSNIP_CLOCK_HAVE_CLOCK_GETTIME)\n#  include <time.h>\n#  if !defined(PSNIP_CLOCK_WALL_METHOD)\n#    if defined(CLOCK_REALTIME_PRECISE)\n#      define PSNIP_CLOCK_WALL_METHOD PSNIP_CLOCK_METHOD_CLOCK_GETTIME\n#      define PSNIP_CLOCK_CLOCK_GETTIME_WALL CLOCK_REALTIME_PRECISE\n#    elif !defined(__sun)\n#      define PSNIP_CLOCK_WALL_METHOD PSNIP_CLOCK_METHOD_CLOCK_GETTIME\n#      define PSNIP_CLOCK_CLOCK_GETTIME_WALL CLOCK_REALTIME\n#    endif\n#  endif\n#  if !defined(PSNIP_CLOCK_CPU_METHOD)\n#    if defined(_POSIX_CPUTIME) || defined(CLOCK_PROCESS_CPUTIME_ID)\n#      define PSNIP_CLOCK_CPU_METHOD PSNIP_CLOCK_METHOD_CLOCK_GETTIME\n#      define PSNIP_CLOCK_CLOCK_GETTIME_CPU CLOCK_PROCESS_CPUTIME_ID\n#    elif defined(CLOCK_VIRTUAL)\n#      define PSNIP_CLOCK_CPU_METHOD PSNIP_CLOCK_METHOD_CLOCK_GETTIME\n#      define PSNIP_CLOCK_CLOCK_GETTIME_CPU CLOCK_VIRTUAL\n#    endif\n#  endif\n#  if !defined(PSNIP_CLOCK_MONOTONIC_METHOD)\n#    if defined(CLOCK_MONOTONIC_RAW)\n#      define PSNIP_CLOCK_MONOTONIC_METHOD PSNIP_CLOCK_METHOD_CLOCK_GETTIME\n#      define PSNIP_CLOCK_CLOCK_GETTIME_MONOTONIC CLOCK_MONOTONIC\n#    elif defined(CLOCK_MONOTONIC_PRECISE)\n#      define PSNIP_CLOCK_MONOTONIC_METHOD PSNIP_CLOCK_METHOD_CLOCK_GETTIME\n#      define PSNIP_CLOCK_CLOCK_GETTIME_MONOTONIC CLOCK_MONOTONIC_PRECISE\n#    elif defined(_POSIX_MONOTONIC_CLOCK) || defined(CLOCK_MONOTONIC)\n#      define PSNIP_CLOCK_MONOTONIC_METHOD PSNIP_CLOCK_METHOD_CLOCK_GETTIME\n#      define PSNIP_CLOCK_CLOCK_GETTIME_MONOTONIC CLOCK_MONOTONIC\n#    endif\n#  endif\n#endif\n\n#if defined(_POSIX_VERSION) && (_POSIX_VERSION >= 200112L)\n#  if !defined(PSNIP_CLOCK_WALL_METHOD)\n#    define PSNIP_CLOCK_WALL_METHOD PSNIP_CLOCK_METHOD_GETTIMEOFDAY\n#  endif\n#endif\n\n#if !defined(PSNIP_CLOCK_WALL_METHOD)\n#  define PSNIP_CLOCK_WALL_METHOD PSNIP_CLOCK_METHOD_TIME\n#endif\n\n#if !defined(PSNIP_CLOCK_CPU_METHOD)\n#  define PSNIP_CLOCK_CPU_METHOD PSNIP_CLOCK_METHOD_CLOCK\n#endif\n\n/* Primarily here for testing. */\n#if !defined(PSNIP_CLOCK_MONOTONIC_METHOD) && defined(PSNIP_CLOCK_REQUIRE_MONOTONIC)\n#  error No monotonic clock found.\n#endif\n\n/* Implementations */\n\n#if \\\n  (defined(PSNIP_CLOCK_CPU_METHOD)       && (PSNIP_CLOCK_CPU_METHOD       == PSNIP_CLOCK_METHOD_CLOCK_GETTIME)) || \\\n  (defined(PSNIP_CLOCK_WALL_METHOD)      && (PSNIP_CLOCK_WALL_METHOD      == PSNIP_CLOCK_METHOD_CLOCK_GETTIME)) || \\\n  (defined(PSNIP_CLOCK_MONOTONIC_METHOD) && (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME)) || \\\n  (defined(PSNIP_CLOCK_CPU_METHOD)       && (PSNIP_CLOCK_CPU_METHOD       == PSNIP_CLOCK_METHOD_CLOCK)) || \\\n  (defined(PSNIP_CLOCK_WALL_METHOD)      && (PSNIP_CLOCK_WALL_METHOD      == PSNIP_CLOCK_METHOD_CLOCK)) || \\\n  (defined(PSNIP_CLOCK_MONOTONIC_METHOD) && (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_CLOCK)) || \\\n  (defined(PSNIP_CLOCK_CPU_METHOD)       && (PSNIP_CLOCK_CPU_METHOD       == PSNIP_CLOCK_METHOD_TIME)) || \\\n  (defined(PSNIP_CLOCK_WALL_METHOD)      && (PSNIP_CLOCK_WALL_METHOD      == PSNIP_CLOCK_METHOD_TIME)) || \\\n  (defined(PSNIP_CLOCK_MONOTONIC_METHOD) && (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_TIME))\n#  include <time.h>\n#endif\n\n#if \\\n  (defined(PSNIP_CLOCK_CPU_METHOD)       && (PSNIP_CLOCK_CPU_METHOD       == PSNIP_CLOCK_METHOD_GETTIMEOFDAY)) || \\\n  (defined(PSNIP_CLOCK_WALL_METHOD)      && (PSNIP_CLOCK_WALL_METHOD      == PSNIP_CLOCK_METHOD_GETTIMEOFDAY)) || \\\n  (defined(PSNIP_CLOCK_MONOTONIC_METHOD) && (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_GETTIMEOFDAY))\n#  include <sys/time.h>\n#endif\n\n#if \\\n  (defined(PSNIP_CLOCK_CPU_METHOD)       && (PSNIP_CLOCK_CPU_METHOD       == PSNIP_CLOCK_METHOD_GETPROCESSTIMES)) || \\\n  (defined(PSNIP_CLOCK_WALL_METHOD)      && (PSNIP_CLOCK_WALL_METHOD      == PSNIP_CLOCK_METHOD_GETPROCESSTIMES)) || \\\n  (defined(PSNIP_CLOCK_MONOTONIC_METHOD) && (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_GETPROCESSTIMES)) || \\\n  (defined(PSNIP_CLOCK_CPU_METHOD)       && (PSNIP_CLOCK_CPU_METHOD       == PSNIP_CLOCK_METHOD_GETTICKCOUNT64)) || \\\n  (defined(PSNIP_CLOCK_WALL_METHOD)      && (PSNIP_CLOCK_WALL_METHOD      == PSNIP_CLOCK_METHOD_GETTICKCOUNT64)) || \\\n  (defined(PSNIP_CLOCK_MONOTONIC_METHOD) && (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_GETTICKCOUNT64))\n#  include <windows.h>\n#endif\n\n#if \\\n  (defined(PSNIP_CLOCK_CPU_METHOD)       && (PSNIP_CLOCK_CPU_METHOD       == PSNIP_CLOCK_METHOD_GETRUSAGE)) || \\\n  (defined(PSNIP_CLOCK_WALL_METHOD)      && (PSNIP_CLOCK_WALL_METHOD      == PSNIP_CLOCK_METHOD_GETRUSAGE)) || \\\n  (defined(PSNIP_CLOCK_MONOTONIC_METHOD) && (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_GETRUSAGE))\n#  include <sys/time.h>\n#  include <sys/resource.h>\n#endif\n\n#if \\\n  (defined(PSNIP_CLOCK_CPU_METHOD)       && (PSNIP_CLOCK_CPU_METHOD       == PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME)) || \\\n  (defined(PSNIP_CLOCK_WALL_METHOD)      && (PSNIP_CLOCK_WALL_METHOD      == PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME)) || \\\n  (defined(PSNIP_CLOCK_MONOTONIC_METHOD) && (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME))\n#  include <CoreServices/CoreServices.h>\n#  include <mach/mach.h>\n#  include <mach/mach_time.h>\n#endif\n\n/*** Implementations ***/\n\n#define PSNIP_CLOCK_NSEC_PER_SEC ((psnip_uint32_t) (1000000000ULL))\n\n#if \\\n  (defined(PSNIP_CLOCK_CPU_METHOD)       && (PSNIP_CLOCK_CPU_METHOD       == PSNIP_CLOCK_METHOD_CLOCK_GETTIME)) || \\\n  (defined(PSNIP_CLOCK_WALL_METHOD)      && (PSNIP_CLOCK_WALL_METHOD      == PSNIP_CLOCK_METHOD_CLOCK_GETTIME)) || \\\n  (defined(PSNIP_CLOCK_MONOTONIC_METHOD) && (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME))\nPSNIP_CLOCK__FUNCTION psnip_uint32_t\npsnip_clock__clock_getres (clockid_t clk_id) {\n  struct timespec res;\n  int r;\n\n  r = clock_getres(clk_id, &res);\n  if (r != 0)\n    return 0;\n\n  return (psnip_uint32_t) (PSNIP_CLOCK_NSEC_PER_SEC / res.tv_nsec);\n}\n\nPSNIP_CLOCK__FUNCTION int\npsnip_clock__clock_gettime (clockid_t clk_id, struct PsnipClockTimespec* res) {\n  struct timespec ts;\n\n  if (clock_gettime(clk_id, &ts) != 0)\n    return -10;\n\n  res->seconds = (psnip_uint64_t) (ts.tv_sec);\n  res->nanoseconds = (psnip_uint64_t) (ts.tv_nsec);\n\n  return 0;\n}\n#endif\n\nPSNIP_CLOCK__FUNCTION psnip_uint32_t\npsnip_clock_wall_get_precision (void) {\n#if !defined(PSNIP_CLOCK_WALL_METHOD)\n  return 0;\n#elif defined(PSNIP_CLOCK_WALL_METHOD) && PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME\n  return psnip_clock__clock_getres(PSNIP_CLOCK_CLOCK_GETTIME_WALL);\n#elif defined(PSNIP_CLOCK_WALL_METHOD) && PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_GETTIMEOFDAY\n  return 1000000;\n#elif defined(PSNIP_CLOCK_WALL_METHOD) && PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_TIME\n  return 1;\n#else\n  return 0;\n#endif\n}\n\nPSNIP_CLOCK__FUNCTION int\npsnip_clock_wall_get_time (struct PsnipClockTimespec* res) {\n  (void) res;\n\n#if !defined(PSNIP_CLOCK_WALL_METHOD)\n  return -2;\n#elif defined(PSNIP_CLOCK_WALL_METHOD) && PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME\n  return psnip_clock__clock_gettime(PSNIP_CLOCK_CLOCK_GETTIME_WALL, res);\n#elif defined(PSNIP_CLOCK_WALL_METHOD) && PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_TIME\n  res->seconds = time(NULL);\n  res->nanoseconds = 0;\n#elif defined(PSNIP_CLOCK_WALL_METHOD) && PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_GETTIMEOFDAY\n  struct timeval tv;\n\n  if (gettimeofday(&tv, NULL) != 0)\n    return -6;\n\n  res->seconds = tv.tv_sec;\n  res->nanoseconds = tv.tv_usec * 1000;\n#else\n  return -2;\n#endif\n\n  return 0;\n}\n\nPSNIP_CLOCK__FUNCTION psnip_uint32_t\npsnip_clock_cpu_get_precision (void) {\n#if !defined(PSNIP_CLOCK_CPU_METHOD)\n  return 0;\n#elif defined(PSNIP_CLOCK_CPU_METHOD) && PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME\n  return psnip_clock__clock_getres(PSNIP_CLOCK_CLOCK_GETTIME_CPU);\n#elif defined(PSNIP_CLOCK_CPU_METHOD) && PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_CLOCK\n  return CLOCKS_PER_SEC;\n#elif defined(PSNIP_CLOCK_CPU_METHOD) && PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_GETPROCESSTIMES\n  return PSNIP_CLOCK_NSEC_PER_SEC / 100;\n#else\n  return 0;\n#endif\n}\n\nPSNIP_CLOCK__FUNCTION int\npsnip_clock_cpu_get_time (struct PsnipClockTimespec* res) {\n#if !defined(PSNIP_CLOCK_CPU_METHOD)\n  (void) res;\n  return -2;\n#elif defined(PSNIP_CLOCK_CPU_METHOD) && PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME\n  return psnip_clock__clock_gettime(PSNIP_CLOCK_CLOCK_GETTIME_CPU, res);\n#elif defined(PSNIP_CLOCK_CPU_METHOD) && PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_CLOCK\n  clock_t t = clock();\n  if (t == ((clock_t) -1))\n    return -5;\n  res->seconds = t / CLOCKS_PER_SEC;\n  res->nanoseconds = (t % CLOCKS_PER_SEC) * (PSNIP_CLOCK_NSEC_PER_SEC / CLOCKS_PER_SEC);\n#elif defined(PSNIP_CLOCK_CPU_METHOD) && PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_GETPROCESSTIMES\n  FILETIME CreationTime, ExitTime, KernelTime, UserTime;\n  LARGE_INTEGER date, adjust;\n\n  if (!GetProcessTimes(GetCurrentProcess(), &CreationTime, &ExitTime, &KernelTime, &UserTime))\n    return -7;\n\n  /* http://www.frenk.com/2009/12/convert-filetime-to-unix-timestamp/ */\n  date.HighPart = UserTime.dwHighDateTime;\n  date.LowPart = UserTime.dwLowDateTime;\n  adjust.QuadPart = 11644473600000 * 10000;\n  date.QuadPart -= adjust.QuadPart;\n\n  res->seconds = date.QuadPart / 10000000;\n  res->nanoseconds = (date.QuadPart % 10000000) * (PSNIP_CLOCK_NSEC_PER_SEC / 100);\n#elif PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_GETRUSAGE\n  struct rusage usage;\n  if (getrusage(RUSAGE_SELF, &usage) != 0)\n    return -8;\n\n  res->seconds = usage.ru_utime.tv_sec;\n  res->nanoseconds = tv.tv_usec * 1000;\n#else\n  (void) res;\n  return -2;\n#endif\n\n  return 0;\n}\n\nPSNIP_CLOCK__FUNCTION psnip_uint32_t\npsnip_clock_monotonic_get_precision (void) {\n#if !defined(PSNIP_CLOCK_MONOTONIC_METHOD)\n  return 0;\n#elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME\n  return psnip_clock__clock_getres(PSNIP_CLOCK_CLOCK_GETTIME_MONOTONIC);\n#elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME\n  static mach_timebase_info_data_t tbi = { 0, };\n  if (tbi.denom == 0)\n    mach_timebase_info(&tbi);\n  return (psnip_uint32_t) (tbi.numer / tbi.denom);\n#elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_GETTICKCOUNT64\n  return 1000;\n#elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_QUERYPERFORMANCECOUNTER\n  LARGE_INTEGER Frequency;\n  QueryPerformanceFrequency(&Frequency);\n  return (psnip_uint32_t) ((Frequency.QuadPart > PSNIP_CLOCK_NSEC_PER_SEC) ? PSNIP_CLOCK_NSEC_PER_SEC : Frequency.QuadPart);\n#else\n  return 0;\n#endif\n}\n\nPSNIP_CLOCK__FUNCTION int\npsnip_clock_monotonic_get_time (struct PsnipClockTimespec* res) {\n#if !defined(PSNIP_CLOCK_MONOTONIC_METHOD)\n  (void) res;\n  return -2;\n#elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME\n  return psnip_clock__clock_gettime(PSNIP_CLOCK_CLOCK_GETTIME_MONOTONIC, res);\n#elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME\n  psnip_uint64_t nsec = mach_absolute_time();\n  static mach_timebase_info_data_t tbi = { 0, };\n  if (tbi.denom == 0)\n    mach_timebase_info(&tbi);\n  nsec *= ((psnip_uint64_t) tbi.numer) / ((psnip_uint64_t) tbi.denom);\n  res->seconds = nsec / PSNIP_CLOCK_NSEC_PER_SEC;\n  res->nanoseconds = nsec % PSNIP_CLOCK_NSEC_PER_SEC;\n#elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_QUERYPERFORMANCECOUNTER\n  LARGE_INTEGER t, f;\n  if (QueryPerformanceCounter(&t) == 0)\n    return -12;\n\n  QueryPerformanceFrequency(&f);\n  res->seconds = t.QuadPart / f.QuadPart;\n  res->nanoseconds = t.QuadPart % f.QuadPart;\n  if (f.QuadPart > PSNIP_CLOCK_NSEC_PER_SEC)\n    res->nanoseconds /= f.QuadPart / PSNIP_CLOCK_NSEC_PER_SEC;\n  else\n    res->nanoseconds *= PSNIP_CLOCK_NSEC_PER_SEC / f.QuadPart;\n#elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_GETTICKCOUNT64\n  const ULONGLONG msec = GetTickCount64();\n  res->seconds = msec / 1000;\n  res->nanoseconds = sec % 1000;\n#else\n  return -2;\n#endif\n\n  return 0;\n}\n\n/* Returns the number of ticks per second for the specified clock.\n * For example, a clock with millisecond precision would return 1000,\n * and a clock with 1 second (such as the time() function) would\n * return 1.\n *\n * If the requested clock isn't available, it will return 0.\n * Hopefully this will be rare, but if it happens to you please let us\n * know so we can work on finding a way to support your system.\n *\n * Note that different clocks on the same system often have a\n * different precisions.\n */\nPSNIP_CLOCK__FUNCTION psnip_uint32_t\npsnip_clock_get_precision (enum PsnipClockType clock_type) {\n  switch (clock_type) {\n    case PSNIP_CLOCK_TYPE_MONOTONIC:\n      return psnip_clock_monotonic_get_precision ();\n    case PSNIP_CLOCK_TYPE_CPU:\n      return psnip_clock_cpu_get_precision ();\n    case PSNIP_CLOCK_TYPE_WALL:\n      return psnip_clock_wall_get_precision ();\n  }\n\n  PSNIP_CLOCK_UNREACHABLE();\n  return 0;\n}\n\n/* Set the provided timespec to the requested time.  Returns 0 on\n * success, or a negative value on failure. */\nPSNIP_CLOCK__FUNCTION int\npsnip_clock_get_time (enum PsnipClockType clock_type, struct PsnipClockTimespec* res) {\n  assert(res != NULL);\n\n  switch (clock_type) {\n    case PSNIP_CLOCK_TYPE_MONOTONIC:\n      return psnip_clock_monotonic_get_time (res);\n    case PSNIP_CLOCK_TYPE_CPU:\n      return psnip_clock_cpu_get_time (res);\n    case PSNIP_CLOCK_TYPE_WALL:\n      return psnip_clock_wall_get_time (res);\n  }\n\n  return -1;\n}\n\n#endif /* !defined(PSNIP_CLOCK_H) */\n\nstatic psnip_uint64_t\nmunit_clock_get_elapsed(struct PsnipClockTimespec* start, struct PsnipClockTimespec* end) {\n  psnip_uint64_t r = (end->seconds - start->seconds) * PSNIP_CLOCK_NSEC_PER_SEC;\n  if (end->nanoseconds < start->nanoseconds) {\n    r -= (start->nanoseconds - end->nanoseconds);\n  } else {\n    r += (end->nanoseconds - start->nanoseconds);\n  }\n  return r;\n}\n\n#else\n#  include <time.h>\n#endif /* defined(MUNIT_ENABLE_TIMING) */\n\n/*** PRNG stuff ***/\n\n/* This is (unless I screwed up, which is entirely possible) the\n * version of PCG with 32-bit state.  It was chosen because it has a\n * small enough state that we should reliably be able to use CAS\n * instead of requiring a lock for thread-safety.\n *\n * If I did screw up, I probably will not bother changing it unless\n * there is a significant bias.  It's really not important this be\n * particularly strong, as long as it is fairly random it's much more\n * important that it be reproducible, so bug reports have a better\n * chance of being reproducible. */\n\n#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) && !defined(__STDC_NO_ATOMICS__) && !defined(__EMSCRIPTEN__) && (!defined(__GNUC_MINOR__) || (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ > 8))\n#  define HAVE_STDATOMIC\n#elif defined(__clang__)\n#  if __has_extension(c_atomic)\n#    define HAVE_CLANG_ATOMICS\n#  endif\n#endif\n\n/* Workaround for http://llvm.org/bugs/show_bug.cgi?id=26911 */\n#if defined(__clang__) && defined(_WIN32)\n#  undef HAVE_STDATOMIC\n#  if defined(__c2__)\n#    undef HAVE_CLANG_ATOMICS\n#  endif\n#endif\n\n#if defined(_OPENMP)\n#  define ATOMIC_UINT32_T uint32_t\n#  define ATOMIC_UINT32_INIT(x) (x)\n#elif defined(HAVE_STDATOMIC)\n#  include <stdatomic.h>\n#  define ATOMIC_UINT32_T _Atomic uint32_t\n#  define ATOMIC_UINT32_INIT(x) ATOMIC_VAR_INIT(x)\n#elif defined(HAVE_CLANG_ATOMICS)\n#  define ATOMIC_UINT32_T _Atomic uint32_t\n#  define ATOMIC_UINT32_INIT(x) (x)\n#elif defined(_WIN32)\n#  define ATOMIC_UINT32_T volatile LONG\n#  define ATOMIC_UINT32_INIT(x) (x)\n#else\n#  define ATOMIC_UINT32_T volatile uint32_t\n#  define ATOMIC_UINT32_INIT(x) (x)\n#endif\n\nstatic ATOMIC_UINT32_T munit_rand_state = ATOMIC_UINT32_INIT(42);\n\n#if defined(_OPENMP)\nstatic inline void\nmunit_atomic_store(ATOMIC_UINT32_T* dest, ATOMIC_UINT32_T value) {\n#pragma omp critical (munit_atomics)\n  *dest = value;\n}\n\nstatic inline uint32_t\nmunit_atomic_load(ATOMIC_UINT32_T* src) {\n  int ret;\n#pragma omp critical (munit_atomics)\n  ret = *src;\n  return ret;\n}\n\nstatic inline uint32_t\nmunit_atomic_cas(ATOMIC_UINT32_T* dest, ATOMIC_UINT32_T* expected, ATOMIC_UINT32_T desired) {\n  munit_bool ret;\n\n#pragma omp critical (munit_atomics)\n  {\n    if (*dest == *expected) {\n      *dest = desired;\n      ret = 1;\n    } else {\n      ret = 0;\n    }\n  }\n\n  return ret;\n}\n#elif defined(HAVE_STDATOMIC)\n#  define munit_atomic_store(dest, value)         atomic_store(dest, value)\n#  define munit_atomic_load(src)                  atomic_load(src)\n#  define munit_atomic_cas(dest, expected, value) atomic_compare_exchange_weak(dest, expected, value)\n#elif defined(HAVE_CLANG_ATOMICS)\n#  define munit_atomic_store(dest, value)         __c11_atomic_store(dest, value, __ATOMIC_SEQ_CST)\n#  define munit_atomic_load(src)                  __c11_atomic_load(src, __ATOMIC_SEQ_CST)\n#  define munit_atomic_cas(dest, expected, value) __c11_atomic_compare_exchange_weak(dest, expected, value, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST)\n#elif defined(__GNUC__) && (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 7)\n#  define munit_atomic_store(dest, value)         __atomic_store_n(dest, value, __ATOMIC_SEQ_CST)\n#  define munit_atomic_load(src)                  __atomic_load_n(src, __ATOMIC_SEQ_CST)\n#  define munit_atomic_cas(dest, expected, value) __atomic_compare_exchange_n(dest, expected, value, 1, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST)\n#elif defined(__GNUC__) && (__GNUC__ >= 4)\n#  define munit_atomic_store(dest,value)          do { *(dest) = (value); } while (0)\n#  define munit_atomic_load(src)                  (*(src))\n#  define munit_atomic_cas(dest, expected, value) __sync_bool_compare_and_swap(dest, *expected, value)\n#elif defined(_WIN32) /* Untested */\n#  define munit_atomic_store(dest,value)          do { *(dest) = (value); } while (0)\n#  define munit_atomic_load(src)                  (*(src))\n#  define munit_atomic_cas(dest, expected, value) InterlockedCompareExchange((dest), (value), *(expected))\n#else\n#  warning No atomic implementation, PRNG will not be thread-safe\n#  define munit_atomic_store(dest, value)         do { *(dest) = (value); } while (0)\n#  define munit_atomic_load(src)                  (*(src))\nstatic inline munit_bool\nmunit_atomic_cas(ATOMIC_UINT32_T* dest, ATOMIC_UINT32_T* expected, ATOMIC_UINT32_T desired) {\n  if (*dest == *expected) {\n    *dest = desired;\n    return 1;\n  } else {\n    return 0;\n  }\n}\n#endif\n\n#define MUNIT_PRNG_MULTIPLIER (747796405U)\n#define MUNIT_PRNG_INCREMENT  (1729U)\n\nstatic munit_uint32_t\nmunit_rand_next_state(munit_uint32_t state) {\n  return state * MUNIT_PRNG_MULTIPLIER + MUNIT_PRNG_INCREMENT;\n}\n\nstatic munit_uint32_t\nmunit_rand_from_state(munit_uint32_t state) {\n  munit_uint32_t res = ((state >> ((state >> 28) + 4)) ^ state) * (277803737U);\n  res ^= res >> 22;\n  return res;\n}\n\nvoid\nmunit_rand_seed(munit_uint32_t seed) {\n  munit_uint32_t state = munit_rand_next_state(seed + MUNIT_PRNG_INCREMENT);\n  munit_atomic_store(&munit_rand_state, state);\n}\n\nstatic munit_uint32_t\nmunit_rand_generate_seed(void) {\n  munit_uint32_t seed, state;\n#if defined(MUNIT_ENABLE_TIMING)\n  struct PsnipClockTimespec wc = { 0, };\n\n  psnip_clock_get_time(PSNIP_CLOCK_TYPE_WALL, &wc);\n  seed = (munit_uint32_t) wc.nanoseconds;\n#else\n  seed = (munit_uint32_t) time(NULL);\n#endif\n\n  state = munit_rand_next_state(seed + MUNIT_PRNG_INCREMENT);\n  return munit_rand_from_state(state);\n}\n\nstatic munit_uint32_t\nmunit_rand_state_uint32(munit_uint32_t* state) {\n  const munit_uint32_t old = *state;\n  *state = munit_rand_next_state(old);\n  return munit_rand_from_state(old);\n}\n\nmunit_uint32_t\nmunit_rand_uint32(void) {\n  munit_uint32_t old, state;\n\n  do {\n    old = munit_atomic_load(&munit_rand_state);\n    state = munit_rand_next_state(old);\n  } while (!munit_atomic_cas(&munit_rand_state, &old, state));\n\n  return munit_rand_from_state(old);\n}\n\nstatic void\nmunit_rand_state_memory(munit_uint32_t* state, size_t size, munit_uint8_t data[MUNIT_ARRAY_PARAM(size)]) {\n  size_t members_remaining = size / sizeof(munit_uint32_t);\n  size_t bytes_remaining = size % sizeof(munit_uint32_t);\n  munit_uint8_t* b = data;\n  munit_uint32_t rv;\n  while (members_remaining-- > 0) {\n    rv = munit_rand_state_uint32(state);\n    memcpy(b, &rv, sizeof(munit_uint32_t));\n    b += sizeof(munit_uint32_t);\n  }\n  if (bytes_remaining != 0) {\n    rv = munit_rand_state_uint32(state);\n    memcpy(b, &rv, bytes_remaining);\n  }\n}\n\nvoid\nmunit_rand_memory(size_t size, munit_uint8_t data[MUNIT_ARRAY_PARAM(size)]) {\n  munit_uint32_t old, state;\n\n  do {\n    state = old = munit_atomic_load(&munit_rand_state);\n    munit_rand_state_memory(&state, size, data);\n  } while (!munit_atomic_cas(&munit_rand_state, &old, state));\n}\n\nstatic munit_uint32_t\nmunit_rand_state_at_most(munit_uint32_t* state, munit_uint32_t salt, munit_uint32_t max) {\n  /* We want (UINT32_MAX + 1) % max, which in unsigned arithmetic is the same\n   * as (UINT32_MAX + 1 - max) % max = -max % max. We compute -max using not\n   * to avoid compiler warnings.\n   */\n  const munit_uint32_t min = (~max + 1U) % max;\n  munit_uint32_t x;\n\n  if (max == (~((munit_uint32_t) 0U)))\n    return munit_rand_state_uint32(state) ^ salt;\n\n  max++;\n\n  do {\n    x = munit_rand_state_uint32(state) ^ salt;\n  } while (x < min);\n\n  return x % max;\n}\n\nstatic munit_uint32_t\nmunit_rand_at_most(munit_uint32_t salt, munit_uint32_t max) {\n  munit_uint32_t old, state;\n  munit_uint32_t retval;\n\n  do {\n    state = old = munit_atomic_load(&munit_rand_state);\n    retval = munit_rand_state_at_most(&state, salt, max);\n  } while (!munit_atomic_cas(&munit_rand_state, &old, state));\n\n  return retval;\n}\n\nint\nmunit_rand_int_range(int min, int max) {\n  munit_uint64_t range = (munit_uint64_t) max - (munit_uint64_t) min;\n\n  if (min > max)\n    return munit_rand_int_range(max, min);\n\n  if (range > (~((munit_uint32_t) 0U)))\n    range = (~((munit_uint32_t) 0U));\n\n  return min + munit_rand_at_most(0, (munit_uint32_t) range);\n}\n\ndouble\nmunit_rand_double(void) {\n  munit_uint32_t old, state;\n  double retval = 0.0;\n\n  do {\n    state = old = munit_atomic_load(&munit_rand_state);\n\n    /* See http://mumble.net/~campbell/tmp/random_real.c for how to do\n     * this right.  Patches welcome if you feel that this is too\n     * biased. */\n    retval = munit_rand_state_uint32(&state) / ((~((munit_uint32_t) 0U)) + 1.0);\n  } while (!munit_atomic_cas(&munit_rand_state, &old, state));\n\n  return retval;\n}\n\n/*** Test suite handling ***/\n\ntypedef struct {\n  unsigned int successful;\n  unsigned int skipped;\n  unsigned int failed;\n  unsigned int errored;\n#if defined(MUNIT_ENABLE_TIMING)\n  munit_uint64_t cpu_clock;\n  munit_uint64_t wall_clock;\n#endif\n} MunitReport;\n\ntypedef struct {\n  const char* prefix;\n  const MunitSuite* suite;\n  const char** tests;\n  munit_uint32_t seed;\n  unsigned int iterations;\n  MunitParameter* parameters;\n  munit_bool single_parameter_mode;\n  void* user_data;\n  MunitReport report;\n  munit_bool colorize;\n  munit_bool fork;\n  munit_bool show_stderr;\n  munit_bool fatal_failures;\n} MunitTestRunner;\n\nconst char*\nmunit_parameters_get(const MunitParameter params[], const char* key) {\n  const MunitParameter* param;\n\n  for (param = params ; param != NULL && param->name != NULL ; param++)\n    if (strcmp(param->name, key) == 0)\n      return param->value;\n  return NULL;\n}\n\n#if defined(MUNIT_ENABLE_TIMING)\nstatic void\nmunit_print_time(FILE* fp, munit_uint64_t nanoseconds) {\n  fprintf(fp, \"%\" MUNIT_TEST_TIME_FORMAT, ((double) nanoseconds) / ((double) PSNIP_CLOCK_NSEC_PER_SEC));\n}\n#endif\n\n/* Add a parameter to an array of parameters. */\nstatic MunitResult\nmunit_parameters_add(size_t* params_size, MunitParameter* params[MUNIT_ARRAY_PARAM(*params_size)], char* name, char* value) {\n  *params = realloc(*params, sizeof(MunitParameter) * (*params_size + 2));\n  if (*params == NULL)\n    return MUNIT_ERROR;\n\n  (*params)[*params_size].name = name;\n  (*params)[*params_size].value = value;\n  (*params_size)++;\n  (*params)[*params_size].name = NULL;\n  (*params)[*params_size].value = NULL;\n\n  return MUNIT_OK;\n}\n\n/* Concatenate two strings, but just return one of the components\n * unaltered if the other is NULL or \"\". */\nstatic char*\nmunit_maybe_concat(size_t* len, char* prefix, char* suffix) {\n  char* res;\n  size_t res_l;\n  const size_t prefix_l = prefix != NULL ? strlen(prefix) : 0;\n  const size_t suffix_l = suffix != NULL ? strlen(suffix) : 0;\n  if (prefix_l == 0 && suffix_l == 0) {\n    res = NULL;\n    res_l = 0;\n  } else if (prefix_l == 0 && suffix_l != 0) {\n    res = suffix;\n    res_l = suffix_l;\n  } else if (prefix_l != 0 && suffix_l == 0) {\n    res = prefix;\n    res_l = prefix_l;\n  } else {\n    res_l = prefix_l + suffix_l;\n    res = malloc(res_l + 1);\n    memcpy(res, prefix, prefix_l);\n    memcpy(res + prefix_l, suffix, suffix_l);\n    res[res_l] = 0;\n  }\n\n  if (len != NULL)\n    *len = res_l;\n\n  return res;\n}\n\n/* Possibly free a string returned by munit_maybe_concat. */\nstatic void\nmunit_maybe_free_concat(char* s, const char* prefix, const char* suffix) {\n  if (prefix != s && suffix != s)\n    free(s);\n}\n\n/* Cheap string hash function, just used to salt the PRNG. */\nstatic munit_uint32_t\nmunit_str_hash(const char* name) {\n  const char *p;\n  munit_uint32_t h = 5381U;\n\n  for (p = name; *p != '\\0'; p++)\n    h = (h << 5) + h + *p;\n\n  return h;\n}\n\nstatic void\nmunit_splice(int from, int to) {\n  munit_uint8_t buf[1024];\n#if !defined(_WIN32)\n  ssize_t len;\n  ssize_t bytes_written;\n  ssize_t write_res;\n#else\n  int len;\n  int bytes_written;\n  int write_res;\n#endif\n  do {\n    len = read(from, buf, sizeof(buf));\n    if (len > 0) {\n      bytes_written = 0;\n      do {\n        write_res = write(to, buf + bytes_written, len - bytes_written);\n        if (write_res < 0)\n          break;\n        bytes_written += write_res;\n      } while (bytes_written < len);\n    }\n    else\n      break;\n  } while (1);\n}\n\n/* This is the part that should be handled in the child process */\nstatic MunitResult\nmunit_test_runner_exec(MunitTestRunner* runner, const MunitTest* test, const MunitParameter params[], MunitReport* report) {\n  unsigned int iterations = runner->iterations;\n  MunitResult result = MUNIT_FAIL;\n#if defined(MUNIT_ENABLE_TIMING)\n  struct PsnipClockTimespec wall_clock_begin = { 0, }, wall_clock_end = { 0, };\n  struct PsnipClockTimespec cpu_clock_begin = { 0, }, cpu_clock_end = { 0, };\n#endif\n  unsigned int i = 0;\n\n  if ((test->options & MUNIT_TEST_OPTION_SINGLE_ITERATION) == MUNIT_TEST_OPTION_SINGLE_ITERATION)\n    iterations = 1;\n  else if (iterations == 0)\n    iterations = runner->suite->iterations;\n\n  munit_rand_seed(runner->seed);\n\n  do {\n    void* data = (test->setup == NULL) ? runner->user_data : test->setup(params, runner->user_data);\n\n#if defined(MUNIT_ENABLE_TIMING)\n    psnip_clock_get_time(PSNIP_CLOCK_TYPE_WALL, &wall_clock_begin);\n    psnip_clock_get_time(PSNIP_CLOCK_TYPE_CPU, &cpu_clock_begin);\n#endif\n\n    result = test->test(params, data);\n\n#if defined(MUNIT_ENABLE_TIMING)\n    psnip_clock_get_time(PSNIP_CLOCK_TYPE_WALL, &wall_clock_end);\n    psnip_clock_get_time(PSNIP_CLOCK_TYPE_CPU, &cpu_clock_end);\n#endif\n\n    if (test->tear_down != NULL)\n      test->tear_down(data);\n\n    if (MUNIT_LIKELY(result == MUNIT_OK)) {\n      report->successful++;\n#if defined(MUNIT_ENABLE_TIMING)\n      report->wall_clock += munit_clock_get_elapsed(&wall_clock_begin, &wall_clock_end);\n      report->cpu_clock += munit_clock_get_elapsed(&cpu_clock_begin, &cpu_clock_end);\n#endif\n    } else {\n      switch ((int) result) {\n        case MUNIT_SKIP:\n          report->skipped++;\n          break;\n        case MUNIT_FAIL:\n          report->failed++;\n          break;\n        case MUNIT_ERROR:\n          report->errored++;\n          break;\n        default:\n          break;\n      }\n      break;\n    }\n  } while (++i < iterations);\n\n  return result;\n}\n\n#if defined(MUNIT_EMOTICON)\n#  define MUNIT_RESULT_STRING_OK    \":)\"\n#  define MUNIT_RESULT_STRING_SKIP  \":|\"\n#  define MUNIT_RESULT_STRING_FAIL  \":(\"\n#  define MUNIT_RESULT_STRING_ERROR \":o\"\n#  define MUNIT_RESULT_STRING_TODO  \":/\"\n#else\n#  define MUNIT_RESULT_STRING_OK    \"OK   \"\n#  define MUNIT_RESULT_STRING_SKIP  \"SKIP \"\n#  define MUNIT_RESULT_STRING_FAIL  \"FAIL \"\n#  define MUNIT_RESULT_STRING_ERROR \"ERROR\"\n#  define MUNIT_RESULT_STRING_TODO  \"TODO \"\n#endif\n\nstatic void\nmunit_test_runner_print_color(const MunitTestRunner* runner, const char* string, char color) {\n  if (runner->colorize)\n    fprintf(MUNIT_OUTPUT_FILE, \"\\x1b[3%cm%s\\x1b[39m\", color, string);\n  else\n    fputs(string, MUNIT_OUTPUT_FILE);\n}\n\n#if !defined(MUNIT_NO_BUFFER)\nstatic int\nmunit_replace_stderr(FILE* stderr_buf) {\n  if (stderr_buf != NULL) {\n    const int orig_stderr = dup(STDERR_FILENO);\n\n    int errfd = fileno(stderr_buf);\n    if (MUNIT_UNLIKELY(errfd == -1)) {\n      exit(EXIT_FAILURE);\n    }\n\n    dup2(errfd, STDERR_FILENO);\n\n    return orig_stderr;\n  }\n\n  return -1;\n}\n\nstatic void\nmunit_restore_stderr(int orig_stderr) {\n  if (orig_stderr != -1) {\n    dup2(orig_stderr, STDERR_FILENO);\n    close(orig_stderr);\n  }\n}\n#endif /* !defined(MUNIT_NO_BUFFER) */\n\n/* Run a test with the specified parameters. */\nstatic void\nmunit_test_runner_run_test_with_params(MunitTestRunner* runner, const MunitTest* test, const MunitParameter params[]) {\n  MunitResult result = MUNIT_OK;\n  MunitReport report = {\n    0, 0, 0, 0,\n#if defined(MUNIT_ENABLE_TIMING)\n    0, 0\n#endif\n  };\n  unsigned int output_l;\n  munit_bool first;\n  const MunitParameter* param;\n  FILE* stderr_buf;\n#if !defined(MUNIT_NO_FORK)\n  int pipefd[2];\n  pid_t fork_pid;\n  int orig_stderr;\n  ssize_t bytes_written = 0;\n  ssize_t write_res;\n  ssize_t bytes_read = 0;\n  ssize_t read_res;\n  int status = 0;\n  pid_t changed_pid;\n#endif\n\n  if (params != NULL) {\n    output_l = 2;\n    fputs(\"  \", MUNIT_OUTPUT_FILE);\n    first = 1;\n    for (param = params ; param != NULL && param->name != NULL ; param++) {\n      if (!first) {\n        fputs(\", \", MUNIT_OUTPUT_FILE);\n        output_l += 2;\n      } else {\n        first = 0;\n      }\n\n      output_l += fprintf(MUNIT_OUTPUT_FILE, \"%s=%s\", param->name, param->value);\n    }\n    while (output_l++ < MUNIT_TEST_NAME_LEN) {\n      fputc(' ', MUNIT_OUTPUT_FILE);\n    }\n  }\n\n  fflush(MUNIT_OUTPUT_FILE);\n\n  stderr_buf = NULL;\n#if !defined(_WIN32) || defined(__MINGW32__)\n  stderr_buf = tmpfile();\n#else\n  tmpfile_s(&stderr_buf);\n#endif\n  if (stderr_buf == NULL) {\n    munit_log_errno(MUNIT_LOG_ERROR, stderr, \"unable to create buffer for stderr\");\n    result = MUNIT_ERROR;\n    goto print_result;\n  }\n\n#if !defined(MUNIT_NO_FORK)\n  if (runner->fork) {\n    pipefd[0] = -1;\n    pipefd[1] = -1;\n    if (pipe(pipefd) != 0) {\n      munit_log_errno(MUNIT_LOG_ERROR, stderr, \"unable to create pipe\");\n      result = MUNIT_ERROR;\n      goto print_result;\n    }\n\n    fork_pid = fork();\n    if (fork_pid == 0) {\n      close(pipefd[0]);\n\n      orig_stderr = munit_replace_stderr(stderr_buf);\n      munit_test_runner_exec(runner, test, params, &report);\n\n      /* Note that we don't restore stderr.  This is so we can buffer\n       * things written to stderr later on (such as by\n       * asan/tsan/ubsan, valgrind, etc.) */\n      close(orig_stderr);\n\n      do {\n        write_res = write(pipefd[1], ((munit_uint8_t*) (&report)) + bytes_written, sizeof(report) - bytes_written);\n        if (write_res < 0) {\n          if (stderr_buf != NULL) {\n            munit_log_errno(MUNIT_LOG_ERROR, stderr, \"unable to write to pipe\");\n          }\n          exit(EXIT_FAILURE);\n        }\n        bytes_written += write_res;\n      } while ((size_t) bytes_written < sizeof(report));\n\n      if (stderr_buf != NULL)\n        fclose(stderr_buf);\n      close(pipefd[1]);\n\n      exit(EXIT_SUCCESS);\n    } else if (fork_pid == -1) {\n      close(pipefd[0]);\n      close(pipefd[1]);\n      if (stderr_buf != NULL) {\n        munit_log_errno(MUNIT_LOG_ERROR, stderr, \"unable to fork\");\n      }\n      report.errored++;\n      result = MUNIT_ERROR;\n    } else {\n      close(pipefd[1]);\n      do {\n        read_res = read(pipefd[0], ((munit_uint8_t*) (&report)) + bytes_read, sizeof(report) - bytes_read);\n        if (read_res < 1)\n          break;\n        bytes_read += read_res;\n      } while (bytes_read < (ssize_t) sizeof(report));\n\n      changed_pid = waitpid(fork_pid, &status, 0);\n\n      if (MUNIT_LIKELY(changed_pid == fork_pid) && MUNIT_LIKELY(WIFEXITED(status))) {\n        if (bytes_read != sizeof(report)) {\n          munit_logf_internal(MUNIT_LOG_ERROR, stderr_buf, \"child exited unexpectedly with status %d\", WEXITSTATUS(status));\n          report.errored++;\n        } else if (WEXITSTATUS(status) != EXIT_SUCCESS) {\n          munit_logf_internal(MUNIT_LOG_ERROR, stderr_buf, \"child exited with status %d\", WEXITSTATUS(status));\n          report.errored++;\n        }\n      } else {\n        if (WIFSIGNALED(status)) {\n#if defined(_XOPEN_VERSION) && (_XOPEN_VERSION >= 700)\n          munit_logf_internal(MUNIT_LOG_ERROR, stderr_buf, \"child killed by signal %d (%s)\", WTERMSIG(status), strsignal(WTERMSIG(status)));\n#else\n          munit_logf_internal(MUNIT_LOG_ERROR, stderr_buf, \"child killed by signal %d\", WTERMSIG(status));\n#endif\n        } else if (WIFSTOPPED(status)) {\n          munit_logf_internal(MUNIT_LOG_ERROR, stderr_buf, \"child stopped by signal %d\", WSTOPSIG(status));\n        }\n        report.errored++;\n      }\n\n      close(pipefd[0]);\n      waitpid(fork_pid, NULL, 0);\n    }\n  } else\n#endif\n  {\n#if !defined(MUNIT_NO_BUFFER)\n    const volatile int orig_stderr = munit_replace_stderr(stderr_buf);\n#endif\n\n#if defined(MUNIT_THREAD_LOCAL)\n    if (MUNIT_UNLIKELY(setjmp(munit_error_jmp_buf) != 0)) {\n      result = MUNIT_FAIL;\n      report.failed++;\n    } else {\n      munit_error_jmp_buf_valid = 1;\n      result = munit_test_runner_exec(runner, test, params, &report);\n    }\n#else\n    result = munit_test_runner_exec(runner, test, params, &report);\n#endif\n\n#if !defined(MUNIT_NO_BUFFER)\n    munit_restore_stderr(orig_stderr);\n#endif\n\n    /* Here just so that the label is used on Windows and we don't get\n     * a warning */\n    goto print_result;\n  }\n\n print_result:\n\n  fputs(\"[ \", MUNIT_OUTPUT_FILE);\n  if ((test->options & MUNIT_TEST_OPTION_TODO) == MUNIT_TEST_OPTION_TODO) {\n    if (report.failed != 0 || report.errored != 0 || report.skipped != 0) {\n      munit_test_runner_print_color(runner, MUNIT_RESULT_STRING_TODO, '3');\n      result = MUNIT_OK;\n    } else {\n      munit_test_runner_print_color(runner, MUNIT_RESULT_STRING_ERROR, '1');\n      if (MUNIT_LIKELY(stderr_buf != NULL))\n        munit_log_internal(MUNIT_LOG_ERROR, stderr_buf, \"Test marked TODO, but was successful.\");\n      runner->report.failed++;\n      result = MUNIT_ERROR;\n    }\n  } else if (report.failed > 0) {\n    munit_test_runner_print_color(runner, MUNIT_RESULT_STRING_FAIL, '1');\n    runner->report.failed++;\n    result = MUNIT_FAIL;\n  } else if (report.errored > 0) {\n    munit_test_runner_print_color(runner, MUNIT_RESULT_STRING_ERROR, '1');\n    runner->report.errored++;\n    result = MUNIT_ERROR;\n  } else if (report.skipped > 0) {\n    munit_test_runner_print_color(runner, MUNIT_RESULT_STRING_SKIP, '3');\n    runner->report.skipped++;\n    result = MUNIT_SKIP;\n  } else if (report.successful > 1) {\n    munit_test_runner_print_color(runner, MUNIT_RESULT_STRING_OK, '2');\n#if defined(MUNIT_ENABLE_TIMING)\n    fputs(\" ] [ \", MUNIT_OUTPUT_FILE);\n    munit_print_time(MUNIT_OUTPUT_FILE, report.wall_clock / report.successful);\n    fputs(\" / \", MUNIT_OUTPUT_FILE);\n    munit_print_time(MUNIT_OUTPUT_FILE, report.cpu_clock / report.successful);\n    fprintf(MUNIT_OUTPUT_FILE, \" CPU ]\\n  %-\" MUNIT_XSTRINGIFY(MUNIT_TEST_NAME_LEN) \"s Total: [ \", \"\");\n    munit_print_time(MUNIT_OUTPUT_FILE, report.wall_clock);\n    fputs(\" / \", MUNIT_OUTPUT_FILE);\n    munit_print_time(MUNIT_OUTPUT_FILE, report.cpu_clock);\n    fputs(\" CPU\", MUNIT_OUTPUT_FILE);\n#endif\n    runner->report.successful++;\n    result = MUNIT_OK;\n  } else if (report.successful > 0) {\n    munit_test_runner_print_color(runner, MUNIT_RESULT_STRING_OK, '2');\n#if defined(MUNIT_ENABLE_TIMING)\n    fputs(\" ] [ \", MUNIT_OUTPUT_FILE);\n    munit_print_time(MUNIT_OUTPUT_FILE, report.wall_clock);\n    fputs(\" / \", MUNIT_OUTPUT_FILE);\n    munit_print_time(MUNIT_OUTPUT_FILE, report.cpu_clock);\n    fputs(\" CPU\", MUNIT_OUTPUT_FILE);\n#endif\n    runner->report.successful++;\n    result = MUNIT_OK;\n  }\n  fputs(\" ]\\n\", MUNIT_OUTPUT_FILE);\n\n  if (stderr_buf != NULL) {\n    if (result == MUNIT_FAIL || result == MUNIT_ERROR || runner->show_stderr) {\n      fflush(MUNIT_OUTPUT_FILE);\n\n      rewind(stderr_buf);\n      munit_splice(fileno(stderr_buf), STDERR_FILENO);\n\n      fflush(stderr);\n    }\n\n    fclose(stderr_buf);\n  }\n}\n\nstatic void\nmunit_test_runner_run_test_wild(MunitTestRunner* runner,\n                                const MunitTest* test,\n                                const char* test_name,\n                                MunitParameter* params,\n                                MunitParameter* p) {\n  const MunitParameterEnum* pe;\n  char** values;\n  MunitParameter* next;\n\n  for (pe = test->parameters ; pe != NULL && pe->name != NULL ; pe++) {\n    if (p->name == pe->name)\n      break;\n  }\n\n  if (pe == NULL)\n    return;\n\n  for (values = pe->values ; *values != NULL ; values++) {\n    next = p + 1;\n    p->value = *values;\n    if (next->name == NULL) {\n      munit_test_runner_run_test_with_params(runner, test, params);\n    } else {\n      munit_test_runner_run_test_wild(runner, test, test_name, params, next);\n    }\n    if (runner->fatal_failures && (runner->report.failed != 0 || runner->report.errored != 0))\n      break;\n  }\n}\n\n/* Run a single test, with every combination of parameters\n * requested. */\nstatic void\nmunit_test_runner_run_test(MunitTestRunner* runner,\n                           const MunitTest* test,\n                           const char* prefix) {\n  char* test_name = munit_maybe_concat(NULL, (char*) prefix, (char*) test->name);\n  /* The array of parameters to pass to\n   * munit_test_runner_run_test_with_params */\n  MunitParameter* params = NULL;\n  size_t params_l = 0;\n  /* Wildcard parameters are parameters which have possible values\n   * specified in the test, but no specific value was passed to the\n   * CLI.  That means we want to run the test once for every\n   * possible combination of parameter values or, if --single was\n   * passed to the CLI, a single time with a random set of\n   * parameters. */\n  MunitParameter* wild_params = NULL;\n  size_t wild_params_l = 0;\n  const MunitParameterEnum* pe;\n  const MunitParameter* cli_p;\n  munit_bool filled;\n  unsigned int possible;\n  char** vals;\n  size_t first_wild;\n  const MunitParameter* wp;\n  int pidx;\n\n  munit_rand_seed(runner->seed);\n\n  fprintf(MUNIT_OUTPUT_FILE, \"%-\" MUNIT_XSTRINGIFY(MUNIT_TEST_NAME_LEN) \"s\", test_name);\n\n  if (test->parameters == NULL) {\n    /* No parameters.  Simple, nice. */\n    munit_test_runner_run_test_with_params(runner, test, NULL);\n  } else {\n    fputc('\\n', MUNIT_OUTPUT_FILE);\n\n    for (pe = test->parameters ; pe != NULL && pe->name != NULL ; pe++) {\n      /* Did we received a value for this parameter from the CLI? */\n      filled = 0;\n      for (cli_p = runner->parameters ; cli_p != NULL && cli_p->name != NULL ; cli_p++) {\n        if (strcmp(cli_p->name, pe->name) == 0) {\n          if (MUNIT_UNLIKELY(munit_parameters_add(&params_l, &params, pe->name, cli_p->value) != MUNIT_OK))\n            goto cleanup;\n          filled = 1;\n          break;\n        }\n      }\n      if (filled)\n        continue;\n\n      /* Nothing from CLI, is the enum NULL/empty?  We're not a\n       * fuzzer… */\n      if (pe->values == NULL || pe->values[0] == NULL)\n        continue;\n\n      /* If --single was passed to the CLI, choose a value from the\n       * list of possibilities randomly. */\n      if (runner->single_parameter_mode) {\n        possible = 0;\n        for (vals = pe->values ; *vals != NULL ; vals++)\n          possible++;\n        /* We want the tests to be reproducible, even if you're only\n         * running a single test, but we don't want every test with\n         * the same number of parameters to choose the same parameter\n         * number, so use the test name as a primitive salt. */\n        pidx = munit_rand_at_most(munit_str_hash(test_name), possible - 1);\n        if (MUNIT_UNLIKELY(munit_parameters_add(&params_l, &params, pe->name, pe->values[pidx]) != MUNIT_OK))\n          goto cleanup;\n      } else {\n        /* We want to try every permutation.  Put in a placeholder\n         * entry, we'll iterate through them later. */\n        if (MUNIT_UNLIKELY(munit_parameters_add(&wild_params_l, &wild_params, pe->name, NULL) != MUNIT_OK))\n          goto cleanup;\n      }\n    }\n\n    if (wild_params_l != 0) {\n      first_wild = params_l;\n      for (wp = wild_params ; wp != NULL && wp->name != NULL ; wp++) {\n        for (pe = test->parameters ; pe != NULL && pe->name != NULL && pe->values != NULL ; pe++) {\n          if (strcmp(wp->name, pe->name) == 0) {\n            if (MUNIT_UNLIKELY(munit_parameters_add(&params_l, &params, pe->name, pe->values[0]) != MUNIT_OK))\n              goto cleanup;\n          }\n        }\n      }\n\n      munit_test_runner_run_test_wild(runner, test, test_name, params, params + first_wild);\n    } else {\n      munit_test_runner_run_test_with_params(runner, test, params);\n    }\n\n  cleanup:\n    free(params);\n    free(wild_params);\n  }\n\n  munit_maybe_free_concat(test_name, prefix, test->name);\n}\n\n/* Recurse through the suite and run all the tests.  If a list of\n * tests to run was provied on the command line, run only those\n * tests.  */\nstatic void\nmunit_test_runner_run_suite(MunitTestRunner* runner,\n                            const MunitSuite* suite,\n                            const char* prefix) {\n  size_t pre_l;\n  char* pre = munit_maybe_concat(&pre_l, (char*) prefix, (char*) suite->prefix);\n  const MunitTest* test;\n  const char** test_name;\n  const MunitSuite* child_suite;\n\n  /* Run the tests. */\n  for (test = suite->tests ; test != NULL && test->test != NULL ; test++) {\n    if (runner->tests != NULL) { /* Specific tests were requested on the CLI */\n      for (test_name = runner->tests ; test_name != NULL && *test_name != NULL ; test_name++) {\n        if ((pre_l == 0 || strncmp(pre, *test_name, pre_l) == 0) &&\n            strncmp(test->name, *test_name + pre_l, strlen(*test_name + pre_l)) == 0) {\n          munit_test_runner_run_test(runner, test, pre);\n          if (runner->fatal_failures && (runner->report.failed != 0 || runner->report.errored != 0))\n            goto cleanup;\n        }\n      }\n    } else { /* Run all tests */\n      munit_test_runner_run_test(runner, test, pre);\n    }\n  }\n\n  if (runner->fatal_failures && (runner->report.failed != 0 || runner->report.errored != 0))\n    goto cleanup;\n\n  /* Run any child suites. */\n  for (child_suite = suite->suites ; child_suite != NULL && child_suite->prefix != NULL ; child_suite++) {\n    munit_test_runner_run_suite(runner, child_suite, pre);\n  }\n\n cleanup:\n\n  munit_maybe_free_concat(pre, prefix, suite->prefix);\n}\n\nstatic void\nmunit_test_runner_run(MunitTestRunner* runner) {\n  munit_test_runner_run_suite(runner, runner->suite, NULL);\n}\n\nstatic void\nmunit_print_help(int argc, char* const argv[MUNIT_ARRAY_PARAM(argc + 1)], void* user_data, const MunitArgument arguments[]) {\n  const MunitArgument* arg;\n  (void) argc;\n\n  printf(\"USAGE: %s [OPTIONS...] [TEST...]\\n\\n\", argv[0]);\n  puts(\" --seed SEED\\n\"\n       \"           Value used to seed the PRNG.  Must be a 32-bit integer in decimal\\n\"\n       \"           notation with no separators (commas, decimals, spaces, etc.), or\\n\"\n       \"           hexadecimal prefixed by \\\"0x\\\".\\n\"\n       \" --iterations N\\n\"\n       \"           Run each test N times.  0 means the default number.\\n\"\n       \" --param name value\\n\"\n       \"           A parameter key/value pair which will be passed to any test with\\n\"\n       \"           takes a parameter of that name.  If not provided, the test will be\\n\"\n       \"           run once for each possible parameter value.\\n\"\n       \" --list    Write a list of all available tests.\\n\"\n       \" --list-params\\n\"\n       \"           Write a list of all available tests and their possible parameters.\\n\"\n       \" --single  Run each parameterized test in a single configuration instead of\\n\"\n       \"           every possible combination\\n\"\n       \" --log-visible debug|info|warning|error\\n\"\n       \" --log-fatal debug|info|warning|error\\n\"\n       \"           Set the level at which messages of different severities are visible,\\n\"\n       \"           or cause the test to terminate.\\n\"\n#if !defined(MUNIT_NO_FORK)\n       \" --no-fork Do not execute tests in a child process.  If this option is supplied\\n\"\n       \"           and a test crashes (including by failing an assertion), no further\\n\"\n       \"           tests will be performed.\\n\"\n#endif\n       \" --fatal-failures\\n\"\n       \"           Stop executing tests as soon as a failure is found.\\n\"\n       \" --show-stderr\\n\"\n       \"           Show data written to stderr by the tests, even if the test succeeds.\\n\"\n       \" --color auto|always|never\\n\"\n       \"           Colorize (or don't) the output.\\n\"\n     /* 12345678901234567890123456789012345678901234567890123456789012345678901234567890 */\n       \" --help    Print this help message and exit.\\n\");\n#if defined(MUNIT_NL_LANGINFO)\n  setlocale(LC_ALL, \"\");\n  fputs((strcasecmp(\"UTF-8\", nl_langinfo(CODESET)) == 0) ? \"µnit\" : \"munit\", stdout);\n#else\n  puts(\"munit\");\n#endif\n  printf(\" %d.%d.%d\\n\"\n         \"Full documentation at: https://nemequ.github.io/munit/\\n\",\n         (MUNIT_CURRENT_VERSION >> 16) & 0xff,\n         (MUNIT_CURRENT_VERSION >> 8) & 0xff,\n         (MUNIT_CURRENT_VERSION >> 0) & 0xff);\n  for (arg = arguments ; arg != NULL && arg->name != NULL ; arg++)\n    arg->write_help(arg, user_data);\n}\n\nstatic const MunitArgument*\nmunit_arguments_find(const MunitArgument arguments[], const char* name) {\n  const MunitArgument* arg;\n\n  for (arg = arguments ; arg != NULL && arg->name != NULL ; arg++)\n    if (strcmp(arg->name, name) == 0)\n      return arg;\n\n  return NULL;\n}\n\nstatic void\nmunit_suite_list_tests(const MunitSuite* suite, munit_bool show_params, const char* prefix) {\n  size_t pre_l;\n  char* pre = munit_maybe_concat(&pre_l, (char*) prefix, (char*) suite->prefix);\n  const MunitTest* test;\n  const MunitParameterEnum* params;\n  munit_bool first;\n  char** val;\n  const MunitSuite* child_suite;\n\n  for (test = suite->tests ;\n       test != NULL && test->name != NULL ;\n       test++) {\n    if (pre != NULL)\n      fputs(pre, stdout);\n    puts(test->name);\n\n    if (show_params) {\n      for (params = test->parameters ;\n           params != NULL && params->name != NULL ;\n           params++) {\n        fprintf(stdout, \" - %s: \", params->name);\n        if (params->values == NULL) {\n          puts(\"Any\");\n        } else {\n          first = 1;\n          for (val = params->values ;\n               *val != NULL ;\n               val++ ) {\n            if(!first) {\n              fputs(\", \", stdout);\n            } else {\n              first = 0;\n            }\n            fputs(*val, stdout);\n          }\n          putc('\\n', stdout);\n        }\n      }\n    }\n  }\n\n  for (child_suite = suite->suites ; child_suite != NULL && child_suite->prefix != NULL ; child_suite++) {\n    munit_suite_list_tests(child_suite, show_params, pre);\n  }\n\n  munit_maybe_free_concat(pre, prefix, suite->prefix);\n}\n\nstatic munit_bool\nmunit_stream_supports_ansi(FILE *stream) {\n#if !defined(_WIN32)\n  return isatty(fileno(stream));\n#else\n\n#if !defined(__MINGW32__)\n  size_t ansicon_size = 0;\n#endif\n\n  if (isatty(fileno(stream))) {\n#if !defined(__MINGW32__)\n    getenv_s(&ansicon_size, NULL, 0, \"ANSICON\");\n    return ansicon_size != 0;\n#else\n    return getenv(\"ANSICON\") != NULL;\n#endif\n  }\n  return 0;\n#endif\n}\n\nint\nmunit_suite_main_custom(const MunitSuite* suite, void* user_data,\n                        int argc, char* const argv[MUNIT_ARRAY_PARAM(argc + 1)],\n                        const MunitArgument arguments[]) {\n  int result = EXIT_FAILURE;\n  MunitTestRunner runner;\n  size_t parameters_size = 0;\n  size_t tests_size = 0;\n  int arg;\n\n  char* envptr;\n  unsigned long ts;\n  char* endptr;\n  unsigned long long iterations;\n  MunitLogLevel level;\n  const MunitArgument* argument;\n  const char** runner_tests;\n  unsigned int tests_run;\n  unsigned int tests_total;\n\n  runner.prefix = NULL;\n  runner.suite = NULL;\n  runner.tests = NULL;\n  runner.seed = 0;\n  runner.iterations = 0;\n  runner.parameters = NULL;\n  runner.single_parameter_mode = 0;\n  runner.user_data = NULL;\n\n  runner.report.successful = 0;\n  runner.report.skipped = 0;\n  runner.report.failed = 0;\n  runner.report.errored = 0;\n#if defined(MUNIT_ENABLE_TIMING)\n  runner.report.cpu_clock = 0;\n  runner.report.wall_clock = 0;\n#endif\n\n  runner.colorize = 0;\n#if !defined(_WIN32)\n  runner.fork = 1;\n#else\n  runner.fork = 0;\n#endif\n  runner.show_stderr = 0;\n  runner.fatal_failures = 0;\n  runner.suite = suite;\n  runner.user_data = user_data;\n  runner.seed = munit_rand_generate_seed();\n  runner.colorize = munit_stream_supports_ansi(MUNIT_OUTPUT_FILE);\n\n  for (arg = 1 ; arg < argc ; arg++) {\n    if (strncmp(\"--\", argv[arg], 2) == 0) {\n      if (strcmp(\"seed\", argv[arg] + 2) == 0) {\n        if (arg + 1 >= argc) {\n          munit_logf_internal(MUNIT_LOG_ERROR, stderr, \"%s requires an argument\", argv[arg]);\n          goto cleanup;\n        }\n\n        envptr = argv[arg + 1];\n        ts = strtoul(argv[arg + 1], &envptr, 0);\n        if (*envptr != '\\0' || ts > (~((munit_uint32_t) 0U))) {\n          munit_logf_internal(MUNIT_LOG_ERROR, stderr, \"invalid value ('%s') passed to %s\", argv[arg + 1], argv[arg]);\n          goto cleanup;\n        }\n        runner.seed = (munit_uint32_t) ts;\n\n        arg++;\n      } else if (strcmp(\"iterations\", argv[arg] + 2) == 0) {\n        if (arg + 1 >= argc) {\n          munit_logf_internal(MUNIT_LOG_ERROR, stderr, \"%s requires an argument\", argv[arg]);\n          goto cleanup;\n        }\n\n        endptr = argv[arg + 1];\n        iterations = strtoul(argv[arg + 1], &endptr, 0);\n        if (*endptr != '\\0' || iterations > UINT_MAX) {\n          munit_logf_internal(MUNIT_LOG_ERROR, stderr, \"invalid value ('%s') passed to %s\", argv[arg + 1], argv[arg]);\n          goto cleanup;\n        }\n\n        runner.iterations = (unsigned int) iterations;\n\n        arg++;\n      } else if (strcmp(\"param\", argv[arg] + 2) == 0) {\n        if (arg + 2 >= argc) {\n          munit_logf_internal(MUNIT_LOG_ERROR, stderr, \"%s requires two arguments\", argv[arg]);\n          goto cleanup;\n        }\n\n        runner.parameters = realloc(runner.parameters, sizeof(MunitParameter) * (parameters_size + 2));\n        if (runner.parameters == NULL) {\n          munit_log_internal(MUNIT_LOG_ERROR, stderr, \"failed to allocate memory\");\n          goto cleanup;\n        }\n        runner.parameters[parameters_size].name = (char*) argv[arg + 1];\n        runner.parameters[parameters_size].value = (char*) argv[arg + 2];\n        parameters_size++;\n        runner.parameters[parameters_size].name = NULL;\n        runner.parameters[parameters_size].value = NULL;\n        arg += 2;\n      } else if (strcmp(\"color\", argv[arg] + 2) == 0) {\n        if (arg + 1 >= argc) {\n          munit_logf_internal(MUNIT_LOG_ERROR, stderr, \"%s requires an argument\", argv[arg]);\n          goto cleanup;\n        }\n\n        if (strcmp(argv[arg + 1], \"always\") == 0)\n          runner.colorize = 1;\n        else if (strcmp(argv[arg + 1], \"never\") == 0)\n          runner.colorize = 0;\n        else if (strcmp(argv[arg + 1], \"auto\") == 0)\n          runner.colorize = munit_stream_supports_ansi(MUNIT_OUTPUT_FILE);\n        else {\n          munit_logf_internal(MUNIT_LOG_ERROR, stderr, \"invalid value ('%s') passed to %s\", argv[arg + 1], argv[arg]);\n          goto cleanup;\n        }\n\n        arg++;\n      } else if (strcmp(\"help\", argv[arg] + 2) == 0) {\n        munit_print_help(argc, argv, user_data, arguments);\n        result = EXIT_SUCCESS;\n        goto cleanup;\n      } else if (strcmp(\"single\", argv[arg] + 2) == 0) {\n        runner.single_parameter_mode = 1;\n      } else if (strcmp(\"show-stderr\", argv[arg] + 2) == 0) {\n        runner.show_stderr = 1;\n#if !defined(_WIN32)\n      } else if (strcmp(\"no-fork\", argv[arg] + 2) == 0) {\n        runner.fork = 0;\n#endif\n      } else if (strcmp(\"fatal-failures\", argv[arg] + 2) == 0) {\n        runner.fatal_failures = 1;\n      } else if (strcmp(\"log-visible\", argv[arg] + 2) == 0 ||\n                 strcmp(\"log-fatal\", argv[arg] + 2) == 0) {\n        if (arg + 1 >= argc) {\n          munit_logf_internal(MUNIT_LOG_ERROR, stderr, \"%s requires an argument\", argv[arg]);\n          goto cleanup;\n        }\n\n        if (strcmp(argv[arg + 1], \"debug\") == 0)\n          level = MUNIT_LOG_DEBUG;\n        else if (strcmp(argv[arg + 1], \"info\") == 0)\n          level = MUNIT_LOG_INFO;\n        else if (strcmp(argv[arg + 1], \"warning\") == 0)\n          level = MUNIT_LOG_WARNING;\n        else if (strcmp(argv[arg + 1], \"error\") == 0)\n          level = MUNIT_LOG_ERROR;\n        else {\n          munit_logf_internal(MUNIT_LOG_ERROR, stderr, \"invalid value ('%s') passed to %s\", argv[arg + 1], argv[arg]);\n          goto cleanup;\n        }\n\n        if (strcmp(\"log-visible\", argv[arg] + 2) == 0)\n          munit_log_level_visible = level;\n        else\n          munit_log_level_fatal = level;\n\n        arg++;\n      } else if (strcmp(\"list\", argv[arg] + 2) == 0) {\n        munit_suite_list_tests(suite, 0, NULL);\n        result = EXIT_SUCCESS;\n        goto cleanup;\n      } else if (strcmp(\"list-params\", argv[arg] + 2) == 0) {\n        munit_suite_list_tests(suite, 1, NULL);\n        result = EXIT_SUCCESS;\n        goto cleanup;\n      } else {\n        argument = munit_arguments_find(arguments, argv[arg] + 2);\n        if (argument == NULL) {\n          munit_logf_internal(MUNIT_LOG_ERROR, stderr, \"unknown argument ('%s')\", argv[arg]);\n          goto cleanup;\n        }\n\n        if (!argument->parse_argument(suite, user_data, &arg, argc, argv))\n          goto cleanup;\n      }\n    } else {\n      runner_tests = realloc((void*) runner.tests, sizeof(char*) * (tests_size + 2));\n      if (runner_tests == NULL) {\n        munit_log_internal(MUNIT_LOG_ERROR, stderr, \"failed to allocate memory\");\n        goto cleanup;\n      }\n      runner.tests = runner_tests;\n      runner.tests[tests_size++] = argv[arg];\n      runner.tests[tests_size] = NULL;\n    }\n  }\n\n  fflush(stderr);\n  fprintf(MUNIT_OUTPUT_FILE, \"Running test suite with seed 0x%08\" PRIx32 \"...\\n\", runner.seed);\n\n  munit_test_runner_run(&runner);\n\n  tests_run = runner.report.successful + runner.report.failed + runner.report.errored;\n  tests_total = tests_run + runner.report.skipped;\n  if (tests_run == 0) {\n    fprintf(stderr, \"No tests run, %d (100%%) skipped.\\n\", runner.report.skipped);\n  } else {\n    fprintf(MUNIT_OUTPUT_FILE, \"%d of %d (%0.0f%%) tests successful, %d (%0.0f%%) test skipped.\\n\",\n            runner.report.successful, tests_run,\n            (((double) runner.report.successful) / ((double) tests_run)) * 100.0,\n            runner.report.skipped,\n            (((double) runner.report.skipped) / ((double) tests_total)) * 100.0);\n  }\n\n  if (runner.report.failed == 0 && runner.report.errored == 0) {\n    result = EXIT_SUCCESS;\n  }\n\n cleanup:\n  free(runner.parameters);\n  free((void*) runner.tests);\n\n  return result;\n}\n\nint\nmunit_suite_main(const MunitSuite* suite, void* user_data,\n                 int argc, char* const argv[MUNIT_ARRAY_PARAM(argc + 1)]) {\n  return munit_suite_main_custom(suite, user_data, argc, argv, NULL);\n}\n"
  },
  {
    "path": "common/floats/src/munit.h",
    "content": "/* µnit Testing Framework\n * Copyright (c) 2013-2017 Evan Nemerson <evan@nemerson.com>\n *\n * Permission is hereby granted, free of charge, to any person\n * obtaining a copy of this software and associated documentation\n * files (the \"Software\"), to deal in the Software without\n * restriction, including without limitation the rights to use, copy,\n * modify, merge, publish, distribute, sublicense, and/or sell copies\n * of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS\n * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n#if !defined(MUNIT_H)\n#define MUNIT_H\n\n#include <stdarg.h>\n#include <stdlib.h>\n\n#define MUNIT_VERSION(major, minor, revision) \\\n  (((major) << 16) | ((minor) << 8) | (revision))\n\n#define MUNIT_CURRENT_VERSION MUNIT_VERSION(0, 4, 1)\n\n#if defined(_MSC_VER) && (_MSC_VER < 1600)\n#  define munit_int8_t   __int8\n#  define munit_uint8_t  unsigned __int8\n#  define munit_int16_t  __int16\n#  define munit_uint16_t unsigned __int16\n#  define munit_int32_t  __int32\n#  define munit_uint32_t unsigned __int32\n#  define munit_int64_t  __int64\n#  define munit_uint64_t unsigned __int64\n#else\n#  include <stdint.h>\n#  define munit_int8_t   int8_t\n#  define munit_uint8_t  uint8_t\n#  define munit_int16_t  int16_t\n#  define munit_uint16_t uint16_t\n#  define munit_int32_t  int32_t\n#  define munit_uint32_t uint32_t\n#  define munit_int64_t  int64_t\n#  define munit_uint64_t uint64_t\n#endif\n\n#if defined(_MSC_VER) && (_MSC_VER < 1800)\n#  if !defined(PRIi8)\n#    define PRIi8 \"i\"\n#  endif\n#  if !defined(PRIi16)\n#    define PRIi16 \"i\"\n#  endif\n#  if !defined(PRIi32)\n#    define PRIi32 \"i\"\n#  endif\n#  if !defined(PRIi64)\n#    define PRIi64 \"I64i\"\n#  endif\n#  if !defined(PRId8)\n#    define PRId8 \"d\"\n#  endif\n#  if !defined(PRId16)\n#    define PRId16 \"d\"\n#  endif\n#  if !defined(PRId32)\n#    define PRId32 \"d\"\n#  endif\n#  if !defined(PRId64)\n#    define PRId64 \"I64d\"\n#  endif\n#  if !defined(PRIx8)\n#    define PRIx8 \"x\"\n#  endif\n#  if !defined(PRIx16)\n#    define PRIx16 \"x\"\n#  endif\n#  if !defined(PRIx32)\n#    define PRIx32 \"x\"\n#  endif\n#  if !defined(PRIx64)\n#    define PRIx64 \"I64x\"\n#  endif\n#  if !defined(PRIu8)\n#    define PRIu8 \"u\"\n#  endif\n#  if !defined(PRIu16)\n#    define PRIu16 \"u\"\n#  endif\n#  if !defined(PRIu32)\n#    define PRIu32 \"u\"\n#  endif\n#  if !defined(PRIu64)\n#    define PRIu64 \"I64u\"\n#  endif\n#else\n#  include <inttypes.h>\n#endif\n\n#if !defined(munit_bool)\n#  if defined(bool)\n#    define munit_bool bool\n#  elif defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)\n#    define munit_bool _Bool\n#  else\n#    define munit_bool int\n#  endif\n#endif\n\n#if defined(__cplusplus)\nextern \"C\" {\n#endif\n\n#if defined(__GNUC__)\n#  define MUNIT_LIKELY(expr) (__builtin_expect ((expr), 1))\n#  define MUNIT_UNLIKELY(expr) (__builtin_expect ((expr), 0))\n#  define MUNIT_UNUSED __attribute__((__unused__))\n#else\n#  define MUNIT_LIKELY(expr) (expr)\n#  define MUNIT_UNLIKELY(expr) (expr)\n#  define MUNIT_UNUSED\n#endif\n\n#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && !defined(__PGI)\n#  define MUNIT_ARRAY_PARAM(name) name\n#else\n#  define MUNIT_ARRAY_PARAM(name)\n#endif\n\n#if !defined(_WIN32)\n#  define MUNIT_SIZE_MODIFIER \"z\"\n#  define MUNIT_CHAR_MODIFIER \"hh\"\n#  define MUNIT_SHORT_MODIFIER \"h\"\n#else\n#  if defined(_M_X64) || defined(__amd64__)\n#    define MUNIT_SIZE_MODIFIER \"I64\"\n#  else\n#    define MUNIT_SIZE_MODIFIER \"\"\n#  endif\n#  define MUNIT_CHAR_MODIFIER \"\"\n#  define MUNIT_SHORT_MODIFIER \"\"\n#endif\n\n#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L\n#  define MUNIT_NO_RETURN _Noreturn\n#elif defined(__GNUC__)\n#  define MUNIT_NO_RETURN __attribute__((__noreturn__))\n#elif defined(_MSC_VER)\n#  define MUNIT_NO_RETURN __declspec(noreturn)\n#else\n#  define MUNIT_NO_RETURN\n#endif\n\n#if defined(_MSC_VER) &&  (_MSC_VER >= 1500)\n#  define MUNIT_PUSH_DISABLE_MSVC_C4127_ __pragma(warning(push)) __pragma(warning(disable:4127))\n#  define MUNIT_POP_DISABLE_MSVC_C4127_ __pragma(warning(pop))\n#else\n#  define MUNIT_PUSH_DISABLE_MSVC_C4127_\n#  define MUNIT_POP_DISABLE_MSVC_C4127_\n#endif\n\ntypedef enum {\n  MUNIT_LOG_DEBUG,\n  MUNIT_LOG_INFO,\n  MUNIT_LOG_WARNING,\n  MUNIT_LOG_ERROR\n} MunitLogLevel;\n\n#if defined(__GNUC__) && !defined(__MINGW32__)\n#  define MUNIT_PRINTF(string_index, first_to_check) __attribute__((format (printf, string_index, first_to_check)))\n#else\n#  define MUNIT_PRINTF(string_index, first_to_check)\n#endif\n\nMUNIT_PRINTF(4, 5)\nvoid munit_logf_ex(MunitLogLevel level, const char* filename, int line, const char* format, ...);\n\n#define munit_logf(level, format, ...) \\\n  munit_logf_ex(level, __FILE__, __LINE__, format, __VA_ARGS__)\n\n#define munit_log(level, msg) \\\n  munit_logf(level, \"%s\", msg)\n\nMUNIT_NO_RETURN\nMUNIT_PRINTF(3, 4)\nvoid munit_errorf_ex(const char* filename, int line, const char* format, ...);\n\n#define munit_errorf(format, ...) \\\n  munit_errorf_ex(__FILE__, __LINE__, format, __VA_ARGS__)\n\n#define munit_error(msg) \\\n  munit_errorf(\"%s\", msg)\n\n#define munit_assert(expr) \\\n  do { \\\n    if (!MUNIT_LIKELY(expr)) { \\\n      munit_error(\"assertion failed: \" #expr); \\\n    } \\\n    MUNIT_PUSH_DISABLE_MSVC_C4127_ \\\n  } while (0) \\\n  MUNIT_POP_DISABLE_MSVC_C4127_\n\n#define munit_assert_true(expr) \\\n  do { \\\n    if (!MUNIT_LIKELY(expr)) { \\\n      munit_error(\"assertion failed: \" #expr \" is not true\"); \\\n    } \\\n    MUNIT_PUSH_DISABLE_MSVC_C4127_ \\\n  } while (0) \\\n  MUNIT_POP_DISABLE_MSVC_C4127_\n\n#define munit_assert_false(expr) \\\n  do { \\\n    if (!MUNIT_LIKELY(!(expr))) { \\\n      munit_error(\"assertion failed: \" #expr \" is not false\"); \\\n    } \\\n    MUNIT_PUSH_DISABLE_MSVC_C4127_ \\\n  } while (0) \\\n  MUNIT_POP_DISABLE_MSVC_C4127_\n\n#define munit_assert_type_full(prefix, suffix, T, fmt, a, op, b)   \\\n  do { \\\n    T munit_tmp_a_ = (a); \\\n    T munit_tmp_b_ = (b); \\\n    if (!(munit_tmp_a_ op munit_tmp_b_)) {                               \\\n      munit_errorf(\"assertion failed: %s %s %s (\" prefix \"%\" fmt suffix \" %s \" prefix \"%\" fmt suffix \")\", \\\n                   #a, #op, #b, munit_tmp_a_, #op, munit_tmp_b_); \\\n    } \\\n    MUNIT_PUSH_DISABLE_MSVC_C4127_ \\\n  } while (0) \\\n  MUNIT_POP_DISABLE_MSVC_C4127_\n\n#define munit_assert_type(T, fmt, a, op, b) \\\n  munit_assert_type_full(\"\", \"\", T, fmt, a, op, b)\n\n#define munit_assert_char(a, op, b) \\\n  munit_assert_type_full(\"'\\\\x\", \"'\", char, \"02\" MUNIT_CHAR_MODIFIER \"x\", a, op, b)\n#define munit_assert_uchar(a, op, b) \\\n  munit_assert_type_full(\"'\\\\x\", \"'\", unsigned char, \"02\" MUNIT_CHAR_MODIFIER \"x\", a, op, b)\n#define munit_assert_short(a, op, b) \\\n  munit_assert_type(short, MUNIT_SHORT_MODIFIER \"d\", a, op, b)\n#define munit_assert_ushort(a, op, b) \\\n  munit_assert_type(unsigned short, MUNIT_SHORT_MODIFIER \"u\", a, op, b)\n#define munit_assert_int(a, op, b) \\\n  munit_assert_type(int, \"d\", a, op, b)\n#define munit_assert_uint(a, op, b) \\\n  munit_assert_type(unsigned int, \"u\", a, op, b)\n#define munit_assert_long(a, op, b) \\\n  munit_assert_type(long int, \"ld\", a, op, b)\n#define munit_assert_ulong(a, op, b) \\\n  munit_assert_type(unsigned long int, \"lu\", a, op, b)\n#define munit_assert_llong(a, op, b) \\\n  munit_assert_type(long long int, \"lld\", a, op, b)\n#define munit_assert_ullong(a, op, b) \\\n  munit_assert_type(unsigned long long int, \"llu\", a, op, b)\n\n#define munit_assert_size(a, op, b) \\\n  munit_assert_type(size_t, MUNIT_SIZE_MODIFIER \"u\", a, op, b)\n\n#define munit_assert_float(a, op, b) \\\n  munit_assert_type(float, \"f\", a, op, b)\n#define munit_assert_double(a, op, b) \\\n  munit_assert_type(double, \"g\", a, op, b)\n#define munit_assert_ptr(a, op, b) \\\n  munit_assert_type(const void*, \"p\", a, op, b)\n\n#define munit_assert_int8(a, op, b)             \\\n  munit_assert_type(munit_int8_t, PRIi8, a, op, b)\n#define munit_assert_uint8(a, op, b) \\\n  munit_assert_type(munit_uint8_t, PRIu8, a, op, b)\n#define munit_assert_int16(a, op, b) \\\n  munit_assert_type(munit_int16_t, PRIi16, a, op, b)\n#define munit_assert_uint16(a, op, b) \\\n  munit_assert_type(munit_uint16_t, PRIu16, a, op, b)\n#define munit_assert_int32(a, op, b) \\\n  munit_assert_type(munit_int32_t, PRIi32, a, op, b)\n#define munit_assert_uint32(a, op, b) \\\n  munit_assert_type(munit_uint32_t, PRIu32, a, op, b)\n#define munit_assert_int64(a, op, b) \\\n  munit_assert_type(munit_int64_t, PRIi64, a, op, b)\n#define munit_assert_uint64(a, op, b) \\\n  munit_assert_type(munit_uint64_t, PRIu64, a, op, b)\n\n#define munit_assert_double_equal(a, b, precision) \\\n  do { \\\n    const double munit_tmp_a_ = (a); \\\n    const double munit_tmp_b_ = (b); \\\n    const double munit_tmp_diff_ = ((munit_tmp_a_ - munit_tmp_b_) < 0) ? \\\n      -(munit_tmp_a_ - munit_tmp_b_) : \\\n      (munit_tmp_a_ - munit_tmp_b_); \\\n    if (MUNIT_UNLIKELY(munit_tmp_diff_ > 1e-##precision)) { \\\n      munit_errorf(\"assertion failed: %s == %s (%0.\" #precision \"g == %0.\" #precision \"g)\", \\\n\t\t   #a, #b, munit_tmp_a_, munit_tmp_b_); \\\n    } \\\n    MUNIT_PUSH_DISABLE_MSVC_C4127_ \\\n  } while (0) \\\n  MUNIT_POP_DISABLE_MSVC_C4127_\n\n#define munit_assert_float_equal(a, b, precision) \\\n  do { \\\n    const float munit_tmp_a_ = (a); \\\n    const float munit_tmp_b_ = (b); \\\n    const float munit_tmp_diff_ = ((munit_tmp_a_ - munit_tmp_b_) < 0) ? \\\n      -(munit_tmp_a_ - munit_tmp_b_) : \\\n      (munit_tmp_a_ - munit_tmp_b_); \\\n    if (MUNIT_UNLIKELY(munit_tmp_diff_ > 1e-##precision)) { \\\n      munit_errorf(\"assertion failed: %s == %s (%0.\" #precision \"g == %0.\" #precision \"g)\", \\\n\t\t   #a, #b, munit_tmp_a_, munit_tmp_b_); \\\n    } \\\n    MUNIT_PUSH_DISABLE_MSVC_C4127_ \\\n  } while (0) \\\n  MUNIT_POP_DISABLE_MSVC_C4127_\n\n#include <string.h>\n#define munit_assert_string_equal(a, b) \\\n  do { \\\n    const char* munit_tmp_a_ = a; \\\n    const char* munit_tmp_b_ = b; \\\n    if (MUNIT_UNLIKELY(strcmp(munit_tmp_a_, munit_tmp_b_) != 0)) { \\\n      munit_errorf(\"assertion failed: string %s == %s (\\\"%s\\\" == \\\"%s\\\")\", \\\n                   #a, #b, munit_tmp_a_, munit_tmp_b_); \\\n    } \\\n    MUNIT_PUSH_DISABLE_MSVC_C4127_ \\\n  } while (0) \\\n  MUNIT_POP_DISABLE_MSVC_C4127_\n\n#define munit_assert_string_not_equal(a, b) \\\n  do { \\\n    const char* munit_tmp_a_ = a; \\\n    const char* munit_tmp_b_ = b; \\\n    if (MUNIT_UNLIKELY(strcmp(munit_tmp_a_, munit_tmp_b_) == 0)) { \\\n      munit_errorf(\"assertion failed: string %s != %s (\\\"%s\\\" == \\\"%s\\\")\", \\\n                   #a, #b, munit_tmp_a_, munit_tmp_b_); \\\n    } \\\n    MUNIT_PUSH_DISABLE_MSVC_C4127_ \\\n  } while (0) \\\n  MUNIT_POP_DISABLE_MSVC_C4127_\n\n#define munit_assert_floats_equal(size, a, b) \\\n  do { \\\n    const float* munit_tmp_a_ = (const float*) (a); \\\n    const float* munit_tmp_b_ = (const float*) (b); \\\n    const size_t munit_tmp_size_ = (size); \\\n    if (MUNIT_UNLIKELY(memcmp(munit_tmp_a_, munit_tmp_b_, munit_tmp_size_ * sizeof(float))) != 0) { \\\n      size_t munit_tmp_pos_; \\\n      for (munit_tmp_pos_ = 0 ; munit_tmp_pos_ < munit_tmp_size_ ; munit_tmp_pos_++) { \\\n        if (abs(munit_tmp_a_[munit_tmp_pos_] - munit_tmp_b_[munit_tmp_pos_]) > 1e-6) { \\\n          munit_errorf(\"assertion failed: floats %s == %s (%f == %f), at offset %\" MUNIT_SIZE_MODIFIER \"u\", \\\n                       #a, #b, munit_tmp_a_[munit_tmp_pos_], munit_tmp_b_[munit_tmp_pos_], munit_tmp_pos_); \\\n          break; \\\n        } \\\n      } \\\n    } \\\n    MUNIT_PUSH_DISABLE_MSVC_C4127_ \\\n  } while (0) \\\n  MUNIT_POP_DISABLE_MSVC_C4127_\n\n#define munit_assert_memory_equal(size, a, b) \\\n  do { \\\n    const unsigned char* munit_tmp_a_ = (const unsigned char*) (a); \\\n    const unsigned char* munit_tmp_b_ = (const unsigned char*) (b); \\\n    const size_t munit_tmp_size_ = (size); \\\n    if (MUNIT_UNLIKELY(memcmp(munit_tmp_a_, munit_tmp_b_, munit_tmp_size_)) != 0) { \\\n      size_t munit_tmp_pos_; \\\n      for (munit_tmp_pos_ = 0 ; munit_tmp_pos_ < munit_tmp_size_ ; munit_tmp_pos_++) { \\\n        if (munit_tmp_a_[munit_tmp_pos_] != munit_tmp_b_[munit_tmp_pos_]) { \\\n          munit_errorf(\"assertion failed: memory %s == %s, at offset %\" MUNIT_SIZE_MODIFIER \"u\", \\\n                       #a, #b, munit_tmp_pos_); \\\n          break; \\\n        } \\\n      } \\\n    } \\\n    MUNIT_PUSH_DISABLE_MSVC_C4127_ \\\n  } while (0) \\\n  MUNIT_POP_DISABLE_MSVC_C4127_\n\n#define munit_assert_memory_not_equal(size, a, b) \\\n  do { \\\n    const unsigned char* munit_tmp_a_ = (const unsigned char*) (a); \\\n    const unsigned char* munit_tmp_b_ = (const unsigned char*) (b); \\\n    const size_t munit_tmp_size_ = (size); \\\n    if (MUNIT_UNLIKELY(memcmp(munit_tmp_a_, munit_tmp_b_, munit_tmp_size_)) == 0) { \\\n      munit_errorf(\"assertion failed: memory %s != %s (%zu bytes)\", \\\n                   #a, #b, munit_tmp_size_); \\\n    } \\\n    MUNIT_PUSH_DISABLE_MSVC_C4127_ \\\n  } while (0) \\\n  MUNIT_POP_DISABLE_MSVC_C4127_\n\n#define munit_assert_ptr_equal(a, b) \\\n  munit_assert_ptr(a, ==, b)\n#define munit_assert_ptr_not_equal(a, b) \\\n  munit_assert_ptr(a, !=, b)\n#define munit_assert_null(ptr) \\\n  munit_assert_ptr(ptr, ==, NULL)\n#define munit_assert_not_null(ptr) \\\n  munit_assert_ptr(ptr, !=, NULL)\n#define munit_assert_ptr_null(ptr) \\\n  munit_assert_ptr(ptr, ==, NULL)\n#define munit_assert_ptr_not_null(ptr) \\\n  munit_assert_ptr(ptr, !=, NULL)\n\n/*** Memory allocation ***/\n\nvoid* munit_malloc_ex(const char* filename, int line, size_t size);\n\n#define munit_malloc(size) \\\n  munit_malloc_ex(__FILE__, __LINE__, (size))\n\n#define munit_new(type) \\\n  ((type*) munit_malloc(sizeof(type)))\n\n#define munit_calloc(nmemb, size) \\\n  munit_malloc((nmemb) * (size))\n\n#define munit_newa(type, nmemb) \\\n  ((type*) munit_calloc((nmemb), sizeof(type)))\n\n/*** Random number generation ***/\n\nvoid munit_rand_seed(munit_uint32_t seed);\nmunit_uint32_t munit_rand_uint32(void);\nint munit_rand_int_range(int min, int max);\ndouble munit_rand_double(void);\nvoid munit_rand_memory(size_t size, munit_uint8_t buffer[MUNIT_ARRAY_PARAM(size)]);\n\n/*** Tests and Suites ***/\n\ntypedef enum {\n  /* Test successful */\n  MUNIT_OK,\n  /* Test failed */\n  MUNIT_FAIL,\n  /* Test was skipped */\n  MUNIT_SKIP,\n  /* Test failed due to circumstances not intended to be tested\n   * (things like network errors, invalid parameter value, failure to\n   * allocate memory in the test harness, etc.). */\n  MUNIT_ERROR\n} MunitResult;\n\ntypedef struct {\n  char*  name;\n  char** values;\n} MunitParameterEnum;\n\ntypedef struct {\n  char* name;\n  char* value;\n} MunitParameter;\n\nconst char* munit_parameters_get(const MunitParameter params[], const char* key);\n\ntypedef enum {\n  MUNIT_TEST_OPTION_NONE             = 0,\n  MUNIT_TEST_OPTION_SINGLE_ITERATION = 1 << 0,\n  MUNIT_TEST_OPTION_TODO             = 1 << 1\n} MunitTestOptions;\n\ntypedef MunitResult (* MunitTestFunc)(const MunitParameter params[], void* user_data_or_fixture);\ntypedef void*       (* MunitTestSetup)(const MunitParameter params[], void* user_data);\ntypedef void        (* MunitTestTearDown)(void* fixture);\n\ntypedef struct {\n  char*               name;\n  MunitTestFunc       test;\n  MunitTestSetup      setup;\n  MunitTestTearDown   tear_down;\n  MunitTestOptions    options;\n  MunitParameterEnum* parameters;\n} MunitTest;\n\ntypedef enum {\n  MUNIT_SUITE_OPTION_NONE = 0\n} MunitSuiteOptions;\n\ntypedef struct MunitSuite_ MunitSuite;\n\nstruct MunitSuite_ {\n  char*             prefix;\n  MunitTest*        tests;\n  MunitSuite*       suites;\n  unsigned int      iterations;\n  MunitSuiteOptions options;\n};\n\nint munit_suite_main(const MunitSuite* suite, void* user_data, int argc, char* const argv[MUNIT_ARRAY_PARAM(argc + 1)]);\n\n/* Note: I'm not very happy with this API; it's likely to change if I\n * figure out something better.  Suggestions welcome. */\n\ntypedef struct MunitArgument_ MunitArgument;\n\nstruct MunitArgument_ {\n  char* name;\n  munit_bool (* parse_argument)(const MunitSuite* suite, void* user_data, int* arg, int argc, char* const argv[MUNIT_ARRAY_PARAM(argc + 1)]);\n  void (* write_help)(const MunitArgument* argument, void* user_data);\n};\n\nint munit_suite_main_custom(const MunitSuite* suite,\n                            void* user_data,\n                            int argc, char* const argv[MUNIT_ARRAY_PARAM(argc + 1)],\n                            const MunitArgument arguments[]);\n\n#if defined(MUNIT_ENABLE_ASSERT_ALIASES)\n\n#define assert_true(expr) munit_assert_true(expr)\n#define assert_false(expr) munit_assert_false(expr)\n#define assert_char(a, op, b) munit_assert_char(a, op, b)\n#define assert_uchar(a, op, b) munit_assert_uchar(a, op, b)\n#define assert_short(a, op, b) munit_assert_short(a, op, b)\n#define assert_ushort(a, op, b) munit_assert_ushort(a, op, b)\n#define assert_int(a, op, b) munit_assert_int(a, op, b)\n#define assert_uint(a, op, b) munit_assert_uint(a, op, b)\n#define assert_long(a, op, b) munit_assert_long(a, op, b)\n#define assert_ulong(a, op, b) munit_assert_ulong(a, op, b)\n#define assert_llong(a, op, b) munit_assert_llong(a, op, b)\n#define assert_ullong(a, op, b) munit_assert_ullong(a, op, b)\n#define assert_size(a, op, b) munit_assert_size(a, op, b)\n#define assert_float(a, op, b) munit_assert_float(a, op, b)\n#define assert_double(a, op, b) munit_assert_double(a, op, b)\n#define assert_ptr(a, op, b) munit_assert_ptr(a, op, b)\n\n#define assert_int8(a, op, b) munit_assert_int8(a, op, b)\n#define assert_uint8(a, op, b) munit_assert_uint8(a, op, b)\n#define assert_int16(a, op, b) munit_assert_int16(a, op, b)\n#define assert_uint16(a, op, b) munit_assert_uint16(a, op, b)\n#define assert_int32(a, op, b) munit_assert_int32(a, op, b)\n#define assert_uint32(a, op, b) munit_assert_uint32(a, op, b)\n#define assert_int64(a, op, b) munit_assert_int64(a, op, b)\n#define assert_uint64(a, op, b) munit_assert_uint64(a, op, b)\n\n#define assert_double_equal(a, b, precision) munit_assert_double_equal(a, b, precision)\n#define assert_string_equal(a, b) munit_assert_string_equal(a, b)\n#define assert_string_not_equal(a, b) munit_assert_string_not_equal(a, b)\n#define assert_memory_equal(size, a, b) munit_assert_memory_equal(size, a, b)\n#define assert_memory_not_equal(size, a, b) munit_assert_memory_not_equal(size, a, b)\n#define assert_ptr_equal(a, b) munit_assert_ptr_equal(a, b)\n#define assert_ptr_not_equal(a, b) munit_assert_ptr_not_equal(a, b)\n#define assert_ptr_null(ptr) munit_assert_null_equal(ptr)\n#define assert_ptr_not_null(ptr) munit_assert_not_null(ptr)\n\n#define assert_null(ptr) munit_assert_null(ptr)\n#define assert_not_null(ptr) munit_assert_not_null(ptr)\n\n#endif /* defined(MUNIT_ENABLE_ASSERT_ALIASES) */\n\n#if defined(__cplusplus)\n}\n#endif\n\n#endif /* !defined(MUNIT_H) */\n\n#if defined(MUNIT_ENABLE_ASSERT_ALIASES)\n#  if defined(assert)\n#    undef assert\n#  endif\n#  define assert(expr) munit_assert(expr)\n#endif"
  },
  {
    "path": "common/heap/filter.go",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage heap\n\nimport (\n\t\"container/heap\"\n\n\t\"golang.org/x/exp/constraints\"\n)\n\n// TopKFilter filters out top k items with maximum weights.\ntype TopKFilter[T any, W constraints.Ordered] struct {\n\t_heap[T, W]\n\tk int\n}\n\n// NewTopKFilter creates a top k filter.\nfunc NewTopKFilter[T any, W constraints.Ordered](k int) *TopKFilter[T, W] {\n\treturn &TopKFilter[T, W]{k: k}\n}\n\n// Push pushes the element x onto the heap.\n// The complexity is O(log n) where n = h.Count().\nfunc (filter *TopKFilter[T, W]) Push(item T, weight W) {\n\theap.Push(&filter._heap, Elem[T, W]{item, weight})\n\tif filter.Len() > filter.k {\n\t\theap.Pop(&filter._heap)\n\t}\n}\n\n// PopAllValues pops all values in the filter with decreasing order.\nfunc (filter *TopKFilter[T, W]) PopAllValues() []T {\n\titems := make([]T, filter.Len())\n\tfor i := len(items) - 1; i >= 0; i-- {\n\t\telem := heap.Pop(&filter._heap).(Elem[T, W])\n\t\titems[i] = elem.Value\n\t}\n\treturn items\n}\n\n// PopAll pops all items in the filter with decreasing order.\nfunc (filter *TopKFilter[T, W]) PopAll() []Elem[T, W] {\n\tresults := make([]Elem[T, W], filter.Len())\n\tfor i := len(results) - 1; i >= 0; i-- {\n\t\tresults[i] = heap.Pop(&filter._heap).(Elem[T, W])\n\t}\n\treturn results\n}\n"
  },
  {
    "path": "common/heap/filter_test.go",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage heap\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTopKFilter(t *testing.T) {\n\t// Test a adjacent vec\n\ta := NewTopKFilter[int32, float32](3)\n\ta.Push(10, 2)\n\ta.Push(20, 8)\n\ta.Push(30, 1)\n\tvalues := a.PopAllValues()\n\tassert.Equal(t, []int32{20, 10, 30}, values)\n\t// Test a full adjacent vec\n\ta = NewTopKFilter[int32, float32](3)\n\ta.Push(10, 2)\n\ta.Push(20, 8)\n\ta.Push(30, 1)\n\ta.Push(40, 2)\n\ta.Push(50, 5)\n\ta.Push(12, 10)\n\ta.Push(67, 7)\n\ta.Push(32, 9)\n\telems := a.PopAll()\n\tassert.Equal(t, []Elem[int32, float32]{\n\t\t{Value: 12, Weight: 10},\n\t\t{Value: 32, Weight: 9},\n\t\t{Value: 20, Weight: 8},\n\t}, elems)\n}\n\nfunc TestTopKStringFilter(t *testing.T) {\n\t// Test a adjacent vec\n\ta := NewTopKFilter[string, float64](3)\n\ta.Push(\"10\", 2)\n\ta.Push(\"20\", 8)\n\ta.Push(\"30\", 1)\n\telems := a.PopAll()\n\tassert.Equal(t, []Elem[string, float64]{\n\t\t{Value: \"20\", Weight: 8},\n\t\t{Value: \"10\", Weight: 2},\n\t\t{Value: \"30\", Weight: 1},\n\t}, elems)\n\t// Test a full adjacent vec\n\ta = NewTopKFilter[string, float64](3)\n\ta.Push(\"10\", 2)\n\ta.Push(\"20\", 8)\n\ta.Push(\"30\", 1)\n\ta.Push(\"40\", 2)\n\ta.Push(\"50\", 5)\n\ta.Push(\"12\", 10)\n\ta.Push(\"67\", 7)\n\ta.Push(\"32\", 9)\n\telems = a.PopAll()\n\tassert.Equal(t, []Elem[string, float64]{\n\t\t{Value: \"12\", Weight: 10},\n\t\t{Value: \"32\", Weight: 9},\n\t\t{Value: \"20\", Weight: 8},\n\t}, elems)\n}\n"
  },
  {
    "path": "common/heap/pq.go",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage heap\n\nimport (\n\t\"container/heap\"\n\t\"encoding/binary\"\n\t\"io\"\n\n\t\"github.com/chewxy/math32\"\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/gorse-io/gorse/common/encoding\"\n\t\"golang.org/x/exp/constraints\"\n)\n\ntype Elem[E any, W constraints.Ordered] struct {\n\tValue  E\n\tWeight W\n}\n\ntype _heap[T any, W constraints.Ordered] struct {\n\telems []Elem[T, W]\n\tdesc  bool\n}\n\nfunc (e *_heap[T, W]) Len() int {\n\treturn len(e.elems)\n}\n\nfunc (e *_heap[T, W]) Less(i, j int) bool {\n\tif e.desc {\n\t\treturn e.elems[i].Weight > e.elems[j].Weight\n\t} else {\n\t\treturn e.elems[i].Weight < e.elems[j].Weight\n\t}\n}\n\nfunc (e *_heap[T, W]) Swap(i, j int) {\n\te.elems[i], e.elems[j] = e.elems[j], e.elems[i]\n}\n\nfunc (e *_heap[T, W]) Push(x interface{}) {\n\tit := x.(Elem[T, W])\n\te.elems = append(e.elems, it)\n}\n\nfunc (e *_heap[T, W]) Pop() interface{} {\n\told := e.elems\n\titem := e.elems[len(old)-1]\n\te.elems = old[0 : len(old)-1]\n\treturn item\n}\n\n// PriorityQueue represents the priority queue.\ntype PriorityQueue struct {\n\t_heap[int32, float32]\n\tlookup mapset.Set[int32]\n}\n\n// NewPriorityQueue initializes an empty priority queue.\nfunc NewPriorityQueue(desc bool) *PriorityQueue {\n\treturn &PriorityQueue{\n\t\t_heap:  _heap[int32, float32]{desc: desc},\n\t\tlookup: mapset.NewSet[int32](),\n\t}\n}\n\n// Push inserts a new element into the queue. No action is performed on duplicate elements.\nfunc (p *PriorityQueue) Push(v int32, weight float32) {\n\tif math32.IsNaN(weight) {\n\t\tpanic(\"NaN weight is forbidden\")\n\t} else if !p.lookup.Contains(v) {\n\t\tnewItem := Elem[int32, float32]{\n\t\t\tValue:  v,\n\t\t\tWeight: weight,\n\t\t}\n\t\theap.Push(&p._heap, newItem)\n\t\tp.lookup.Add(v)\n\t}\n}\n\n// Pop removes the element with the highest priority from the queue and returns it.\n// In case of an empty queue, an error is returned.\nfunc (p *PriorityQueue) Pop() (int32, float32) {\n\titem := heap.Pop(&p._heap).(Elem[int32, float32])\n\treturn item.Value, item.Weight\n}\n\nfunc (p *PriorityQueue) Peek() (int32, float32) {\n\treturn p.elems[0].Value, p.elems[0].Weight\n}\n\nfunc (p *PriorityQueue) Values() []int32 {\n\tvalues := make([]int32, 0, p.Len())\n\tfor _, elem := range p.elems {\n\t\tvalues = append(values, elem.Value)\n\t}\n\treturn values\n}\n\nfunc (p *PriorityQueue) Elems() []Elem[int32, float32] {\n\treturn p.elems\n}\n\nfunc (p *PriorityQueue) Clone() *PriorityQueue {\n\tpq := NewPriorityQueue(p.desc)\n\tpq.elems = make([]Elem[int32, float32], p.Len())\n\tcopy(pq.elems, p.elems)\n\treturn pq\n}\n\nfunc (p *PriorityQueue) Reverse() *PriorityQueue {\n\tpq := NewPriorityQueue(!p.desc)\n\tpq.elems = make([]Elem[int32, float32], 0, p.Len())\n\tfor _, elem := range p.elems {\n\t\tpq.Push(elem.Value, elem.Weight)\n\t}\n\treturn pq\n}\n\nfunc (p *PriorityQueue) Marshal(w io.Writer) error {\n\tif err := binary.Write(w, binary.LittleEndian, p.desc); err != nil {\n\t\treturn err\n\t}\n\treturn encoding.WriteSlice(w, p.elems)\n}\n\nfunc (p *PriorityQueue) Unmarshal(r io.Reader) error {\n\tif err := binary.Read(r, binary.LittleEndian, &p.desc); err != nil {\n\t\treturn err\n\t}\n\tif err := encoding.ReadSlice(r, &p.elems); err != nil {\n\t\treturn err\n\t}\n\tp.lookup = mapset.NewSetWithSize[int32](p.Len())\n\tfor _, elem := range p.elems {\n\t\tp.lookup.Add(elem.Value)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "common/heap/pq_test.go",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage heap\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/samber/lo\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"modernc.org/sortutil\"\n)\n\nfunc TestPriorityQueue(t *testing.T) {\n\tpq := NewPriorityQueue(false)\n\telements := []int32{5, 3, 7, 8, 6, 2, 9}\n\tfor _, e := range elements {\n\t\tpq.Push(e, float32(e))\n\t}\n\tassert.Equal(t, len(elements), pq.Len())\n\tassert.ElementsMatch(t, elements, pq.Values())\n\tassert.Equal(t, len(elements), len(pq.Elems()))\n\n\t// test clone\n\tcp := pq.Clone()\n\tassert.Equal(t, len(elements), cp.Len())\n\n\t// test peek pop\n\tsort.Sort(sortutil.Int32Slice(elements))\n\tfor _, e := range elements {\n\t\tvalue, weight := pq.Peek()\n\t\tassert.Equal(t, e, value)\n\t\tassert.Equal(t, e, int32(weight))\n\t\tvalue, weight = pq.Pop()\n\t\tassert.Equal(t, e, value)\n\t\tassert.Equal(t, e, int32(weight))\n\t}\n\n\t// test reverse\n\tr := cp.Reverse()\n\tlo.Reverse(elements)\n\tfor _, e := range elements {\n\t\tvalue, weight := r.Pop()\n\t\tassert.Equal(t, e, value)\n\t\tassert.Equal(t, e, int32(weight))\n\t}\n}\n\nfunc TestMarshalUnmarshal(t *testing.T) {\n\tpq := NewPriorityQueue(false)\n\telements := []int32{5, 3, 7, 8, 6, 2, 9}\n\tfor _, e := range elements {\n\t\tpq.Push(e, float32(e))\n\t}\n\n\tpath := filepath.Join(t.TempDir(), \"pq.bin\")\n\tf, err := os.Create(path)\n\tassert.NoError(t, err)\n\tdefer f.Close()\n\terr = pq.Marshal(f)\n\tassert.NoError(t, err)\n\n\tf, err = os.Open(path)\n\tassert.NoError(t, err)\n\tdefer f.Close()\n\tpq2 := NewPriorityQueue(false)\n\terr = pq2.Unmarshal(f)\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, pq.Len(), pq2.Len())\n\tassert.Equal(t, pq.desc, pq2.desc)\n\tassert.Equal(t, pq.elems, pq2.elems)\n\tassert.True(t, pq.lookup.Equal(pq2.lookup))\n}\n"
  },
  {
    "path": "common/jsonutil/json.go",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage jsonutil\n\nimport \"encoding/json\"\n\n// Marshal returns the JSON encoding of v.\nfunc Marshal(v interface{}) ([]byte, error) {\n\treturn json.Marshal(v)\n}\n\n// Unmarshal parses the JSON-encoded data and stores the result\n// in the value pointed to by v. If data is empty, Unmarshal clears\n// contents in v.\nfunc Unmarshal(data []byte, v interface{}) error {\n\tif len(data) == 0 {\n\t\tdata = []byte(\"null\")\n\t}\n\treturn json.Unmarshal(data, v)\n}\n\n// MustMarshal returns the JSON encoding of v. Panic if error occurs.\nfunc MustMarshal(v interface{}) string {\n\tdata, err := Marshal(v)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn string(data)\n}\n"
  },
  {
    "path": "common/jsonutil/json_test.go",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage jsonutil\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestUnmarshal(t *testing.T) {\n\tvar a []int\n\terr := Unmarshal([]byte(\"[1,2,3]\"), &a)\n\tassert.NoError(t, err)\n\tassert.Equal(t, []int{1, 2, 3}, a)\n\n\terr = Unmarshal([]byte(\"\"), &a)\n\tassert.NoError(t, err)\n\tassert.Empty(t, a)\n}\n\nfunc TestMarshal(t *testing.T) {\n\tdata, err := Marshal(nil)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"null\", string(data))\n}\n\nfunc TestMustMarshal(t *testing.T) {\n\tassert.Panics(t, func() {\n\t\tMustMarshal(make(chan int))\n\t})\n}\n"
  },
  {
    "path": "common/log/log.go",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage log\n\nimport (\n\t\"net/url\"\n\t\"os\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/c-bata/goptuna\"\n\t\"github.com/emicklei/go-restful/v3\"\n\t\"github.com/go-sql-driver/mysql\"\n\t\"github.com/spf13/pflag\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.uber.org/zap\"\n\t\"go.uber.org/zap/zapcore\"\n\t\"gopkg.in/natefinch/lumberjack.v2\"\n)\n\nvar (\n\tlogger       *zap.Logger\n\topenaiLogger *zap.Logger\n)\n\nfunc init() {\n\t// setup default logger\n\tvar err error\n\tlogger, err = zap.NewProduction()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// setup OpenAI logger\n\tif testing.Testing() {\n\t\topenaiLogger, err = zap.NewDevelopment()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t} else {\n\t\topenaiLogger = zap.NewNop()\n\t}\n\t// Windows file sink support: https://github.com/uber-go/zap/issues/621\n\tif runtime.GOOS == \"windows\" {\n\t\tif err := zap.RegisterSink(\"windows\", func(u *url.URL) (zap.Sink, error) {\n\t\t\t// Remove leading slash left by url.Parse()\n\t\t\treturn os.OpenFile(u.Path[1:], os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)\n\t\t}); err != nil {\n\t\t\tlogger.Fatal(\"failed to register Windows file sink\", zap.Error(err))\n\t\t}\n\t}\n}\n\n// Logger get current logger\nfunc Logger() *zap.Logger {\n\treturn logger\n}\n\nfunc ResponseLogger(resp *restful.Response) *zap.Logger {\n\treturn logger.With(zap.String(\"request_id\", resp.Header().Get(\"X-Request-ID\")))\n}\n\nfunc CloseLogger() {\n\tcfg := zap.NewProductionConfig()\n\tcfg.Level = zap.NewAtomicLevelAt(zap.FatalLevel)\n\tvar err error\n\tlogger, err = cfg.Build()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc AddFlags(flagSet *pflag.FlagSet) {\n\tflagSet.String(\"log-path\", \"\", \"path of log file\")\n\tflagSet.Int(\"log-max-size\", 100, \"maximum size in megabytes of the log file\")\n\tflagSet.Int(\"log-max-age\", 0, \"maximum number of days to retain old log files\")\n\tflagSet.Int(\"log-max-backups\", 0, \"maximum number of old log files to retain\")\n}\n\nfunc SetLogger(flagSet *pflag.FlagSet, debug bool) {\n\t// enable or disable debug mode\n\tvar (\n\t\tencoder zapcore.Encoder\n\t\tlevel   zapcore.LevelEnabler\n\t)\n\tif debug {\n\t\tencoder = zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())\n\t\tlevel = zap.DebugLevel\n\t} else {\n\t\tencoder = zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())\n\t\tlevel = zap.InfoLevel\n\t}\n\t// create lumberjack logger\n\twriters := []zapcore.WriteSyncer{zapcore.AddSync(os.Stdout)}\n\tif flagSet.Changed(\"log-path\") {\n\t\tpath, _ := flagSet.GetString(\"log-path\")\n\t\tmaxSize, _ := flagSet.GetInt(\"log-max-size\")\n\t\tmaxAge, _ := flagSet.GetInt(\"log-max-age\")\n\t\tmaxBackups, _ := flagSet.GetInt(\"log-max-backups\")\n\t\twriters = append(writers, zapcore.AddSync(&lumberjack.Logger{\n\t\t\tFilename:   path,\n\t\t\tMaxSize:    maxSize,\n\t\t\tMaxBackups: maxBackups,\n\t\t\tMaxAge:     maxAge,\n\t\t\tCompress:   false,\n\t\t}))\n\t}\n\t// create zap logger\n\tcore := zapcore.NewCore(encoder, zap.CombineWriteSyncers(writers...), level)\n\tlogger = zap.New(core)\n}\n\nconst mysqlPrefix = \"mysql://\"\n\nfunc RedactDBURL(rawURL string) string {\n\tif strings.HasPrefix(rawURL, \"sqlite://\") {\n\t\treturn rawURL\n\t} else if strings.HasPrefix(rawURL, mysqlPrefix) {\n\t\tparsed, err := mysql.ParseDSN(rawURL[len(mysqlPrefix):])\n\t\tif err != nil {\n\t\t\treturn rawURL\n\t\t}\n\t\tparsed.User = strings.Repeat(\"x\", len(parsed.User))\n\t\tparsed.Passwd = strings.Repeat(\"x\", len(parsed.Passwd))\n\t\treturn mysqlPrefix + parsed.FormatDSN()\n\t} else {\n\t\tparsed, err := url.Parse(rawURL)\n\t\tif err != nil {\n\t\t\treturn rawURL\n\t\t}\n\t\tusername := parsed.User.Username()\n\t\tpassword, _ := parsed.User.Password()\n\t\tparsed.User = url.UserPassword(strings.Repeat(\"x\", len(username)), strings.Repeat(\"x\", len(password)))\n\t\treturn parsed.String()\n\t}\n}\n\nfunc GetErrorHandler() otel.ErrorHandler {\n\treturn &errorHandler{}\n}\n\ntype errorHandler struct{}\n\nfunc (h *errorHandler) Handle(err error) {\n\tLogger().Error(\"opentelemetry failure\", zap.Error(err))\n}\n\nfunc OpenAILogger() *zap.Logger {\n\treturn openaiLogger\n}\n\nfunc InitOpenAILogger(filename string) {\n\tif filename != \"\" {\n\t\topenaiLogger = zap.New(\n\t\t\tzapcore.NewCore(\n\t\t\t\tzapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),\n\t\t\t\tzapcore.AddSync(&lumberjack.Logger{\n\t\t\t\t\tFilename: filename,\n\t\t\t\t}),\n\t\t\t\tzap.InfoLevel))\n\t}\n}\n\ntype OptunaLogger struct {\n\tlogger *zap.Logger\n}\n\nfunc NewOptunaLogger(logger *zap.Logger) goptuna.Logger {\n\treturn &OptunaLogger{logger: logger}\n}\n\nfunc (o OptunaLogger) Debug(msg string, fields ...interface{}) {\n\to.logger.Debug(msg, zap.Any(\"fields\", fields))\n}\n\nfunc (o OptunaLogger) Info(msg string, fields ...interface{}) {\n\to.logger.Info(msg, zap.Any(\"fields\", fields))\n}\n\nfunc (o OptunaLogger) Warn(msg string, fields ...interface{}) {\n\to.logger.Warn(msg, zap.Any(\"fields\", fields))\n}\n\nfunc (o OptunaLogger) Error(msg string, fields ...interface{}) {\n\to.logger.Error(msg, zap.Any(\"fields\", fields))\n}\n"
  },
  {
    "path": "common/log/log_test.go",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage log\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/spf13/pflag\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSetDevelopmentLogger(t *testing.T) {\n\ttemp, err := os.MkdirTemp(\"\", \"gorse\")\n\tassert.NoError(t, err)\n\tflagSet := pflag.NewFlagSet(\"test\", pflag.ContinueOnError)\n\tAddFlags(flagSet)\n\t// set existed path\n\terr = flagSet.Set(\"log-path\", temp+\"/gorse.log\")\n\tassert.NoError(t, err)\n\tSetLogger(flagSet, true)\n\tLogger().Debug(\"test\")\n\tassert.FileExists(t, temp+\"/gorse.log\")\n\t// set non-existed path\n\terr = flagSet.Set(\"log-path\", temp+\"/gorse/gorse.log\")\n\tassert.NoError(t, err)\n\tSetLogger(flagSet, true)\n\tLogger().Debug(\"test\")\n\tassert.FileExists(t, temp+\"/gorse/gorse.log\")\n}\n\nfunc TestSetProductionLogger(t *testing.T) {\n\ttemp, err := os.MkdirTemp(\"\", \"gorse\")\n\tassert.NoError(t, err)\n\tflagSet := pflag.NewFlagSet(\"test\", pflag.ContinueOnError)\n\tAddFlags(flagSet)\n\t// set existed path\n\terr = flagSet.Set(\"log-path\", temp+\"/gorse.log\")\n\tassert.NoError(t, err)\n\tSetLogger(flagSet, false)\n\tLogger().Info(\"test\")\n\tassert.FileExists(t, temp+\"/gorse.log\")\n\t// set non-existed path\n\terr = flagSet.Set(\"log-path\", temp+\"/gorse/gorse.log\")\n\tassert.NoError(t, err)\n\tSetLogger(flagSet, false)\n\tLogger().Info(\"test\")\n\tassert.FileExists(t, temp+\"/gorse/gorse.log\")\n}\n\nfunc TestRedactDBURL(t *testing.T) {\n\tassert.Equal(t, \"sqlite://data/data.sqlite\", RedactDBURL(\"sqlite://data/data.sqlite\"))\n\tassert.Equal(t, \"mysql://xxxxx:xxxxxxxxxx@tcp(localhost:3306)/gorse?parseTime=true\", RedactDBURL(\"mysql://gorse:gorse_pass@tcp(localhost:3306)/gorse?parseTime=true\"))\n\tassert.Equal(t, \"postgres://xxx:xxxxxx@1.2.3.4:5432/mydb?sslmode=verify-full\", RedactDBURL(\"postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full\"))\n\tassert.Equal(t, \"mysql://gorse:gorse_pass@tcp(localhost:3306) gorse?parseTime=true\", RedactDBURL(\"mysql://gorse:gorse_pass@tcp(localhost:3306) gorse?parseTime=true\"))\n\tassert.Equal(t, \"postgres://bob:secret@1.2.3.4:5432 mydb?sslmode=verify-full\", RedactDBURL(\"postgres://bob:secret@1.2.3.4:5432 mydb?sslmode=verify-full\"))\n}\n"
  },
  {
    "path": "common/mock/openai.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage mock\n\nimport (\n\t\"bytes\"\n\t\"crypto/md5\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\n\t\"github.com/emicklei/go-restful/v3\"\n\t\"github.com/samber/lo\"\n\t\"github.com/sashabaranov/go-openai\"\n)\n\ntype OpenAIServer struct {\n\tlistener   net.Listener\n\thttpServer *http.Server\n\tauthToken  string\n\tready      chan struct{}\n}\n\nfunc NewOpenAIServer() *OpenAIServer {\n\ts := &OpenAIServer{}\n\tws := new(restful.WebService)\n\tws.Path(\"/v1\").\n\t\tConsumes(restful.MIME_JSON).\n\t\tProduces(restful.MIME_JSON, \"text/event-stream\")\n\tws.Route(ws.POST(\"chat/completions\").\n\t\tReads(openai.ChatCompletionRequest{}).\n\t\tWrites(openai.ChatCompletionResponse{}).\n\t\tTo(s.chatCompletion))\n\tws.Route(ws.POST(\"embeddings\").\n\t\tReads(openai.EmbeddingRequest{}).\n\t\tWrites(openai.EmbeddingResponse{}).\n\t\tTo(s.embeddings))\n\tcontainer := restful.NewContainer()\n\tcontainer.Add(ws)\n\ts.httpServer = &http.Server{Handler: container}\n\ts.authToken = \"ollama\"\n\ts.ready = make(chan struct{})\n\treturn s\n}\n\nfunc (s *OpenAIServer) Start() error {\n\tvar err error\n\ts.listener, err = net.Listen(\"tcp\", \"\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tclose(s.ready)\n\treturn s.httpServer.Serve(s.listener)\n}\n\nfunc (s *OpenAIServer) BaseURL() string {\n\treturn fmt.Sprintf(\"http://%s/v1\", s.listener.Addr().String())\n}\n\nfunc (s *OpenAIServer) AuthToken() string {\n\treturn s.authToken\n}\n\nfunc (s *OpenAIServer) Ready() {\n\t<-s.ready\n}\n\nfunc (s *OpenAIServer) Close() error {\n\treturn s.httpServer.Close()\n}\n\nfunc (s *OpenAIServer) chatCompletion(req *restful.Request, resp *restful.Response) {\n\tvar r openai.ChatCompletionRequest\n\terr := req.ReadEntity(&r)\n\tif err != nil {\n\t\t_ = resp.WriteError(http.StatusBadRequest, err)\n\t\treturn\n\t}\n\tcontent := r.Messages[0].Content\n\tif r.Model == \"deepseek-r1\" {\n\t\tcontent = \"<think>To be or not to be, that is the question.</think>\" + content\n\t}\n\tif r.Stream {\n\t\tfor i := 0; i < len(content); i += 8 {\n\t\t\tbuf := bytes.NewBuffer(nil)\n\t\t\tbuf.WriteString(\"data: \")\n\t\t\tencoder := json.NewEncoder(buf)\n\t\t\t_ = encoder.Encode(openai.ChatCompletionStreamResponse{\n\t\t\t\tChoices: []openai.ChatCompletionStreamChoice{{\n\t\t\t\t\tDelta: openai.ChatCompletionStreamChoiceDelta{\n\t\t\t\t\t\tContent: content[i:min(i+8, len(content))],\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t})\n\t\t\tbuf.WriteString(\"\\n\")\n\t\t\t_, _ = resp.Write(buf.Bytes())\n\t\t}\n\t} else {\n\t\t_ = resp.WriteEntity(openai.ChatCompletionResponse{\n\t\t\tChoices: []openai.ChatCompletionChoice{{\n\t\t\t\tMessage: openai.ChatCompletionMessage{\n\t\t\t\t\tContent: content,\n\t\t\t\t},\n\t\t\t}},\n\t\t})\n\t}\n}\n\nfunc (s *OpenAIServer) embeddings(req *restful.Request, resp *restful.Response) {\n\t// parse request\n\tvar r openai.EmbeddingRequest\n\terr := req.ReadEntity(&r)\n\tif err != nil {\n\t\t_ = resp.WriteError(http.StatusBadRequest, err)\n\t\treturn\n\t}\n\tinput, ok := r.Input.(string)\n\tif !ok {\n\t\t_ = resp.WriteError(http.StatusBadRequest, fmt.Errorf(\"invalid input type\"))\n\t\treturn\n\t}\n\n\t// write response\n\t_ = resp.WriteEntity(openai.EmbeddingResponse{\n\t\tData: []openai.Embedding{{\n\t\t\tEmbedding: Hash(input),\n\t\t}},\n\t})\n}\n\nfunc Hash(input string) []float32 {\n\thasher := md5.New()\n\t_, err := hasher.Write([]byte(input))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\th := hasher.Sum(nil)\n\treturn lo.Map(h, func(b byte, _ int) float32 {\n\t\treturn float32(b)\n\t})\n}\n"
  },
  {
    "path": "common/mock/openai_test.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage mock\n\nimport (\n\t\"io\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/juju/errors\"\n\t\"github.com/sashabaranov/go-openai\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\ntype OpenAITestSuite struct {\n\tsuite.Suite\n\tserver *OpenAIServer\n\tclient *openai.Client\n}\n\nfunc (suite *OpenAITestSuite) SetupSuite() {\n\t// Start mock server\n\tsuite.server = NewOpenAIServer()\n\tgo func() {\n\t\t_ = suite.server.Start()\n\t}()\n\tsuite.server.Ready()\n\t// Create client\n\tclientConfig := openai.DefaultConfig(suite.server.AuthToken())\n\tclientConfig.BaseURL = suite.server.BaseURL()\n\tsuite.client = openai.NewClientWithConfig(clientConfig)\n}\n\nfunc (suite *OpenAITestSuite) TearDownSuite() {\n\tsuite.NoError(suite.server.Close())\n}\n\nfunc (suite *OpenAITestSuite) TestChatCompletion() {\n\tresp, err := suite.client.CreateChatCompletion(\n\t\tsuite.T().Context(),\n\t\topenai.ChatCompletionRequest{\n\t\t\tModel: \"qwen2.5\",\n\t\t\tMessages: []openai.ChatCompletionMessage{\n\t\t\t\t{\n\t\t\t\t\tRole:    openai.ChatMessageRoleUser,\n\t\t\t\t\tContent: \"Hello\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t)\n\tsuite.NoError(err)\n\tsuite.Equal(\"Hello\", resp.Choices[0].Message.Content)\n}\n\nfunc (suite *OpenAITestSuite) TestChatCompletionStream() {\n\tcontent := \"In my younger and more vulnerable years my father gave me some advice that I've been turning over in\" +\n\t\t\" my mind ever since. Whenever you feel like criticizing anyone, he told me, just remember that all the \" +\n\t\t\"people in this world haven't had the advantages that you've had.\"\n\tstream, err := suite.client.CreateChatCompletionStream(\n\t\tsuite.T().Context(),\n\t\topenai.ChatCompletionRequest{\n\t\t\tModel: \"qwen2.5\",\n\t\t\tMessages: []openai.ChatCompletionMessage{\n\t\t\t\t{\n\t\t\t\t\tRole:    openai.ChatMessageRoleUser,\n\t\t\t\t\tContent: content,\n\t\t\t\t},\n\t\t\t},\n\t\t\tStream: true,\n\t\t},\n\t)\n\tsuite.NoError(err)\n\tdefer stream.Close()\n\tvar buffer strings.Builder\n\tfor {\n\t\tvar resp openai.ChatCompletionStreamResponse\n\t\tresp, err = stream.Recv()\n\t\tif errors.Is(err, io.EOF) {\n\t\t\tsuite.Equal(content, buffer.String())\n\t\t\treturn\n\t\t}\n\t\tsuite.NoError(err)\n\t\tbuffer.WriteString(resp.Choices[0].Delta.Content)\n\t}\n}\n\nfunc (suite *OpenAITestSuite) TestEmbeddings() {\n\tresp, err := suite.client.CreateEmbeddings(\n\t\tsuite.T().Context(),\n\t\topenai.EmbeddingRequest{\n\t\t\tInput: \"Hello\",\n\t\t\tModel: \"mxbai-embed-large\",\n\t\t},\n\t)\n\tsuite.NoError(err)\n\tsuite.Equal([]float32{139, 26, 153, 83, 196, 97, 18, 150, 168, 39, 171, 248, 196, 120, 4, 215}, resp.Data[0].Embedding)\n}\n\nfunc TestOpenAITestSuite(t *testing.T) {\n\tsuite.Run(t, new(OpenAITestSuite))\n}\n"
  },
  {
    "path": "common/monitor/progress.go",
    "content": "// Copyright 2023 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage monitor\n\nimport (\n\t\"context\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/gorse-io/gorse/protocol\"\n\t\"modernc.org/mathutil\"\n)\n\ntype spanKeyType string\n\nvar spanKeyName = spanKeyType(uuid.New().String())\n\ntype Status string\n\nconst (\n\tStatusPending   Status = \"Pending\"\n\tStatusComplete  Status = \"Complete\"\n\tStatusRunning   Status = \"Running\"\n\tStatusSuspended Status = \"Suspended\"\n\tStatusFailed    Status = \"Failed\"\n)\n\ntype Monitor struct {\n\tname  string\n\tspans sync.Map\n}\n\nfunc NewTracer(name string) *Monitor {\n\treturn &Monitor{name: name}\n}\n\n// Start creates a root span.\nfunc (t *Monitor) Start(ctx context.Context, name string, total int) (context.Context, *Span) {\n\tspan := &Span{\n\t\tname:   name,\n\t\tstatus: StatusRunning,\n\t\ttotal:  total,\n\t\tstart:  time.Now(),\n\t}\n\tt.spans.Store(name, span)\n\treturn context.WithValue(ctx, spanKeyName, span), span\n}\n\nfunc (t *Monitor) List() []Progress {\n\tvar progress []Progress\n\tt.spans.Range(func(key, value interface{}) bool {\n\t\tspan := value.(*Span)\n\t\tp := span.Progress()\n\t\tp.Tracer = t.name\n\t\tprogress = append(progress, p)\n\t\treturn true\n\t})\n\t// sort by start time\n\tsort.Slice(progress, func(i, j int) bool {\n\t\treturn progress[i].StartTime.Before(progress[j].StartTime)\n\t})\n\treturn progress\n}\n\ntype Span struct {\n\tname     string\n\tstatus   Status\n\ttotal    int\n\tcount    int\n\terr      string\n\tstart    time.Time\n\tfinish   time.Time\n\tchildren sync.Map\n}\n\nfunc (s *Span) Add(n int) {\n\ts.count = mathutil.Min(s.count+n, s.total)\n}\n\nfunc (s *Span) End() {\n\tif s.status == StatusRunning {\n\t\ts.status = StatusComplete\n\t\ts.count = s.total\n\t\ts.finish = time.Now()\n\t}\n}\n\nfunc (s *Span) Fail(err error) {\n\ts.status = StatusFailed\n\ts.err = err.Error()\n}\n\nfunc (s *Span) Count() int {\n\treturn s.count\n}\n\nfunc (s *Span) Progress() Progress {\n\t// find running children\n\tvar children []Progress\n\ts.children.Range(func(key, value interface{}) bool {\n\t\tchild := value.(*Span)\n\t\tprogress := child.Progress()\n\t\tif progress.Status == StatusRunning {\n\t\t\tchildren = append(children, progress)\n\t\t}\n\t\tif s.err == \"\" && progress.Error != \"\" {\n\t\t\ts.err = progress.Error\n\t\t\ts.status = StatusFailed\n\t\t}\n\t\treturn true\n\t})\n\t// leaf node\n\tif len(children) == 0 {\n\t\treturn Progress{\n\t\t\tName:       s.name,\n\t\t\tStatus:     s.status,\n\t\t\tError:      s.err,\n\t\t\tCount:      s.count,\n\t\t\tTotal:      s.total,\n\t\t\tStartTime:  s.start,\n\t\t\tFinishTime: s.finish,\n\t\t}\n\t}\n\t// non-leaf node\n\tchildTotal := children[0].Total\n\tparentTotal := s.total * childTotal\n\tparentCount := s.count * childTotal\n\tfor _, child := range children {\n\t\tparentCount += childTotal * child.Count / child.Total\n\t}\n\treturn Progress{\n\t\tName:       s.name,\n\t\tStatus:     s.status,\n\t\tError:      s.err,\n\t\tCount:      parentCount,\n\t\tTotal:      parentTotal,\n\t\tStartTime:  s.start,\n\t\tFinishTime: s.finish,\n\t}\n}\n\nfunc Start(ctx context.Context, name string, total int) (context.Context, *Span) {\n\tchildSpan := &Span{\n\t\tname:   name,\n\t\tstatus: StatusRunning,\n\t\ttotal:  total,\n\t\tcount:  0,\n\t\tstart:  time.Now(),\n\t}\n\tif ctx == nil {\n\t\treturn nil, childSpan\n\t}\n\tspan, ok := (ctx).Value(spanKeyName).(*Span)\n\tif !ok {\n\t\treturn ctx, childSpan\n\t}\n\tspan.children.Store(name, childSpan)\n\treturn context.WithValue(ctx, spanKeyName, childSpan), childSpan\n}\n\nfunc Fail(ctx context.Context, err error) {\n\tspan, ok := (ctx).Value(spanKeyName).(*Span)\n\tif !ok {\n\t\treturn\n\t}\n\tspan.Fail(err)\n}\n\ntype Progress struct {\n\tTracer     string\n\tName       string\n\tStatus     Status\n\tError      string\n\tCount      int\n\tTotal      int\n\tStartTime  time.Time\n\tFinishTime time.Time\n}\n\nfunc DecodeProgress(in *protocol.PushProgressRequest) []Progress {\n\tvar progressList []Progress\n\tfor _, p := range in.Progress {\n\t\tprogressList = append(progressList, Progress{\n\t\t\tTracer:     p.GetTracer(),\n\t\t\tName:       p.GetName(),\n\t\t\tStatus:     Status(p.GetStatus()),\n\t\t\tCount:      int(p.GetCount()),\n\t\t\tTotal:      int(p.GetTotal()),\n\t\t\tStartTime:  time.UnixMilli(p.GetStartTime()),\n\t\t\tFinishTime: time.UnixMilli(p.GetFinishTime()),\n\t\t})\n\t}\n\treturn progressList\n}\n\nfunc EncodeProgress(progressList []Progress) *protocol.PushProgressRequest {\n\tvar pbList []*protocol.Progress\n\tfor _, p := range progressList {\n\t\tpbList = append(pbList, &protocol.Progress{\n\t\t\tTracer:     p.Tracer,\n\t\t\tName:       p.Name,\n\t\t\tStatus:     string(p.Status),\n\t\t\tCount:      int64(p.Count),\n\t\t\tTotal:      int64(p.Total),\n\t\t\tStartTime:  p.StartTime.UnixMilli(),\n\t\t\tFinishTime: p.FinishTime.UnixMilli(),\n\t\t})\n\t}\n\treturn &protocol.PushProgressRequest{\n\t\tProgress: pbList,\n\t}\n}\n"
  },
  {
    "path": "common/monitor/progress_test.go",
    "content": "// Copyright 2023 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage monitor\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\ntype ProgressTestSuite struct {\n\tsuite.Suite\n\ttracer Monitor\n}\n\nfunc (suite *ProgressTestSuite) SetupTest() {\n\tsuite.tracer = Monitor{}\n}\n\nfunc TestProgressTestSuite(t *testing.T) {\n\tsuite.Run(t, new(ProgressTestSuite))\n}\n\nfunc TestEncodeDecode(t *testing.T) {\n\tprogressList := []Progress{\n\t\t{\n\t\t\tTracer:     \"tracer\",\n\t\t\tName:       \"a\",\n\t\t\tTotal:      100,\n\t\t\tCount:      50,\n\t\t\tStatus:     StatusRunning,\n\t\t\tStartTime:  time.Date(2018, time.January, 1, 0, 0, 0, 0, time.Local),\n\t\t\tFinishTime: time.Date(2018, time.January, 2, 0, 0, 0, 0, time.Local),\n\t\t},\n\t\t{\n\t\t\tTracer:     \"tracer\",\n\t\t\tName:       \"b\",\n\t\t\tTotal:      100,\n\t\t\tCount:      50,\n\t\t\tStatus:     StatusRunning,\n\t\t\tStartTime:  time.Date(2018, time.January, 1, 0, 0, 0, 0, time.Local),\n\t\t\tFinishTime: time.Date(2018, time.January, 2, 0, 0, 0, 0, time.Local),\n\t\t},\n\t}\n\tpb := EncodeProgress(progressList)\n\tassert.Equal(t, progressList, DecodeProgress(pb))\n}\n"
  },
  {
    "path": "common/nn/functions.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage nn\n\nimport (\n\t\"fmt\"\n)\n\nfunc Neg(x *Tensor) *Tensor {\n\treturn apply(&neg{}, x)\n}\n\n// Add returns the element-wise sum of two tensors. The shape of the second tensor must be a suffix sequence of the shape of the first tensor.\nfunc Add(x0 *Tensor, x ...*Tensor) *Tensor {\n\toutput := x0\n\tfor _, x1 := range x {\n\t\tif len(x0.shape) < len(x1.shape) {\n\t\t\toutput, x1 = x1, output\n\t\t}\n\t\tfor i := 0; i < len(x1.shape); i++ {\n\t\t\tif x0.shape[len(x0.shape)-len(x1.shape)+i] != x1.shape[i] {\n\t\t\t\tpanic(fmt.Sprintf(\"the shape of one tensor %v must be a suffix sequence of the shape of the other tensor %v\", x0.shape, x1.shape))\n\t\t\t}\n\t\t}\n\t\toutput = apply(&add{}, output, x1)\n\t}\n\treturn output\n}\n\n// Sub returns the element-wise difference of two tensors. The shape of the second tensor must be a suffix sequence of the shape of the first tensor.\nfunc Sub(x0, x1 *Tensor) *Tensor {\n\tif len(x0.shape) < len(x1.shape) {\n\t\tpanic(fmt.Sprintf(\"the shape of the second tensor %v must be a suffix sequence of the shape of the first tensor %v\", x1.shape, x0.shape))\n\t}\n\tfor i := 0; i < len(x1.shape); i++ {\n\t\tif x0.shape[len(x0.shape)-len(x1.shape)+i] != x1.shape[i] {\n\t\t\tpanic(\"the shape of the second tensor must be a suffix sequence of the shape of the first tensor\")\n\t\t}\n\t}\n\treturn apply(&sub{}, x0, x1)\n}\n\n// Mul returns the element-wise product of two tensors. The shape of the second tensor must be a suffix sequence of the shape of the first tensor.\nfunc Mul(x0, x1 *Tensor) *Tensor {\n\tif len(x0.shape) < len(x1.shape) {\n\t\tx0, x1 = x1, x0\n\t}\n\tfor i := 0; i < len(x1.shape); i++ {\n\t\tif x0.shape[len(x0.shape)-len(x1.shape)+i] != x1.shape[i] {\n\t\t\tpanic(fmt.Sprintf(\"the shape of the second tensor %v must be a suffix sequence of the shape of the first tensor %v\", x1.shape, x0.shape))\n\t\t}\n\t}\n\treturn apply(&mul{}, x0, x1)\n}\n\n// Div returns the element-wise division of two tensors. The shape of the second tensor must be a suffix sequence of the shape of the first tensor.\nfunc Div(x0, x1 *Tensor) *Tensor {\n\tif len(x0.shape) < len(x1.shape) {\n\t\tpanic(fmt.Sprintf(\"the shape of the second tensor %v must be a suffix sequence of the shape of the first tensor %v\", x1.shape, x0.shape))\n\t}\n\tfor i := 0; i < len(x1.shape); i++ {\n\t\tif x0.shape[len(x0.shape)-len(x1.shape)+i] != x1.shape[i] {\n\t\t\tpanic(\"the shape of the second tensor must be a suffix sequence of the shape of the first tensor\")\n\t\t}\n\t}\n\treturn apply(&div{}, x0, x1)\n}\n\n// Square returns the element-wise square of a tensor.\nfunc Square(x *Tensor) *Tensor {\n\treturn apply(&square{}, x)\n}\n\n// Pow returns the element-wise power of a tensor. The shape of the second tensor must be a suffix sequence of the shape of the first tensor.\nfunc Pow(x *Tensor, n *Tensor) *Tensor {\n\tif len(x.shape) < len(n.shape) {\n\t\tpanic(\"the shape of the second tensor must be a suffix sequence of the shape of the first tensor\")\n\t}\n\tfor i := 0; i < len(n.shape); i++ {\n\t\tif n.shape[len(n.shape)-len(x.shape)+i] != x.shape[i] {\n\t\t\tpanic(\"the shape of the second tensor must be a suffix sequence of the shape of the first tensor\")\n\t\t}\n\t}\n\treturn apply(&pow{}, x, n)\n}\n\n// Exp returns the element-wise exponential of a tensor.\nfunc Exp(x *Tensor) *Tensor {\n\treturn apply(&exp{}, x)\n}\n\n// Log returns the element-wise natural logarithm of a tensor.\nfunc Log(x *Tensor) *Tensor {\n\treturn apply(&log{}, x)\n}\n\n// Sin returns the element-wise sine of a tensor.\nfunc Sin(x *Tensor) *Tensor {\n\treturn apply(&sin{}, x)\n}\n\nfunc Cos(x *Tensor) *Tensor {\n\treturn apply(&cos{}, x)\n}\n\nfunc Abs(x *Tensor) *Tensor {\n\treturn apply(&abs{}, x)\n}\n\n// Sum returns the sum of all elements in a tensor.\nfunc Sum(x *Tensor, along ...int) *Tensor {\n\tif len(along) > 1 {\n\t\tpanic(\"only one along is allowed\")\n\t} else if len(along) == 1 {\n\t\treturn apply(&partialSum{along: int64(along[0])}, x)\n\t}\n\treturn apply(&sum{}, x)\n}\n\n// Mean returns the mean of all elements in a tensor.\nfunc Mean(x *Tensor) *Tensor {\n\treturn apply(&mean{}, x)\n}\n\nfunc MatMul(x, y *Tensor, transpose1, transpose2 bool, jobs int) *Tensor {\n\top := &matMul{\n\t\ttranspose1: transpose1,\n\t\ttranspose2: transpose2,\n\t\tjobs:       jobs,\n\t}\n\treturn apply(op, x, y)\n}\n\nfunc BMM(x, y *Tensor, transpose1, transpose2 bool, jobs int) *Tensor {\n\top := &batchMatMul{\n\t\ttranspose1: transpose1,\n\t\ttranspose2: transpose2,\n\t\tjobs:       jobs,\n\t}\n\treturn apply(op, x, y)\n}\n\nfunc Broadcast(x *Tensor, shape ...int) *Tensor {\n\treturn apply(&broadcast{shape: shape}, x)\n}\n\nfunc Flatten(x *Tensor) *Tensor {\n\treturn apply(&flatten{}, x)\n}\n\nfunc Reshape(x *Tensor, shape ...int) *Tensor {\n\tsize1 := 1\n\tfor i := range x.shape {\n\t\tsize1 *= x.shape[i]\n\t}\n\tsize2 := 1\n\tfor i := range shape {\n\t\tsize2 *= shape[i]\n\t}\n\tif size1 != size2 {\n\t\tpanic(\"the size of the tensor must be equal to the size of the new shape\")\n\t}\n\treturn apply(&reshape{shape: shape}, x)\n}\n\nfunc Embedding(w, x *Tensor) *Tensor {\n\treturn apply(&embedding{}, w, x)\n}\n\nfunc Sigmoid(x *Tensor) *Tensor {\n\treturn apply(&sigmoid{}, x)\n}\n\nfunc ReLu(x *Tensor) *Tensor {\n\treturn apply(&relu{}, x)\n}\n\nfunc Softmax(x *Tensor, axis int) *Tensor {\n\treturn apply(&softmax{axis: axis}, x)\n}\n\nfunc MeanSquareError(x, y *Tensor) *Tensor {\n\treturn Mean(Square(Sub(x, y)))\n}\n\nfunc SoftmaxCrossEntropy(x, y *Tensor) *Tensor {\n\tif len(x.shape) != 2 {\n\t\tpanic(\"the shape of the first tensor must be 2-D\")\n\t}\n\tif len(y.shape) != 1 {\n\t\tpanic(\"the shape of the second tensor must be 1-D\")\n\t}\n\tif x.shape[0] != y.shape[0] {\n\t\tpanic(\"the size of the first tensor must be equal to the size of the second tensor\")\n\t}\n\treturn apply(&softmaxCrossEntropy{}, x, y)\n}\n\n// BCEWithLogits calculates the binary cross-entropy loss between target and prediction\n// with logits. This implementation is numerically stable.\n// It is equivalent to the formula:\n//\n//\tmax(prediction, 0) - prediction*y + log(1 + exp(-|prediction|))\n//\n// where y = (target + 1) / 2, target is -1 or 1.\nfunc BCEWithLogits(target, prediction, weights *Tensor) *Tensor {\n\t// To prevent overflow, we use the mathematically equivalent and more stable formula.\n\t// This avoids calculating exp(x) where x is a large positive number.\n\n\t// term1 = max(prediction, 0)\n\tterm1 := ReLu(prediction)\n\n\t// y = (target + 1) / 2\n\ty := Div(Add(NewScalar(1), target), NewScalar(2))\n\n\t// term2 = prediction * y\n\tterm2 := Mul(prediction, y)\n\n\t// term3 = log(1 + exp(-|prediction|))\n\tabsPrediction := Abs(prediction)\n\texpTerm := Exp(Neg(absPrediction))\n\tlogTerm := Log(Add(NewScalar(1), expTerm))\n\n\t// loss = max(prediction, 0) - prediction*y + log(1 + exp(-|prediction|))\n\tloss := Add(Sub(term1, term2), logTerm)\n\n\tif weights != nil {\n\t\tloss = Mul(loss, weights)\n\t}\n\treturn Mean(loss)\n}\n"
  },
  {
    "path": "common/nn/layers.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage nn\n\nimport (\n\t\"io\"\n\t\"reflect\"\n\t\"strconv\"\n\n\t\"github.com/chewxy/math32\"\n\t\"github.com/gorse-io/gorse/protocol\"\n\t\"github.com/juju/errors\"\n\t\"github.com/matttproud/golang_protobuf_extensions/pbutil\"\n)\n\ntype Layer interface {\n\tParameters() []*Tensor\n\tForward(x *Tensor) *Tensor\n\tSetJobs(jobs int)\n}\n\ntype Model Layer\n\ntype LinearLayer struct {\n\tW    *Tensor\n\tB    *Tensor\n\tjobs int\n}\n\nfunc NewLinear(in, out int) Layer {\n\tbound := 1.0 / math32.Sqrt(float32(in))\n\treturn &LinearLayer{\n\t\tW: Uniform(-bound, bound, in, out),\n\t\tB: Zeros(out),\n\t}\n}\n\nfunc (l *LinearLayer) Forward(x *Tensor) *Tensor {\n\treturn Add(MatMul(x, l.W, false, false, l.jobs), l.B)\n}\n\nfunc (l *LinearLayer) Parameters() []*Tensor {\n\treturn []*Tensor{l.W, l.B}\n}\n\nfunc (l *LinearLayer) SetJobs(jobs int) {\n\tl.jobs = max(1, jobs)\n}\n\ntype flattenLayer struct{}\n\nfunc NewFlatten() Layer {\n\treturn &flattenLayer{}\n}\n\nfunc (f *flattenLayer) Parameters() []*Tensor {\n\treturn nil\n}\n\nfunc (f *flattenLayer) Forward(x *Tensor) *Tensor {\n\treturn Flatten(x)\n}\n\nfunc (f *flattenLayer) SetJobs(int) {}\n\ntype EmbeddingLayer struct {\n\tW *Tensor\n}\n\nfunc NewEmbedding(n int, shape ...int) Layer {\n\twShape := append([]int{n}, shape...)\n\treturn &EmbeddingLayer{\n\t\tW: Normal(0, 0.01, wShape...),\n\t}\n}\n\nfunc (e *EmbeddingLayer) Parameters() []*Tensor {\n\treturn []*Tensor{e.W}\n}\n\nfunc (e *EmbeddingLayer) Forward(x *Tensor) *Tensor {\n\treturn Embedding(e.W, x)\n}\n\nfunc (e *EmbeddingLayer) SetJobs(int) {}\n\ntype sigmoidLayer struct{}\n\nfunc NewSigmoid() Layer {\n\treturn &sigmoidLayer{}\n}\n\nfunc (s *sigmoidLayer) Parameters() []*Tensor {\n\treturn nil\n}\n\nfunc (s *sigmoidLayer) Forward(x *Tensor) *Tensor {\n\treturn Sigmoid(x)\n}\n\nfunc (s *sigmoidLayer) SetJobs(int) {}\n\ntype reluLayer struct{}\n\nfunc NewReLU() Layer {\n\treturn &reluLayer{}\n}\n\nfunc (r *reluLayer) Parameters() []*Tensor {\n\treturn nil\n}\n\nfunc (r *reluLayer) Forward(x *Tensor) *Tensor {\n\treturn ReLu(x)\n}\n\nfunc (r *reluLayer) SetJobs(int) {}\n\ntype Sequential struct {\n\tLayers []Layer\n}\n\nfunc NewSequential(layers ...Layer) Model {\n\treturn &Sequential{Layers: layers}\n}\n\nfunc (s *Sequential) Parameters() []*Tensor {\n\tvar params []*Tensor\n\tfor _, l := range s.Layers {\n\t\tparams = append(params, l.Parameters()...)\n\t}\n\treturn params\n}\n\nfunc (s *Sequential) Forward(x *Tensor) *Tensor {\n\tfor _, l := range s.Layers {\n\t\tx = l.Forward(x)\n\t}\n\treturn x\n}\n\nfunc (s *Sequential) SetJobs(jobs int) {\n\tfor _, l := range s.Layers {\n\t\tl.SetJobs(jobs)\n\t}\n}\n\ntype Attention struct {\n\tW    Layer\n\tH    *Tensor\n\tjobs int\n}\n\nfunc NewAttention(dimensions, k int) *Attention {\n\treturn &Attention{\n\t\tW: NewLinear(dimensions, k),\n\t\tH: Normal(0, 0.01, k, dimensions),\n\t}\n}\n\nfunc (a *Attention) Parameters() []*Tensor {\n\tvar params []*Tensor\n\tparams = append(params, a.H)\n\tparams = append(params, a.W.Parameters()...)\n\treturn params\n}\n\nfunc (a *Attention) Forward(x *Tensor) *Tensor {\n\treturn Mul(\n\t\tSoftmax(MatMul(ReLu(a.W.Forward(x)), a.H, false, false, a.jobs), 1),\n\t\tx,\n\t)\n}\n\nfunc (a *Attention) SetJobs(jobs int) {\n\ta.W.SetJobs(jobs)\n\ta.jobs = max(1, jobs)\n}\n\nfunc Save(o any, w io.Writer) error {\n\tvar save func(o any, key []string) error\n\tsave = func(o any, key []string) error {\n\t\tswitch typed := o.(type) {\n\t\tcase *Tensor:\n\t\t\tpb := typed.toPB()\n\t\t\tpb.Key = key\n\t\t\t_, err := pbutil.WriteDelimited(w, pb)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tdefault:\n\t\t\ttp := reflect.TypeOf(o)\n\t\t\tif tp.Kind() == reflect.Ptr {\n\t\t\t\treturn save(reflect.ValueOf(o).Elem().Interface(), key)\n\t\t\t} else if tp.Kind() == reflect.Struct {\n\t\t\t\tfor i := 0; i < tp.NumField(); i++ {\n\t\t\t\t\tfield := tp.Field(i)\n\t\t\t\t\tif field.IsExported() {\n\t\t\t\t\t\tnewKey := make([]string, len(key))\n\t\t\t\t\t\tcopy(newKey, key)\n\t\t\t\t\t\tnewKey = append(newKey, field.Name)\n\t\t\t\t\t\tif err := save(reflect.ValueOf(o).Field(i).Interface(), newKey); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if tp.Kind() == reflect.Slice {\n\t\t\t\tfor i := 0; i < reflect.ValueOf(o).Len(); i++ {\n\t\t\t\t\tnewKey := make([]string, len(key))\n\t\t\t\t\tcopy(newKey, key)\n\t\t\t\t\tnewKey = append(newKey, strconv.Itoa(i))\n\t\t\t\t\tif err := save(reflect.ValueOf(o).Index(i).Interface(), newKey); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn errors.New(\"unexpected type\")\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\treturn save(o, nil)\n}\n\nfunc Load(o any, r io.Reader) error {\n\tvar place func(o any, key []string, pb *protocol.Tensor) error\n\tplace = func(o any, key []string, pb *protocol.Tensor) error {\n\t\tswitch typed := o.(type) {\n\t\tcase *Tensor:\n\t\t\ttyped.fromPB(pb)\n\t\tdefault:\n\t\t\ttp := reflect.TypeOf(o)\n\t\t\tif tp.Kind() == reflect.Ptr {\n\t\t\t\treturn place(reflect.ValueOf(o).Elem().Interface(), key, pb)\n\t\t\t} else if tp.Kind() == reflect.Struct {\n\t\t\t\tfield := reflect.ValueOf(o).FieldByName(key[0])\n\t\t\t\tif field.IsValid() {\n\t\t\t\t\tif err := place(field.Interface(), key[1:], pb); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if tp.Kind() == reflect.Slice {\n\t\t\t\tindex, err := strconv.Atoi(key[0])\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\telem := reflect.ValueOf(o).Index(index)\n\t\t\t\tif elem.IsValid() {\n\t\t\t\t\tif err := place(elem.Interface(), key[1:], pb); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn errors.New(\"unexpected type\")\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Read data\n\tfor {\n\t\tpb := new(protocol.Tensor)\n\t\tif _, err := pbutil.ReadDelimited(r, pb); err != nil {\n\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tif err := place(o, pb.Key, pb); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "common/nn/nn_test.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage nn\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/csv\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/chewxy/math32\"\n\t\"github.com/gorse-io/gorse/common/datautil\"\n\t\"github.com/gorse-io/gorse/common/util\"\n\t\"github.com/samber/lo\"\n\t\"github.com/schollz/progressbar/v3\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"golang.org/x/sys/cpu\"\n)\n\nfunc TestLinearRegression(t *testing.T) {\n\tx := Rand(100, 1)\n\ty := Add(Rand(100, 1), NewScalar(5), Mul(NewScalar(2), x))\n\n\tw := Zeros(1, 1)\n\tb := Zeros(1)\n\tpredict := func(x *Tensor) *Tensor { return Add(MatMul(x, w, false, false, 0), b) }\n\n\tlr := float32(0.1)\n\tfor i := 0; i < 100; i++ {\n\t\tyPred := predict(x)\n\t\tloss := MeanSquareError(y, yPred)\n\n\t\tw.grad = nil\n\t\tb.grad = nil\n\t\tloss.Backward()\n\n\t\tw.sub(w.grad.mul(NewScalar(lr)))\n\t\tb.sub(b.grad.mul(NewScalar(lr)))\n\t}\n\n\tassert.Equal(t, []int{1, 1}, w.shape)\n\tassert.InDelta(t, float64(2), w.data[0], 0.6)\n\tassert.Equal(t, []int{1}, b.shape)\n\tassert.InDelta(t, float64(5), b.data[0], 0.6)\n}\n\nfunc TestNeuralNetwork(t *testing.T) {\n\tx := Rand(100, 1)\n\ty := Add(Rand(100, 1), Sin(Mul(x, NewScalar(2*math32.Pi))))\n\n\tmodel := NewSequential(\n\t\tNewLinear(1, 10),\n\t\tNewSigmoid(),\n\t\tNewLinear(10, 1),\n\t)\n\tNormalInit(model.(*Sequential).Layers[0].(*LinearLayer).W, 0, 0.01)\n\tNormalInit(model.(*Sequential).Layers[2].(*LinearLayer).W, 0, 0.01)\n\toptimizer := NewSGD(model.Parameters(), 0.2)\n\n\tvar l float32\n\tfor i := 0; i < 10000; i++ {\n\t\tyPred := model.Forward(x)\n\t\tloss := MeanSquareError(y, yPred)\n\n\t\toptimizer.ZeroGrad()\n\t\tloss.Backward()\n\n\t\toptimizer.Step()\n\t\tl = loss.data[0]\n\t}\n\tassert.InDelta(t, float64(0), l, 0.2)\n}\n\nfunc iris() (*Tensor, *Tensor, error) {\n\t// Download dataset\n\tpath, err := datautil.DownloadAndUnzip(\"iris\")\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tdataFile := filepath.Join(path, \"iris.data\")\n\t// Load data\n\tf, err := os.Open(dataFile)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\treader := csv.NewReader(f)\n\trows, err := reader.ReadAll()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\t// Parse data\n\tdata := make([]float32, len(rows)*4)\n\ttarget := make([]float32, len(rows))\n\ttypes := make(map[string]int)\n\tfor i, row := range rows {\n\t\tfor j, cell := range row[:4] {\n\t\t\tdata[i*4+j], err = util.ParseFloat[float32](cell)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t}\n\t\tif _, exist := types[row[4]]; !exist {\n\t\t\ttypes[row[4]] = len(types)\n\t\t}\n\t\ttarget[i] = float32(types[row[4]])\n\t}\n\treturn NewTensor(data, len(rows), 4), NewTensor(target, len(rows)), nil\n}\n\nfunc TestIris(t *testing.T) {\n\tx, y, err := iris()\n\tassert.NoError(t, err)\n\n\tmodel := NewSequential(\n\t\tNewLinear(4, 100),\n\t\tNewLinear(100, 100),\n\t\tNewLinear(100, 3),\n\t)\n\toptimizer := NewAdam(model.Parameters(), 0.01)\n\n\tvar l float32\n\tfor i := 0; i < 1000; i++ {\n\t\tyPred := model.Forward(x)\n\t\tloss := SoftmaxCrossEntropy(yPred, y)\n\n\t\toptimizer.ZeroGrad()\n\t\tloss.Backward()\n\n\t\toptimizer.Step()\n\t\tl = loss.data[0]\n\t}\n\tassert.InDelta(t, float32(0), l, 0.1)\n}\n\nfunc mnist() (lo.Tuple2[*Tensor, *Tensor], lo.Tuple2[*Tensor, *Tensor], error) {\n\tvar train, test lo.Tuple2[*Tensor, *Tensor]\n\t// Download and unzip dataset\n\tpath, err := datautil.DownloadAndUnzip(\"mnist\")\n\tif err != nil {\n\t\treturn train, test, err\n\t}\n\t// Open dataset\n\ttrain.A, train.B, err = openMNISTFile(filepath.Join(path, \"train.libfm\"))\n\tif err != nil {\n\t\treturn train, test, err\n\t}\n\ttest.A, test.B, err = openMNISTFile(filepath.Join(path, \"test.libfm\"))\n\tif err != nil {\n\t\treturn train, test, err\n\t}\n\treturn train, test, nil\n}\n\nfunc openMNISTFile(path string) (*Tensor, *Tensor, error) {\n\t// Open file\n\tf, err := os.Open(path)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tdefer f.Close()\n\t// Read data line by line\n\tvar (\n\t\timages []float32\n\t\tlabels []float32\n\t)\n\tscanner := bufio.NewScanner(f)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tsplits := strings.Split(line, \" \")\n\t\t// Parse label\n\t\tlabel, err := util.ParseFloat[float32](splits[0])\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\tlabels = append(labels, label)\n\t\t// Parse image\n\t\timage := make([]float32, 784)\n\t\tfor _, split := range splits[1:] {\n\t\t\tkv := strings.Split(split, \":\")\n\t\t\tindex, err := strconv.Atoi(kv[0])\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t\tvalue, err := util.ParseFloat[float32](kv[1])\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t\timage[index] = value\n\t\t}\n\t\timages = append(images, image...)\n\t}\n\treturn NewTensor(images, len(labels), 784), NewTensor(labels, len(labels)), nil\n}\n\nfunc accuracy(prediction, target *Tensor) float32 {\n\tvar precision float32\n\tfor i, gt := range target.data {\n\t\tif prediction.Slice(i, i+1).argmax()[1] == int(gt) {\n\t\t\tprecision += 1\n\t\t}\n\t}\n\tprecision /= float32(len(target.data))\n\treturn precision\n}\n\nfunc TestMNIST(t *testing.T) {\n\tif (runtime.GOOS != \"darwin\" || runtime.GOARCH != \"arm64\") && !cpu.X86.HasAVX512F {\n\t\t// Since the test takes a long time, we run the test only in development environment.\n\t\t// 1. Mac with Apple Silicon.\n\t\t// 2. x86 CPU with AVX512 support.\n\t\tt.Skip(\"Skip test on non-development environment.\")\n\t}\n\n\ttrain, test, err := mnist()\n\tassert.NoError(t, err)\n\n\tmodel := NewSequential(\n\t\tNewLinear(784, 1000),\n\t\tNewReLU(),\n\t\tNewLinear(1000, 10),\n\t)\n\tmodel.SetJobs(runtime.NumCPU())\n\toptimizer := NewAdam(model.Parameters(), 0.001)\n\n\tconst (\n\t\tbatchSize = 1000\n\t\tnumEpoch  = 5\n\t)\n\tfor i := 0; i < numEpoch; i++ {\n\t\tstartTime := time.Now()\n\t\tsumLoss, sumAcc := float32(0), float32(0)\n\t\tbar := progressbar.Default(int64(train.A.shape[0]), fmt.Sprintf(\"Epoch %v/%v\", i+1, numEpoch))\n\t\tfor j := 0; j < train.A.shape[0]; j += batchSize {\n\t\t\txBatch := train.A.Slice(j, j+batchSize)\n\t\t\tyBatch := train.B.Slice(j, j+batchSize)\n\n\t\t\tyPred := model.Forward(xBatch)\n\t\t\tloss := SoftmaxCrossEntropy(yPred, yBatch)\n\n\t\t\toptimizer.ZeroGrad()\n\t\t\tloss.Backward()\n\n\t\t\toptimizer.Step()\n\t\t\tsumLoss += loss.data[0]\n\t\t\tsumAcc += accuracy(yPred, yBatch)\n\t\t\tassert.NoError(t, bar.Add(batchSize))\n\t\t}\n\t\tsumLoss /= float32(train.A.shape[0] / batchSize)\n\t\tsumAcc /= float32(train.A.shape[0] / batchSize)\n\t\tassert.NoError(t, bar.Finish())\n\t\tfmt.Println(\"Duration:\", time.Since(startTime), \"Loss:\", sumLoss, \"Accuracy:\", sumAcc)\n\t}\n\n\tSetInferenceMode(true)\n\ttestAcc := accuracy(model.Forward(test.A), test.B)\n\tSetInferenceMode(false)\n\tfmt.Println(\"Test Accuracy:\", testAcc)\n\tassert.Greater(t, float64(testAcc), 0.95)\n}\n\nfunc spiral() (*Tensor, *Tensor, error) {\n\tnumData, numClass, inputDim := 100, 3, 2\n\tdataSize := numClass * numData\n\tx := Zeros(dataSize, inputDim)\n\tt := Zeros(dataSize)\n\n\tfor j := 0; j < numClass; j++ {\n\t\tfor i := 0; i < numData; i++ {\n\t\t\trate := float32(i) / float32(numData)\n\t\t\tradius := 1.0 * rate\n\t\t\ttheta := float32(j)*4.0 + 4.0*rate + float32(rand.NormFloat64())*0.2\n\t\t\tix := numData*j + i\n\t\t\tx.data[ix*inputDim] = radius * math32.Sin(theta)\n\t\t\tx.data[ix*inputDim+1] = radius * math32.Cos(theta)\n\t\t\tt.data[ix] = float32(j)\n\t\t}\n\t}\n\n\tindices := rand.Perm(dataSize)\n\tx = x.SliceIndices(indices...)\n\tt = t.SliceIndices(indices...)\n\treturn x, t, nil\n}\n\nfunc TestSaveAndLoad(t *testing.T) {\n\tx, y, err := spiral()\n\tassert.NoError(t, err)\n\n\tmodel := NewSequential(\n\t\tNewLinear(2, 10),\n\t\tNewSigmoid(),\n\t\tNewLinear(10, 3),\n\t)\n\toptimizer := NewAdam(model.Parameters(), 0.01)\n\n\tvar expected float32\n\tfor i := 0; i < 300; i++ {\n\t\tyPred := model.Forward(x)\n\t\tloss := SoftmaxCrossEntropy(yPred, y)\n\n\t\toptimizer.ZeroGrad()\n\t\tloss.Backward()\n\n\t\toptimizer.Step()\n\t\texpected = loss.data[0]\n\t}\n\n\tbuffer := bytes.NewBuffer(nil)\n\terr = Save(model, buffer)\n\tassert.NoError(t, err)\n\tmodelLoaded := NewSequential(\n\t\tNewLinear(2, 10),\n\t\tNewSigmoid(),\n\t\tNewLinear(10, 3),\n\t)\n\terr = Load(modelLoaded, buffer)\n\tassert.NoError(t, err)\n\tyPred := modelLoaded.Forward(x)\n\tloss := SoftmaxCrossEntropy(yPred, y)\n\tassert.InDelta(t, float64(expected), float64(loss.data[0]), 0.01)\n}\n"
  },
  {
    "path": "common/nn/op.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage nn\n\nimport (\n\t\"weak\"\n\n\t\"github.com/chewxy/math32\"\n\t\"github.com/gorse-io/gorse/common/floats\"\n)\n\ntype op interface {\n\tString() string\n\tforward(inputs ...*Tensor) *Tensor\n\tbackward(dy *Tensor) []*Tensor\n\tinputsAndOutput() ([]*Tensor, *Tensor)\n\tsetInputs(inputs ...*Tensor)\n\tsetOutput(y *Tensor)\n\tgeneration() int\n\tsetGeneration(gen int)\n}\n\ntype base struct {\n\tinputs []*Tensor\n\toutput weak.Pointer[Tensor]\n\tgen    int\n}\n\nfunc (b *base) inputsAndOutput() ([]*Tensor, *Tensor) {\n\treturn b.inputs, b.output.Value()\n}\n\nfunc (b *base) setInputs(inputs ...*Tensor) {\n\tb.inputs = inputs\n}\n\nfunc (b *base) setOutput(y *Tensor) {\n\tb.output = weak.Make(y)\n}\n\nfunc (b *base) generation() int {\n\treturn b.gen\n}\n\nfunc (b *base) setGeneration(gen int) {\n\tb.gen = gen\n}\n\nfunc apply[T op](f T, inputs ...*Tensor) *Tensor {\n\ty := f.forward(inputs...)\n\tf.setInputs(inputs...)\n\tf.setOutput(y)\n\tif !inferenceMode.Load() {\n\t\ty.op = f\n\t\tgen := 0\n\t\tfor _, x := range inputs {\n\t\t\tgen = max(gen, x.generation())\n\t\t}\n\t\tf.setGeneration(gen + 1)\n\t} else {\n\t\ty.op = nil\n\t}\n\treturn y\n}\n\ntype neg struct {\n\tbase\n}\n\nfunc (n *neg) String() string {\n\treturn \"Neg\"\n}\n\nfunc (n *neg) forward(inputs ...*Tensor) *Tensor {\n\ty := inputs[0].clone()\n\ty.neg()\n\treturn y\n}\n\nfunc (n *neg) backward(dy *Tensor) []*Tensor {\n\tdx := dy.clone()\n\tdx.neg()\n\treturn []*Tensor{dx}\n}\n\ntype add struct {\n\tbase\n}\n\nfunc (a *add) String() string {\n\treturn \"Add\"\n}\n\nfunc (a *add) forward(inputs ...*Tensor) *Tensor {\n\ty := inputs[0].clone()\n\ty.add(inputs[1])\n\treturn y\n}\n\nfunc (a *add) backward(dy *Tensor) []*Tensor {\n\tgx0 := dy.clone()\n\tgx1 := Zeros(a.inputs[1].shape...)\n\twSize := 1\n\tfor i := range gx1.shape {\n\t\twSize *= gx1.shape[i]\n\t}\n\tfor i := range dy.data {\n\t\tgx1.data[i%wSize] += dy.data[i]\n\t}\n\treturn []*Tensor{gx0, gx1}\n}\n\ntype sub struct {\n\tbase\n}\n\nfunc (s *sub) String() string {\n\treturn \"Sub\"\n}\n\nfunc (s *sub) forward(inputs ...*Tensor) *Tensor {\n\ty := inputs[0].clone()\n\ty.sub(inputs[1])\n\treturn y\n}\n\nfunc (s *sub) backward(dy *Tensor) []*Tensor {\n\tgx0 := dy.clone()\n\tgx1 := Zeros(s.inputs[1].shape...)\n\twSize := 1\n\tfor i := range gx1.shape {\n\t\twSize *= gx1.shape[i]\n\t}\n\tfor i := range dy.data {\n\t\tgx1.data[i%wSize] -= dy.data[i]\n\t}\n\treturn []*Tensor{gx0, gx1}\n}\n\ntype mul struct {\n\tbase\n}\n\nfunc (m *mul) String() string {\n\treturn \"Mul\"\n}\n\nfunc (m *mul) forward(inputs ...*Tensor) *Tensor {\n\ty := inputs[0].clone()\n\ty.mul(inputs[1])\n\treturn y\n}\n\nfunc (m *mul) backward(dy *Tensor) []*Tensor {\n\tgx0 := dy.clone()\n\tgx0.mul(m.inputs[1])\n\tgx1 := Zeros(m.inputs[1].shape...)\n\twSize := 1\n\tfor i := range gx1.shape {\n\t\twSize *= gx1.shape[i]\n\t}\n\tfor i := range dy.data {\n\t\tgx1.data[i%wSize] += dy.data[i] * m.inputs[0].data[i]\n\t}\n\treturn []*Tensor{gx0, gx1}\n}\n\ntype div struct {\n\tbase\n}\n\nfunc (d *div) String() string {\n\treturn \"Div\"\n}\n\nfunc (d *div) forward(inputs ...*Tensor) *Tensor {\n\ty := inputs[0].clone()\n\ty.div(inputs[1])\n\treturn y\n}\n\nfunc (d *div) backward(dy *Tensor) []*Tensor {\n\twSize := 1\n\tfor i := range d.inputs[1].shape {\n\t\twSize *= d.inputs[1].shape[i]\n\t}\n\tgx0 := Zeros(d.inputs[0].shape...)\n\tfor i := range dy.data {\n\t\tgx0.data[i] = dy.data[i] / d.inputs[1].data[i%wSize]\n\t}\n\tgx1 := Zeros(d.inputs[1].shape...)\n\tfor i := range dy.data {\n\t\tgx1.data[i%wSize] -= dy.data[i] * d.inputs[0].data[i] / d.inputs[1].data[i%wSize] / d.inputs[1].data[i%wSize]\n\t}\n\treturn []*Tensor{gx0, gx1}\n}\n\ntype sin struct {\n\tbase\n}\n\nfunc (s *sin) String() string {\n\treturn \"Sin\"\n}\n\nfunc (s *sin) forward(inputs ...*Tensor) *Tensor {\n\ty := inputs[0].clone()\n\ty.sin()\n\treturn y\n}\n\nfunc (s *sin) backward(dy *Tensor) []*Tensor {\n\tdx := s.inputs[0].clone()\n\tdx.cos()\n\tdx.mul(dy)\n\treturn []*Tensor{dx}\n}\n\ntype cos struct {\n\tbase\n}\n\nfunc (c *cos) String() string {\n\treturn \"Cos\"\n}\n\nfunc (c *cos) forward(inputs ...*Tensor) *Tensor {\n\ty := inputs[0].clone()\n\ty.cos()\n\treturn y\n}\n\nfunc (c *cos) backward(dy *Tensor) []*Tensor {\n\tdx := c.inputs[0].clone()\n\tdx.sin()\n\tdx.neg()\n\tdx.mul(dy)\n\treturn []*Tensor{dx}\n}\n\ntype square struct {\n\tbase\n}\n\nfunc (s *square) String() string {\n\treturn \"Square\"\n}\n\nfunc (s *square) forward(inputs ...*Tensor) *Tensor {\n\ty := inputs[0].clone()\n\ty.square()\n\treturn y\n}\n\nfunc (s *square) backward(dy *Tensor) []*Tensor {\n\tdx := s.inputs[0].clone()\n\tfloats.MulTo(dx.data, dy.data, dx.data)\n\tfloats.MulConst(dx.data, 2)\n\treturn []*Tensor{dx}\n}\n\ntype pow struct {\n\tbase\n}\n\nfunc (p *pow) String() string {\n\treturn \"Pow\"\n}\n\nfunc (p *pow) forward(inputs ...*Tensor) *Tensor {\n\ty := inputs[0].clone()\n\ty.pow(inputs[1])\n\treturn y\n}\n\nfunc (p *pow) backward(dy *Tensor) []*Tensor {\n\tdx0 := p.inputs[0].clone()\n\tdx0.pow(p.inputs[1])\n\tdx0.mul(p.inputs[1])\n\tdx0.div(p.inputs[0])\n\tdx0.mul(dy)\n\twSize := 1\n\tfor i := range p.inputs[1].shape {\n\t\twSize *= p.inputs[1].shape[i]\n\t}\n\tdx1 := Zeros(p.inputs[1].shape...)\n\tfor i := range dy.data {\n\t\tdx1.data[i%wSize] += dy.data[i] * p.output.Value().data[i] * math32.Log(p.inputs[0].data[i])\n\t}\n\treturn []*Tensor{dx0, dx1}\n}\n\ntype exp struct {\n\tbase\n}\n\nfunc (e *exp) String() string {\n\treturn \"Exp\"\n}\n\nfunc (e *exp) forward(inputs ...*Tensor) *Tensor {\n\ty := inputs[0].clone()\n\ty.exp()\n\treturn y\n}\n\nfunc (e *exp) backward(dy *Tensor) []*Tensor {\n\tdx := e.inputs[0].clone()\n\tdx.exp()\n\tdx.mul(dy)\n\treturn []*Tensor{dx}\n}\n\ntype log struct {\n\tbase\n}\n\nfunc (l *log) String() string {\n\treturn \"Log\"\n}\n\nfunc (l *log) forward(inputs ...*Tensor) *Tensor {\n\ty := inputs[0].clone()\n\ty.log()\n\treturn y\n}\n\nfunc (l *log) backward(dy *Tensor) []*Tensor {\n\tdx := dy.clone()\n\tdx.div(l.inputs[0])\n\treturn []*Tensor{dx}\n}\n\ntype abs struct {\n\tbase\n}\n\nfunc (a *abs) String() string {\n\treturn \"Abs\"\n}\n\nfunc (a *abs) forward(inputs ...*Tensor) *Tensor {\n\ty := inputs[0].clone()\n\tfor i := range y.data {\n\t\tif y.data[i] < 0 {\n\t\t\ty.data[i] = -y.data[i]\n\t\t}\n\t}\n\treturn y\n}\n\nfunc (a *abs) backward(dy *Tensor) []*Tensor {\n\tdx := dy.clone()\n\tfor i := range dx.data {\n\t\tif a.inputs[0].data[i] < 0 {\n\t\t\tdx.data[i] = -dx.data[i]\n\t\t}\n\t}\n\treturn []*Tensor{dx}\n}\n\ntype sum struct {\n\tbase\n}\n\nfunc (s *sum) String() string {\n\treturn \"Sum\"\n}\n\nfunc (s *sum) forward(inputs ...*Tensor) *Tensor {\n\tx := inputs[0]\n\ty := NewTensor([]float32{0})\n\tfor i := range x.data {\n\t\ty.data[0] += x.data[i]\n\t}\n\treturn y\n}\n\nfunc (s *sum) backward(dy *Tensor) []*Tensor {\n\tdx := Zeros(s.inputs[0].shape...)\n\tfor i := range dx.data {\n\t\tdx.data[i] = dy.data[0]\n\t}\n\treturn []*Tensor{dx}\n}\n\ntype partialSum struct {\n\tbase\n\talong int64\n}\n\nfunc (p *partialSum) String() string {\n\treturn \"Sum\"\n}\n\nfunc (p *partialSum) forward(inputs ...*Tensor) *Tensor {\n\tx := inputs[0]\n\t// Squash the shape.\n\ts1, s2, s3 := 1, 1, 1\n\tfor i := 0; i < len(x.shape); i++ {\n\t\tif int64(i) == p.along {\n\t\t\ts2 = x.shape[i]\n\t\t} else if int64(i) < p.along {\n\t\t\ts1 *= x.shape[i]\n\t\t} else {\n\t\t\ts3 *= x.shape[i]\n\t\t}\n\t}\n\t// Calculate the output size and shape.\n\toutputSize := s1 * s3\n\toutputShape := make([]int, 0)\n\tfor i := 0; i < len(x.shape); i++ {\n\t\tif int64(i) != p.along {\n\t\t\toutputShape = append(outputShape, x.shape[i])\n\t\t}\n\t}\n\t// Calculate the output.\n\ty := NewTensor(make([]float32, outputSize), outputShape...)\n\tfor i := 0; i < s1; i++ {\n\t\tfor j := 0; j < s2; j++ {\n\t\t\tfor k := 0; k < s3; k++ {\n\t\t\t\ty.data[i*s3+k] += x.data[i*s2*s3+j*s3+k]\n\t\t\t}\n\t\t}\n\t}\n\treturn y\n}\n\nfunc (p *partialSum) backward(dy *Tensor) []*Tensor {\n\tx := p.inputs[0]\n\t// Squash the shape.\n\ts1, s2, s3 := 1, 1, 1\n\tfor i := 0; i < len(x.shape); i++ {\n\t\tif int64(i) == p.along {\n\t\t\ts2 = x.shape[i]\n\t\t} else if int64(i) < p.along {\n\t\t\ts1 *= x.shape[i]\n\t\t} else {\n\t\t\ts3 *= x.shape[i]\n\t\t}\n\t}\n\t// Calculate the output.\n\tdx := Zeros(x.shape...)\n\tfor i := 0; i < s1; i++ {\n\t\tfor j := 0; j < s2; j++ {\n\t\t\tfor k := 0; k < s3; k++ {\n\t\t\t\tdx.data[i*s2*s3+j*s3+k] = dy.data[i*s3+k]\n\t\t\t}\n\t\t}\n\t}\n\treturn []*Tensor{dx}\n}\n\ntype mean struct {\n\tbase\n}\n\nfunc (m *mean) String() string {\n\treturn \"Mean\"\n}\n\nfunc (m *mean) forward(inputs ...*Tensor) *Tensor {\n\tx := inputs[0]\n\ty := NewTensor([]float32{0})\n\tfor i := range x.data {\n\t\ty.data[0] += x.data[i]\n\t}\n\ty.data[0] /= float32(len(x.data))\n\treturn y\n}\n\nfunc (m *mean) backward(dy *Tensor) []*Tensor {\n\tdx := Zeros(m.inputs[0].shape...)\n\tfor i := range dx.data {\n\t\tdx.data[i] = dy.data[0] / float32(len(dx.data))\n\t}\n\treturn []*Tensor{dx}\n}\n\ntype matMul struct {\n\tbase\n\ttranspose1 bool\n\ttranspose2 bool\n\tjobs       int\n}\n\nfunc (m *matMul) String() string {\n\treturn \"MatMul\"\n}\n\nfunc (m *matMul) forward(inputs ...*Tensor) *Tensor {\n\treturn inputs[0].matMul(inputs[1], m.transpose1, m.transpose2, m.jobs)\n}\n\nfunc (m *matMul) backward(dy *Tensor) []*Tensor {\n\tvar dx0, dx1 *Tensor\n\tif !m.transpose1 && !m.transpose2 { // y = x0 * x1\n\t\t// dx0 = dy * x1^T\n\t\tdx0 = dy.matMul(m.inputs[1], false, true, m.jobs)\n\t\t// dx1 = x0^T * dy\n\t\tdx1 = m.inputs[0].matMul(dy, true, false, m.jobs)\n\t} else if m.transpose1 && !m.transpose2 { // y = x0^T * x1\n\t\t// dx0 = dy * x1^T\n\t\tdx0 = m.inputs[1].matMul(dy, false, true, m.jobs)\n\t\t// dx1 = dy^T * x0\n\t\tdx1 = m.inputs[0].matMul(dy, false, false, m.jobs)\n\t} else if !m.transpose1 && m.transpose2 { // y = x0 * x1^T\n\t\t// dx0 = dy * x1\n\t\tdx0 = dy.matMul(m.inputs[1], false, false, m.jobs)\n\t\t// dx1 = dy^T * x0\n\t\tdx1 = dy.matMul(m.inputs[0], true, false, m.jobs)\n\t} else { // y = x0^T * x1^T\n\t\t// dx0 = x1 * dy^T\n\t\tdx0 = m.inputs[1].matMul(dy, true, true, m.jobs)\n\t\t// dx1 = dy * x0^T\n\t\tdx1 = dy.matMul(m.inputs[0], true, true, m.jobs)\n\t}\n\treturn []*Tensor{dx0, dx1}\n}\n\ntype batchMatMul struct {\n\tbase\n\ttranspose1 bool\n\ttranspose2 bool\n\tjobs       int\n}\n\nfunc (b *batchMatMul) String() string {\n\treturn \"BatchMatMul\"\n}\n\nfunc (b *batchMatMul) forward(inputs ...*Tensor) *Tensor {\n\treturn inputs[0].batchMatMul(inputs[1], b.transpose1, b.transpose2, b.jobs)\n}\n\nfunc (b *batchMatMul) backward(dy *Tensor) []*Tensor {\n\tvar dx0, dx1 *Tensor\n\tif !b.transpose1 && !b.transpose2 { // y = x0 * x1\n\t\t// dx0 = dy * x1^T\n\t\tdx0 = dy.batchMatMul(b.inputs[1], false, true, b.jobs)\n\t\t// dx1 = x0^T * dy\n\t\tdx1 = b.inputs[0].batchMatMul(dy, true, false, b.jobs)\n\t} else if b.transpose1 && !b.transpose2 { // y = x0^T * x1\n\t\t// dx0 = dy * x1^T\n\t\tdx0 = b.inputs[1].batchMatMul(dy, false, true, b.jobs)\n\t\t// dx1 = dy^T * x0\n\t\tdx1 = b.inputs[0].batchMatMul(dy, false, false, b.jobs)\n\t} else if !b.transpose1 && b.transpose2 { // y = x0 * x1^T\n\t\t// dx0 = dy * x1\n\t\tdx0 = dy.batchMatMul(b.inputs[1], false, false, b.jobs)\n\t\t// dx1 = dy^T * x0\n\t\tdx1 = dy.batchMatMul(b.inputs[0], true, false, b.jobs)\n\t} else { // y = x0^T * x1^T\n\t\t// dx0 = x1 * dy^T\n\t\tdx0 = b.inputs[1].batchMatMul(dy, true, true, b.jobs)\n\t\t// dx1 = dy * x0^T\n\t\tdx1 = dy.batchMatMul(b.inputs[0], true, true, b.jobs)\n\t}\n\treturn []*Tensor{dx0, dx1}\n}\n\ntype broadcast struct {\n\tbase\n\tshape []int\n}\n\nfunc (b *broadcast) String() string {\n\treturn \"Broadcast\"\n}\n\nfunc (b *broadcast) forward(inputs ...*Tensor) *Tensor {\n\tx := inputs[0]\n\t// Concatenate the shape\n\tshape := make([]int, len(x.shape))\n\tcopy(shape, x.shape)\n\tshape = append(shape, b.shape...)\n\tsize := 1\n\tfor i := range shape {\n\t\tsize *= shape[i]\n\t}\n\t// Create a new tensor with the new shape\n\ty := NewTensor(make([]float32, size), shape...)\n\twSize := 1\n\tfor i := range b.shape {\n\t\twSize *= b.shape[i]\n\t}\n\tfor i := range x.data {\n\t\tfor j := i * wSize; j < (i+1)*wSize; j++ {\n\t\t\ty.data[j] = x.data[i]\n\t\t}\n\t}\n\treturn y\n}\n\nfunc (b *broadcast) backward(dy *Tensor) []*Tensor {\n\tgx := Zeros(b.inputs[0].shape...)\n\twSize := 1\n\tfor i := range b.shape {\n\t\twSize *= b.shape[i]\n\t}\n\tfor i := range gx.data {\n\t\tfor j := i * wSize; j < (i+1)*wSize; j++ {\n\t\t\tgx.data[i] += dy.data[j]\n\t\t}\n\t}\n\treturn []*Tensor{gx}\n}\n\ntype flatten struct {\n\tbase\n}\n\nfunc (f *flatten) String() string {\n\treturn \"Flatten\"\n}\n\nfunc (f *flatten) forward(inputs ...*Tensor) *Tensor {\n\treturn NewTensor(inputs[0].data, len(inputs[0].data))\n}\n\nfunc (f *flatten) backward(dy *Tensor) []*Tensor {\n\treturn []*Tensor{NewTensor(dy.data, f.inputs[0].shape...)}\n}\n\ntype reshape struct {\n\tbase\n\tshape []int\n}\n\nfunc (r *reshape) String() string {\n\treturn \"Reshape\"\n}\n\nfunc (r *reshape) forward(inputs ...*Tensor) *Tensor {\n\treturn NewTensor(inputs[0].data, r.shape...)\n}\n\nfunc (r *reshape) backward(dy *Tensor) []*Tensor {\n\treturn []*Tensor{NewTensor(dy.data, r.inputs[0].shape...)}\n}\n\ntype embedding struct {\n\tbase\n}\n\nfunc (e *embedding) String() string {\n\treturn \"Embedding\"\n}\n\nfunc (e *embedding) forward(inputs ...*Tensor) *Tensor {\n\tw, x := inputs[0], inputs[1]\n\t// Calculate embedding size\n\tdim := 1\n\tfor i := 1; i < len(w.shape); i++ {\n\t\tdim *= w.shape[i]\n\t}\n\t// Calculate shape\n\tshape := make([]int, len(x.shape), len(x.shape)+1)\n\tcopy(shape, x.shape)\n\tshape = append(shape, w.shape[1:]...)\n\t// Calculate data size\n\tsize := 1\n\tfor _, s := range shape {\n\t\tsize *= s\n\t}\n\t// Create output tensor\n\tdata := make([]float32, size)\n\tfor i := 0; i < len(x.data); i++ {\n\t\tindex := int(x.data[i])\n\t\tcopy(data[i*dim:(i+1)*dim], w.data[index*dim:(index+1)*dim])\n\t}\n\treturn NewTensor(data, shape...)\n}\n\nfunc (e *embedding) backward(dy *Tensor) []*Tensor {\n\tw, x := e.inputs[0], e.inputs[1]\n\tdim := 1\n\tfor i := 1; i < len(w.shape); i++ {\n\t\tdim *= w.shape[i]\n\t}\n\tdw := Zeros(w.shape...)\n\tfor i := 0; i < len(x.data); i++ {\n\t\tindex := int(x.data[i])\n\t\tfor j := 0; j < dim; j++ {\n\t\t\tdw.data[index*dim+j] += dy.data[i*dim+j]\n\t\t}\n\t}\n\treturn []*Tensor{dw}\n}\n\ntype sigmoid struct {\n\tbase\n}\n\nfunc (s *sigmoid) String() string {\n\treturn \"Sigmoid\"\n}\n\nfunc (s *sigmoid) forward(inputs ...*Tensor) *Tensor {\n\t// y = tanh(x * 0.5) * 0.5 + 0.5\n\ty := inputs[0].clone()\n\ty.mul(NewScalar(0.5))\n\ty.tanh()\n\ty.mul(NewScalar(0.5))\n\ty.add(NewScalar(0.5))\n\treturn y\n}\n\nfunc (s *sigmoid) backward(dy *Tensor) []*Tensor {\n\t// dx = dy * y * (1 - y)\n\tdx := s.output.Value().clone()\n\tdx.neg()\n\tdx.add(NewScalar(1))\n\tdx.mul(s.output.Value())\n\tdx.mul(dy)\n\treturn []*Tensor{dx}\n}\n\ntype relu struct {\n\tbase\n}\n\nfunc (r *relu) String() string {\n\treturn \"ReLU\"\n}\n\nfunc (r *relu) forward(inputs ...*Tensor) *Tensor {\n\ty := inputs[0].clone()\n\ty.maximum(NewScalar(0))\n\treturn y\n}\n\nfunc (r *relu) backward(dy *Tensor) []*Tensor {\n\tx := r.inputs[0]\n\tdx := x.clone().gt(NewScalar(0)).mul(dy)\n\treturn []*Tensor{dx}\n}\n\ntype softmax struct {\n\tbase\n\taxis int\n}\n\nfunc (s *softmax) String() string {\n\treturn \"Softmax\"\n}\n\nfunc (s *softmax) forward(inputs ...*Tensor) *Tensor {\n\tx := inputs[0]\n\ty := x.clone()\n\ty.sub(x.max(s.axis, true))\n\ty.exp()\n\ty.div(y.sum(s.axis, true))\n\treturn y\n}\n\nfunc (s *softmax) backward(dy *Tensor) []*Tensor {\n\ty := s.output.Value()\n\tgx := y.clone()\n\tgx.mul(dy)\n\tsumdx := gx.sum(s.axis, true)\n\ty.mul(sumdx)\n\tgx.sub(y)\n\treturn []*Tensor{gx}\n}\n\ntype softmaxCrossEntropy struct {\n\tbase\n}\n\nfunc (c *softmaxCrossEntropy) String() string {\n\treturn \"SoftmaxCrossEntropy\"\n}\n\nfunc (c *softmaxCrossEntropy) forward(inputs ...*Tensor) *Tensor {\n\tx, t := inputs[0], inputs[1]\n\tm := x.max(1, true)\n\ts := x.clone().bSub(m)    // x - m\n\ts = s.exp()               // exp(x - m)\n\ts = s.sum(1, true)        // sum(exp(x - m))\n\ts.log()                   // log(sum(exp(x - m)))\n\tm.add(s)                  // m + log(sum(exp(x - m)))\n\tlogP := x.clone().bSub(m) // x - (m + log(sum(exp(x - m))))\n\tvar crossEntropy float32\n\tfor i := 0; i < len(t.data); i++ {\n\t\tcrossEntropy -= logP.Get(i, int(t.data[i]))\n\t}\n\tcrossEntropy /= float32(len(t.data))\n\treturn NewScalar(crossEntropy)\n}\n\nfunc (c *softmaxCrossEntropy) backward(dy *Tensor) []*Tensor {\n\tx, t := c.inputs[0], c.inputs[1]\n\t// gy *= 1/N\n\tgy := dy.clone().mul(NewScalar(1 / float32(len(t.data))))\n\t// y = softmax(x)\n\ty := x.clone()\n\ty.bSub(x.max(1, true))\n\ty.exp()\n\ty.bDiv(y.sum(1, true))\n\t// convert to one-hot\n\toneHot := Zeros(x.shape...)\n\tfor i := 0; i < len(t.data); i++ {\n\t\toneHot.data[i*x.shape[1]+int(t.data[i])] = 1\n\t}\n\t// y = (y - t_onehot) * gy\n\ty = y.sub(oneHot).mul(gy)\n\treturn []*Tensor{y, Zeros(t.shape...)}\n}\n\ntype opHeap []op\n\nfunc (h opHeap) Len() int {\n\treturn len(h)\n}\n\nfunc (h opHeap) Less(i, j int) bool {\n\treturn h[i].generation() > h[j].generation()\n}\n\nfunc (h opHeap) Swap(i, j int) {\n\th[i], h[j] = h[j], h[i]\n}\n\nfunc (h *opHeap) Push(o any) {\n\t*h = append(*h, o.(op))\n}\n\nfunc (h *opHeap) Pop() any {\n\told := *h\n\tn := len(old)\n\tx := old[n-1]\n\t*h = old[0 : n-1]\n\treturn x\n}\n"
  },
  {
    "path": "common/nn/op_test.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage nn\n\nimport (\n\t\"testing\"\n\n\t\"github.com/chewxy/math32\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nconst (\n\teps  = 1e-4\n\trtol = 1e-2\n\tatol = 5e-3\n)\n\nfunc numericalDiff(f func(*Tensor) *Tensor, x *Tensor) *Tensor {\n\tx0, x1 := x.clone(), x.clone()\n\tdx := make([]float32, len(x.data))\n\tfor i, v := range x.data {\n\t\tx0.data[i] = v - eps\n\t\tx1.data[i] = v + eps\n\t\ty0 := f(x0)\n\t\ty1 := f(x1)\n\t\tfor j := range y0.data {\n\t\t\tdx[i] += (y1.data[j] - y0.data[j]) / (2 * eps)\n\t\t}\n\t\tx0.data[i] = v\n\t\tx1.data[i] = v\n\t}\n\treturn NewTensor(dx, x.shape...)\n}\n\nfunc allClose(t *testing.T, a, b *Tensor) {\n\tif !assert.Equal(t, a.shape, b.shape) {\n\t\treturn\n\t}\n\tfor i := range a.data {\n\t\tif math32.Abs(a.data[i]-b.data[i]) > atol+rtol*math32.Abs(b.data[i]) {\n\t\t\tt.Fatalf(\"a.data[%d] = %f, b.data[%d] = %f\\n\", i, a.data[i], i, b.data[i])\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc TestAdd(t *testing.T) {\n\t// (2,3) + (2,3) -> (2,3)\n\tx := NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty := NewTensor([]float32{2, 3, 4, 5, 6, 7}, 2, 3)\n\tz := Add(x, y)\n\tassert.Equal(t, []float32{3, 5, 7, 9, 11, 13}, z.data)\n\n\t// Test gradient\n\tx = Rand(2, 3)\n\ty = Rand(2, 3)\n\tz = Add(x, y)\n\tz.Backward()\n\tdx := numericalDiff(func(x *Tensor) *Tensor { return Add(x, y) }, x)\n\tallClose(t, x.grad, dx)\n\tdy := numericalDiff(func(y *Tensor) *Tensor { return Add(x, y) }, y)\n\tallClose(t, y.grad, dy)\n\n\t// (2,3) + () -> (2,3)\n\tx = NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty = NewTensor([]float32{2})\n\tz = Add(x, y)\n\tassert.Equal(t, []float32{3, 4, 5, 6, 7, 8}, z.data)\n\n\t// Test gradient\n\tz.Backward()\n\tassert.Equal(t, []float32{1, 1, 1, 1, 1, 1}, x.grad.data)\n\tassert.Equal(t, []float32{6}, y.grad.data)\n\n\t// (2,3) + (3) -> (2,3)\n\tx = NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty = NewTensor([]float32{2, 3, 4}, 3)\n\tz = Add(x, y)\n\tassert.Equal(t, []float32{3, 5, 7, 6, 8, 10}, z.data)\n\n\t// Test gradient\n\tz.Backward()\n\tassert.Equal(t, []float32{1, 1, 1, 1, 1, 1}, x.grad.data)\n\tassert.Equal(t, []float32{2, 2, 2}, y.grad.data)\n}\n\nfunc TestSub(t *testing.T) {\n\t// (2,3) - (2,3) -> (2,3)\n\tx := NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty := NewTensor([]float32{2, 3, 4, 5, 6, 7}, 2, 3)\n\tz := Sub(x, y)\n\tassert.Equal(t, []float32{-1, -1, -1, -1, -1, -1}, z.data)\n\n\t// Test gradient\n\tx = Rand(2, 3)\n\ty = Rand(2, 3)\n\tz = Sub(x, y)\n\tz.Backward()\n\tdx := numericalDiff(func(x *Tensor) *Tensor { return Sub(x, y) }, x)\n\tallClose(t, x.grad, dx)\n\tdy := numericalDiff(func(y *Tensor) *Tensor { return Sub(x, y) }, y)\n\tallClose(t, y.grad, dy)\n\n\t// (2,3) - () -> (2,3)\n\tx = NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty = NewTensor([]float32{2})\n\tz = Sub(x, y)\n\tassert.Equal(t, []float32{-1, 0, 1, 2, 3, 4}, z.data)\n\n\t// Test gradient\n\tz.Backward()\n\tassert.Equal(t, []float32{1, 1, 1, 1, 1, 1}, x.grad.data)\n\tassert.Equal(t, []float32{-6}, y.grad.data)\n\n\t// (2,3) - (3) -> (2,3)\n\tx = NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty = NewTensor([]float32{2, 3, 4}, 3)\n\tz = Sub(x, y)\n\tassert.Equal(t, []float32{-1, -1, -1, 2, 2, 2}, z.data)\n\n\t// Test gradient\n\tz.Backward()\n\tassert.Equal(t, []float32{1, 1, 1, 1, 1, 1}, x.grad.data)\n\tassert.Equal(t, []float32{-2, -2, -2}, y.grad.data)\n}\n\nfunc TestMul(t *testing.T) {\n\t// (2,3) * (2,3) -> (2,3)\n\tx := NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty := NewTensor([]float32{2, 3, 4, 5, 6, 7}, 2, 3)\n\tz := Mul(x, y)\n\tassert.Equal(t, []float32{2, 6, 12, 20, 30, 42}, z.data)\n\n\t// Test gradient\n\tx = Rand(2, 3)\n\ty = Rand(2, 3)\n\tz = Mul(x, y)\n\tz.Backward()\n\tdx := numericalDiff(func(x *Tensor) *Tensor { return Mul(x, y) }, x)\n\tallClose(t, x.grad, dx)\n\tdy := numericalDiff(func(y *Tensor) *Tensor { return Mul(x, y) }, y)\n\tallClose(t, y.grad, dy)\n\n\t// (2,3) * () -> (2,3)\n\tx = NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty = NewTensor([]float32{2})\n\tz = Mul(x, y)\n\tassert.Equal(t, []float32{2, 4, 6, 8, 10, 12}, z.data)\n\n\t// Test gradient\n\tz.Backward()\n\tassert.Equal(t, []float32{2, 2, 2, 2, 2, 2}, x.grad.data)\n\tassert.Equal(t, []float32{21}, y.grad.data)\n\n\t// (2,3) * (3) -> (2,3)\n\tx = NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty = NewTensor([]float32{2, 3, 4}, 3)\n\tz = Mul(x, y)\n\tassert.Equal(t, []float32{2, 6, 12, 8, 15, 24}, z.data)\n\n\t// Test gradient\n\tz.Backward()\n\tassert.Equal(t, []float32{2, 3, 4, 2, 3, 4}, x.grad.data)\n\tassert.Equal(t, []float32{5, 7, 9}, y.grad.data)\n}\n\nfunc TestDiv(t *testing.T) {\n\t// (2,3) / (2,3) -> (2,3)\n\tx := NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty := NewTensor([]float32{2, 3, 4, 5, 6, 7}, 2, 3)\n\tz := Div(x, y)\n\tassert.InDeltaSlice(t, []float32{0.5, 2.0 / 3.0, 0.75, 4.0 / 5.0, 5.0 / 6.0, 6.0 / 7.0}, z.data, 1e-6)\n\n\t// Test gradient\n\tz.Backward()\n\tdx := numericalDiff(func(x *Tensor) *Tensor { return Div(x, y) }, x)\n\tallClose(t, x.grad, dx)\n\tdy := numericalDiff(func(y *Tensor) *Tensor { return Div(x, y) }, y)\n\tallClose(t, y.grad, dy)\n\n\t// (2,3) / () -> (2,3)\n\tx = NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty = NewTensor([]float32{2})\n\tz = Div(x, y)\n\tassert.InDeltaSlice(t, []float32{0.5, 1, 1.5, 2, 2.5, 3}, z.data, 1e-6)\n\n\t// Test gradient\n\tz.Backward()\n\tassert.InDeltaSlice(t, []float32{0.5, 0.5, 0.5, 0.5, 0.5, 0.5}, x.grad.data, 1e-6)\n\tassert.InDeltaSlice(t, []float32{-21.0 / 4.0}, y.grad.data, 1e-6)\n\n\t// (2,3) / (3) -> (2,3)\n\tx = NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty = NewTensor([]float32{2, 3, 4}, 3)\n\tz = Div(x, y)\n\tassert.InDeltaSlice(t, []float32{0.5, 2.0 / 3.0, 3.0 / 4.0, 2, 5.0 / 3.0, 1.5}, z.data, 1e-6)\n\n\t// Test gradient\n\tz.Backward()\n\tassert.InDeltaSlice(t, []float32{1.0 / 2, 1.0 / 3, 1.0 / 4, 1.0 / 2, 1.0 / 3, 1.0 / 4}, x.grad.data, 1e-6)\n\tassert.InDeltaSlice(t, []float32{-5.0 / 4.0, -7.0 / 9.0, -9.0 / 16.0}, y.grad.data, 1e-6)\n}\n\nfunc TestSquare(t *testing.T) {\n\t// (2,3) -> (2,3)\n\tx := NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty := Square(x)\n\tassert.Equal(t, []float32{1, 4, 9, 16, 25, 36}, y.data)\n\n\t// Test gradient\n\tx = Rand(2, 3)\n\ty = Square(x)\n\ty.Backward()\n\tdx := numericalDiff(Square, x)\n\tallClose(t, x.grad, dx)\n}\n\nfunc TestPow(t *testing.T) {\n\t// (2,3) ** (2,3) -> (2,3)\n\tx := NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty := NewTensor([]float32{2, 3, 4, 5, 6, 7}, 2, 3)\n\tz := Pow(x, y)\n\tassert.InDeltaSlice(t, []float32{1, 8, 81, 1024, 15625, 279936}, z.data, 1e-6)\n\n\t// Test gradient\n\tx = Uniform(0.5, 1, 2, 3)\n\ty = Uniform(0.5, 1, 2, 3)\n\tz = Pow(x, y)\n\tz.Backward()\n\tdx := numericalDiff(func(x *Tensor) *Tensor { return Pow(x, y) }, x)\n\tallClose(t, x.grad, dx)\n\tdy := numericalDiff(func(y *Tensor) *Tensor { return Pow(x, y) }, y)\n\tallClose(t, y.grad, dy)\n\n\t// (2,3) ** () -> (2,3)\n\tx = NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty = NewTensor([]float32{2})\n\tz = Pow(x, y)\n\tassert.InDeltaSlice(t, []float32{1, 4, 9, 16, 25, 36}, z.data, 1e-6)\n\n\t// Test gradient\n\tz.Backward()\n\tassert.InDeltaSlice(t, []float32{2, 4, 6, 8, 10, 12}, x.grad.data, 1e-6)\n\tassert.InDeltaSlice(t, []float32{\n\t\tmath32.Pow(1, 2)*math32.Log(1) +\n\t\t\tmath32.Pow(2, 2)*math32.Log(2) +\n\t\t\tmath32.Pow(3, 2)*math32.Log(3) +\n\t\t\tmath32.Pow(4, 2)*math32.Log(4) +\n\t\t\tmath32.Pow(5, 2)*math32.Log(5) +\n\t\t\tmath32.Pow(6, 2)*math32.Log(6),\n\t}, y.grad.data, 1e-6)\n}\n\nfunc TestExp(t *testing.T) {\n\t// (2,3) -> (2,3)\n\tx := NewTensor([]float32{0, 1, 2, 3, 4, 5}, 2, 3)\n\ty := Exp(x)\n\tassert.InDeltaSlice(t, []float32{1, math32.Exp(1), math32.Exp(2), math32.Exp(3), math32.Exp(4), math32.Exp(5)}, y.data, 1e-5)\n\n\t// Test gradient\n\tx = Rand(2, 3)\n\ty = Exp(x)\n\ty.Backward()\n\tdx := numericalDiff(Exp, x)\n\tallClose(t, x.grad, dx)\n}\n\nfunc TestLog(t *testing.T) {\n\t// (2,3) -> (2,3)\n\tx := NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty := Log(x)\n\tassert.InDeltaSlice(t, []float32{0, math32.Log(2), math32.Log(3), math32.Log(4), math32.Log(5), math32.Log(6)}, y.data, 1e-6)\n\n\t// Test gradient\n\tx = Uniform(1, 2, 2, 3)\n\ty = Log(x)\n\ty.Backward()\n\tdx := numericalDiff(Log, x)\n\tallClose(t, x.grad, dx)\n}\n\nfunc TestAbs(t *testing.T) {\n\t// (2,3) -> (2,3)\n\tx := NewTensor([]float32{1, -2, 3, -4, 5, -6}, 2, 3)\n\ty := Abs(x)\n\tassert.Equal(t, []float32{1, 2, 3, 4, 5, 6}, y.data)\n\n\t// Test gradient\n\tx = Rand(2, 3)\n\ty = Abs(x)\n\ty.Backward()\n\tdx := numericalDiff(Abs, x)\n\tallClose(t, x.grad, dx)\n}\n\nfunc TestSum(t *testing.T) {\n\t// (2,3) -> ()\n\tx := NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty := Sum(x)\n\tassert.Equal(t, []float32{21}, y.data)\n\n\t// Test gradient\n\tx = Rand(2, 3)\n\ty = Sum(x)\n\ty.Backward()\n\tassert.Equal(t, []float32{1, 1, 1, 1, 1, 1}, x.grad.data)\n\n\t// (2,3,2) -> (2,2)\n\tx = NewTensor([]float32{1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6}, 2, 3, 2)\n\ty = Sum(x, 1)\n\tassert.Equal(t, []int{2, 2}, y.shape)\n\tassert.Equal(t, []float32{9, 12, 9, 12}, y.data)\n\n\t// Test gradient\n\tx = Rand(2, 3, 2)\n\ty = Sum(x, 1)\n\ty.Backward()\n\tassert.Equal(t, []int{2, 3, 2}, x.grad.shape)\n\tassert.Equal(t, []float32{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, x.grad.data)\n}\n\nfunc TestMean(t *testing.T) {\n\t// (2,3) -> ()\n\tx := NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty := Mean(x)\n\tassert.Equal(t, []float32{3.5}, y.data)\n\n\t// Test gradient\n\tx = Rand(2, 3)\n\ty = Mean(x)\n\ty.Backward()\n\tassert.Equal(t, []float32{1.0 / 6, 1.0 / 6, 1.0 / 6, 1.0 / 6, 1.0 / 6, 1.0 / 6}, x.grad.data)\n}\n\nfunc TestCos(t *testing.T) {\n\t// (2,3) -> (2,3)\n\tx := NewTensor([]float32{0, 0.1, 0.2, 0.3, 0.4, 0.5}, 2, 3)\n\ty := Cos(x)\n\tassert.InDeltaSlice(t, []float32{1, 0.9950041652780258, 0.9800665778412416, 0.955336489125606, 0.9210609940028851, 0.8775825618903728}, y.data, 1e-6)\n\n\t// Test gradient\n\tx = Rand(2, 3)\n\ty = Cos(x)\n\ty.Backward()\n\tdx := numericalDiff(Cos, x)\n\tallClose(t, x.grad, dx)\n}\n\nfunc TestSin(t *testing.T) {\n\t// (2,3) -> (2,3)\n\tx := NewTensor([]float32{0, 1, 2, 3, 4, 5}, 2, 3)\n\ty := Sin(x)\n\tassert.InDeltaSlice(t, []float32{0, 0.8414709848078965, 0.9092974268256817, 0.1411200080598672, -0.7568024953079282, -0.9589242746631385}, y.data, 1e-6)\n\n\t// Test gradient\n\tx = Rand(2, 3)\n\ty = Sin(x)\n\ty.Backward()\n\tdx := numericalDiff(Sin, x)\n\tallClose(t, x.grad, dx)\n}\n\nfunc TestMatMul(t *testing.T) {\n\t// (2,3) * (3,4) -> (2,4)\n\tx := NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty := NewTensor([]float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, 3, 4)\n\tz := MatMul(x, y, false, false, 0)\n\tassert.Equal(t, []int{2, 4}, z.shape)\n\tassert.Equal(t, []float32{38, 44, 50, 56, 83, 98, 113, 128}, z.data)\n\n\t// Test gradient\n\tz.Backward()\n\tassert.Equal(t, []int{2, 3}, x.grad.shape)\n\tassert.Equal(t, []float32{10, 26, 42, 10, 26, 42}, x.grad.data)\n\tassert.Equal(t, []int{3, 4}, y.grad.shape)\n\tassert.Equal(t, []float32{5, 5, 5, 5, 7, 7, 7, 7, 9, 9, 9, 9}, y.grad.data)\n\n\t// (3,2).T * (3,4) -> (2,4)\n\tx = Rand(3, 2)\n\ty = Rand(3, 4)\n\tz = MatMul(x, y, true, false, 0)\n\tassert.Equal(t, []int{2, 4}, z.shape)\n\tz.Backward()\n\tassert.Equal(t, []int{3, 2}, x.grad.shape)\n\tassert.Equal(t, []int{3, 4}, y.grad.shape)\n\n\t// (2,3) * (4,3).T -> (2,4)\n\tx = Rand(2, 3)\n\ty = Rand(4, 3)\n\tz = MatMul(x, y, false, true, 0)\n\tassert.Equal(t, []int{2, 4}, z.shape)\n\tz.Backward()\n\tassert.Equal(t, []int{2, 3}, x.grad.shape)\n\tassert.Equal(t, []int{4, 3}, y.grad.shape)\n\n\t// (3,2).T * (4,3).T -> (2,4)\n\tx = Rand(3, 2)\n\ty = Rand(4, 3)\n\tz = MatMul(x, y, true, true, 0)\n\tassert.Equal(t, []int{2, 4}, z.shape)\n\tz.Backward()\n\tassert.Equal(t, []int{3, 2}, x.grad.shape)\n}\n\nfunc TestBMM(t *testing.T) {\n\t// (2,2,3) * (2,3,4) -> (2,2,4)\n\tx := NewTensor([]float32{1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6}, 2, 2, 3)\n\ty := NewTensor([]float32{\n\t\t1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,\n\t\t1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,\n\t}, 2, 3, 4)\n\tz := BMM(x, y, false, false, 0)\n\tassert.Equal(t, []int{2, 2, 4}, z.shape)\n\tassert.Equal(t, []float32{\n\t\t38, 44, 50, 56, 83, 98, 113, 128,\n\t\t38, 44, 50, 56, 83, 98, 113, 128,\n\t}, z.data)\n\n\t// Test gradient\n\tz.Backward()\n\tassert.Equal(t, []int{2, 2, 3}, x.grad.shape)\n\tassert.Equal(t, []float32{\n\t\t10, 26, 42, 10, 26, 42,\n\t\t10, 26, 42, 10, 26, 42,\n\t}, x.grad.data)\n\tassert.Equal(t, []int{2, 3, 4}, y.grad.shape)\n\tassert.Equal(t, []float32{\n\t\t5, 5, 5, 5, 7, 7, 7, 7, 9, 9, 9, 9,\n\t\t5, 5, 5, 5, 7, 7, 7, 7, 9, 9, 9, 9,\n\t}, y.grad.data)\n\n\t// (2,3,2).T * (2,3,4) -> (2,2,4)\n\tx = Rand(2, 3, 2)\n\ty = Rand(2, 3, 4)\n\tz = BMM(x, y, true, false, 0)\n\tassert.Equal(t, []int{2, 2, 4}, z.shape)\n\tz.Backward()\n\tassert.Equal(t, []int{2, 3, 2}, x.grad.shape)\n\n\t// (2,2,3) * (2,4,3).T -> (2,2,4)\n\tx = Rand(2, 2, 3)\n\ty = Rand(2, 4, 3)\n\tz = BMM(x, y, false, true, 0)\n\tassert.Equal(t, []int{2, 2, 4}, z.shape)\n\tz.Backward()\n\tassert.Equal(t, []int{2, 2, 3}, x.grad.shape)\n\n\t// (2,3,2).T * (2,43).T -> (2,2,4)\n\tx = Rand(2, 3, 2)\n\ty = Rand(2, 4, 3)\n\tz = BMM(x, y, true, true, 0)\n\tassert.Equal(t, []int{2, 2, 4}, z.shape)\n\tz.Backward()\n\tassert.Equal(t, []int{2, 3, 2}, x.grad.shape)\n}\n\nfunc TestBroadcast(t *testing.T) {\n\t// (2) -> (2,3)\n\tx := NewTensor([]float32{1, 2}, 2)\n\ty := Broadcast(x, 3)\n\tassert.Equal(t, []float32{1, 1, 1, 2, 2, 2}, y.data)\n\n\t// Test gradient\n\ty.Backward()\n\tassert.Equal(t, []float32{3, 3}, x.grad.data)\n}\n\nfunc TestEmbedding(t *testing.T) {\n\t// (2,3) -> (2,3,2)\n\tx := NewTensor([]float32{0, 1, 0, 3, 0, 5}, 2, 3)\n\tw := NewTensor([]float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, 6, 2)\n\ty := Embedding(w, x)\n\tassert.Equal(t, []int{2, 3, 2}, y.shape)\n\tassert.Equal(t, []float32{0, 1, 2, 3, 0, 1, 6, 7, 0, 1, 10, 11}, y.data)\n\n\t// Test gradient\n\ty.Backward()\n\tassert.Nil(t, x.grad)\n\tassert.Equal(t, []float32{3, 3, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1}, w.grad.data)\n\n\t// (2,3) -> (2,3,1,2)\n\tx = NewTensor([]float32{0, 1, 0, 3, 0, 5}, 2, 3)\n\tw = NewTensor([]float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, 6, 1, 2)\n\ty = Embedding(w, x)\n\tassert.Equal(t, []int{2, 3, 1, 2}, y.shape)\n\tassert.Equal(t, []float32{0, 1, 2, 3, 0, 1, 6, 7, 0, 1, 10, 11}, y.data)\n\n\t// Test gradient\n\ty.Backward()\n\tassert.Nil(t, x.grad)\n\tassert.Equal(t, []float32{3, 3, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1}, w.grad.data)\n}\n\nfunc TestSigmoid(t *testing.T) {\n\t// (2,3) -> (2,3)\n\tx := NewTensor([]float32{0, 1, 2, 3, 4, 5}, 2, 3)\n\ty := Sigmoid(x)\n\tassert.InDeltaSlice(t, []float32{0.5, 0.7310585786300049, 0.8807970779778823, 0.9525741268224334, 0.9820137900379085, 0.9933071490757153}, y.data, 1e-6)\n\n\t// Test gradient\n\tx = Rand(2, 3)\n\ty = Sigmoid(x)\n\ty.Backward()\n\tdx := numericalDiff(Sigmoid, x)\n\tallClose(t, x.grad, dx)\n}\n\nfunc TestReLu(t *testing.T) {\n\t// (2,3) -> (2,3)\n\tx := NewTensor([]float32{-1, 0, 1, 2, 3, 4}, 2, 3)\n\ty := ReLu(x)\n\tassert.Equal(t, []float32{0, 0, 1, 2, 3, 4}, y.data)\n\n\t// Test gradient\n\tx = Rand(2, 3)\n\ty = ReLu(x)\n\ty.Backward()\n\tdx := numericalDiff(ReLu, x)\n\tallClose(t, x.grad, dx)\n}\n\nfunc TestSoftmax(t *testing.T) {\n\t// (1,3) -> (1,3)\n\tx := NewTensor([]float32{3.0, 1.0, 0.2}, 1, 3)\n\ty := Softmax(x, 1)\n\tassert.Equal(t, []int{1, 3}, y.shape)\n\tassert.InDeltaSlice(t, []float32{0.8360188027814407, 0.11314284146556013, 0.05083835575299916}, y.data, 1e-6)\n\n\t// Test gradient\n\ty.Backward()\n\tdx := numericalDiff(func(x *Tensor) *Tensor { return Softmax(x, 1) }, x)\n\tallClose(t, x.grad, dx)\n}\n\nfunc TestFlatten(t *testing.T) {\n\t// (2,3) -> (6)\n\tx := NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty := Flatten(x)\n\tassert.Equal(t, []float32{1, 2, 3, 4, 5, 6}, y.data)\n\n\t// Test gradient\n\ty.Backward()\n\tassert.Equal(t, []float32{1, 1, 1, 1, 1, 1}, x.grad.data)\n}\n\nfunc TestReshape(t *testing.T) {\n\t// (2,3) -> (3,2)\n\tx := NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty := Reshape(x, 3, 2)\n\tassert.Equal(t, []float32{1, 2, 3, 4, 5, 6}, y.data)\n\n\t// Test gradient\n\ty.Backward()\n\tassert.Equal(t, []float32{1, 1, 1, 1, 1, 1}, x.grad.data)\n}\n\nfunc TestSoftmaxCrossEntropy(t *testing.T) {\n\t// (2,3) -> (2,3)\n\tx := NewTensor([]float32{0.3, 2.9, 4.0, 0.2, 1.0, 3.0}, 3, 2)\n\ty := NewTensor([]float32{1, 0, 1}, 3)\n\tz := SoftmaxCrossEntropy(x, y)\n\tassert.Empty(t, z.shape)\n\tassert.InDelta(t, float32(0.07356563982184072), z.data[0], 1e-4)\n\n\t// Test gradient\n\tz.Backward()\n\tdx := numericalDiff(func(x *Tensor) *Tensor { return SoftmaxCrossEntropy(x, y) }, x)\n\tallClose(t, x.grad, dx)\n}\n\nfunc TestReuseLeaf(t *testing.T) {\n\t// x + x\n\tx := NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ty := Add(x, x)\n\tassert.Equal(t, []float32{2, 4, 6, 8, 10, 12}, y.data)\n\n\t// Test gradient\n\ty.Backward()\n\tdx := numericalDiff(func(x *Tensor) *Tensor { return Add(x, x) }, x)\n\tallClose(t, x.grad, dx)\n}\n\nfunc TestReuseNode(t *testing.T) {\n\t// x^2 + x^2\n\tx := NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ttemp := Pow(x, NewTensor([]float32{2}))\n\ty := Add(temp, temp)\n\tassert.Equal(t, []float32{2, 8, 18, 32, 50, 72}, y.data)\n\n\t// Test gradient\n\ty.Backward()\n\tdx := numericalDiff(func(x *Tensor) *Tensor {\n\t\ttemp := Pow(x, NewTensor([]float32{2}))\n\t\treturn Add(temp, temp)\n\t}, x)\n\tallClose(t, x.grad, dx)\n}\n\nfunc TestDependency(t *testing.T) {\n\t// x^2 + 2x^2\n\tx := NewTensor([]float32{1, 2, 3, 4, 5, 6}, 2, 3)\n\ttemp := Pow(x, NewTensor([]float32{2}))\n\ty := Add(temp, Mul(NewTensor([]float32{2}), temp))\n\tassert.Equal(t, []float32{3, 12, 27, 48, 75, 108}, y.data)\n\n\t// Test gradient\n\ty.Backward()\n\tdx := numericalDiff(func(x *Tensor) *Tensor {\n\t\ttemp := Pow(x, NewTensor([]float32{2}))\n\t\treturn Add(temp, Mul(NewTensor([]float32{2}), temp))\n\t}, x)\n\tallClose(t, x.grad, dx)\n}\n\nfunc TestSphere(t *testing.T) {\n\t// x^2 + y^2\n\tx := NewScalar(1)\n\ty := NewScalar(1)\n\tz := Add(Mul(x, x), Mul(y, y))\n\tassert.Equal(t, []float32{2}, z.data)\n\n\t// Test gradient\n\tz.Backward()\n\tdx := numericalDiff(func(x *Tensor) *Tensor { return Add(Mul(x, x), Mul(y, y)) }, x)\n\tdy := numericalDiff(func(y *Tensor) *Tensor { return Add(Mul(x, x), Mul(y, y)) }, y)\n\tallClose(t, x.grad, dx)\n\tallClose(t, y.grad, dy)\n}\n\nfunc TestMatyas(t *testing.T) {\n\t// 0.26 * (x^2 + y^2) - 0.48 * x * y\n\tx := NewScalar(1)\n\ty := NewScalar(1)\n\tz := Sub(Mul(NewScalar(0.26), Add(Mul(x, x), Mul(y, y))), Mul(NewScalar(0.48), Mul(x, y)))\n\tassert.InDeltaSlice(t, []float32{0.04}, z.data, 1e-6)\n\n\t// Test gradient\n\tz.Backward()\n\tdx := numericalDiff(func(x *Tensor) *Tensor {\n\t\treturn Sub(Mul(NewScalar(0.26), Add(Mul(x, x), Mul(y, y))), Mul(NewScalar(0.48), Mul(x, y)))\n\t}, x)\n\tdy := numericalDiff(func(y *Tensor) *Tensor {\n\t\treturn Sub(Mul(NewScalar(0.26), Add(Mul(x, x), Mul(y, y))), Mul(NewScalar(0.48), Mul(x, y)))\n\t}, y)\n\tallClose(t, x.grad, dx)\n\tallClose(t, y.grad, dy)\n}\n\nfunc TestGoldsteinPrice(t *testing.T) {\n\t// (1 + (x + y + 1)^2 * (19 - 14x + 3x^2 - 14y + 6xy + 3y^2)) * (30 + (2x - 3y)^2 * (18 - 32x + 12x^2 + 48y - 36xy + 27y^2))\n\tx := NewScalar(1)\n\ty := NewScalar(1)\n\tz := Mul(\n\t\tAdd(NewScalar(1), Mul(\n\t\t\tPow(Add(x, y, NewScalar(1)), NewScalar(2)), // (x + y + 1)^2\n\t\t\tAdd(\n\t\t\t\tNewScalar(19),                              // 19\n\t\t\t\tMul(NewScalar(-14), x),                     // -14x\n\t\t\t\tMul(NewScalar(3), Pow(x, NewScalar(2))),    // 3x^2\n\t\t\t\tMul(NewScalar(-14), y),                     // -14y\n\t\t\t\tMul(NewScalar(6), Mul(x, y)),               // 6xy\n\t\t\t\tMul(NewScalar(3), Pow(y, NewScalar(2)))))), // 3y^2\n\t\tAdd(NewScalar(30), Mul(\n\t\t\tPow(Sub(Mul(NewScalar(2), x), Mul(NewScalar(3), y)), NewScalar(2)), // (2x - 3y)^2\n\t\t\tAdd(\n\t\t\t\tNewScalar(18),                               // 18\n\t\t\t\tMul(NewScalar(-32), x),                      // -32x\n\t\t\t\tMul(NewScalar(12), Pow(x, NewScalar(2))),    // 12x^2\n\t\t\t\tMul(NewScalar(48), y),                       // 48y\n\t\t\t\tMul(NewScalar(-36), Mul(x, y)),              // -36xy\n\t\t\t\tMul(NewScalar(27), Pow(y, NewScalar(2))))))) // 27y^2\n\tassert.InDeltaSlice(t, []float32{1876}, z.data, 1e-6)\n\n\t// Test gradient\n\tz.Backward()\n\tdx := numericalDiff(func(x *Tensor) *Tensor {\n\t\treturn Mul(\n\t\t\tAdd(NewScalar(1), Mul(\n\t\t\t\tPow(Add(x, y, NewScalar(1)), NewScalar(2)), // (x + y + 1)^2\n\t\t\t\tAdd(\n\t\t\t\t\tNewScalar(19),                              // 19\n\t\t\t\t\tMul(NewScalar(-14), x),                     // -14x\n\t\t\t\t\tMul(NewScalar(3), Pow(x, NewScalar(2))),    // 3x^2\n\t\t\t\t\tMul(NewScalar(-14), y),                     // -14y\n\t\t\t\t\tMul(NewScalar(6), Mul(x, y)),               // 6xy\n\t\t\t\t\tMul(NewScalar(3), Pow(y, NewScalar(2)))))), // 3y^2\n\t\t\tAdd(NewScalar(30), Mul(\n\t\t\t\tPow(Sub(Mul(NewScalar(2), x), Mul(NewScalar(3), y)), NewScalar(2)), // (2x - 3y)^2\n\t\t\t\tAdd(\n\t\t\t\t\tNewScalar(18),                               // 18\n\t\t\t\t\tMul(NewScalar(-32), x),                      // -32x\n\t\t\t\t\tMul(NewScalar(12), Pow(x, NewScalar(2))),    // 12x^2\n\t\t\t\t\tMul(NewScalar(48), y),                       // 48y\n\t\t\t\t\tMul(NewScalar(-36), Mul(x, y)),              // -36xy\n\t\t\t\t\tMul(NewScalar(27), Pow(y, NewScalar(2))))))) // 27y^2\n\t}, x)\n\tdy := numericalDiff(func(y *Tensor) *Tensor {\n\t\treturn Mul(\n\t\t\tAdd(NewScalar(1), Mul(\n\t\t\t\tPow(Add(x, y, NewScalar(1)), NewScalar(2)), // (x + y + 1)^2\n\t\t\t\tAdd(\n\t\t\t\t\tNewScalar(19),                              // 19\n\t\t\t\t\tMul(NewScalar(-14), x),                     // -14x\n\t\t\t\t\tMul(NewScalar(3), Pow(x, NewScalar(2))),    // 3x^2\n\t\t\t\t\tMul(NewScalar(-14), y),                     // -14y\n\t\t\t\t\tMul(NewScalar(6), Mul(x, y)),               // 6xy\n\t\t\t\t\tMul(NewScalar(3), Pow(y, NewScalar(2)))))), // 3y^2\n\t\t\tAdd(NewScalar(30), Mul(\n\t\t\t\tPow(Sub(Mul(NewScalar(2), x), Mul(NewScalar(3), y)), NewScalar(2)), // (2x - 3y)^2\n\t\t\t\tAdd(\n\t\t\t\t\tNewScalar(18),                               // 18\n\t\t\t\t\tMul(NewScalar(-32), x),                      // -32x\n\t\t\t\t\tMul(NewScalar(12), Pow(x, NewScalar(2))),    // 12x^2\n\t\t\t\t\tMul(NewScalar(48), y),                       // 48y\n\t\t\t\t\tMul(NewScalar(-36), Mul(x, y)),              // -36xy\n\t\t\t\t\tMul(NewScalar(27), Pow(y, NewScalar(2))))))) // 27y^2\n\t}, y)\n\tallClose(t, x.grad, dx)\n\tallClose(t, y.grad, dy)\n}\n"
  },
  {
    "path": "common/nn/optimizers.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage nn\n\nimport (\n\t\"sync\"\n\n\t\"github.com/chewxy/math32\"\n\t\"github.com/gorse-io/gorse/common/floats\"\n\t\"github.com/samber/lo\"\n)\n\ntype Optimizer interface {\n\tSetWeightDecay(rate float32)\n\tSetJobs(jobs int)\n\tZeroGrad()\n\tStep()\n}\n\ntype baseOptimizer struct {\n\tparams []*Tensor\n\twd     float32\n\tjobs   int\n}\n\nfunc (o *baseOptimizer) ZeroGrad() {\n\tfor _, p := range o.params {\n\t\tp.grad = nil\n\t}\n}\n\nfunc (o *baseOptimizer) SetWeightDecay(wd float32) {\n\to.wd = wd\n}\n\nfunc (o *baseOptimizer) SetJobs(jobs int) {\n\to.jobs = jobs\n}\n\ntype SGD struct {\n\tbaseOptimizer\n\tlr float32\n\tb  []float32\n}\n\nfunc NewSGD(params []*Tensor, lr float32) Optimizer {\n\tbufSize := 0\n\tfor _, p := range params {\n\t\tbufSize = max(bufSize, len(p.data))\n\t}\n\treturn &SGD{\n\t\tbaseOptimizer: baseOptimizer{params: params},\n\t\tlr:            lr,\n\t\tb:             make([]float32, bufSize),\n\t}\n}\n\nfunc (s *SGD) Step() {\n\tfor _, p := range s.params {\n\t\tb := s.b[:len(p.data)]\n\t\tparts := partitionAligned(len(p.data), s.jobs, 32)\n\t\tvar wg sync.WaitGroup\n\t\tfor _, part := range parts {\n\t\t\ti, j := part.A, part.B\n\t\t\twg.Go(func() {\n\t\t\t\tfloats.MulConstAddTo(p.data[i:j], s.wd, p.grad.data[i:j], b[i:j])\n\t\t\t\tfloats.MulConstAdd(b[i:j], -s.lr, p.data[i:j])\n\t\t\t})\n\t\t}\n\t\twg.Wait()\n\t}\n}\n\ntype Adam struct {\n\tbaseOptimizer\n\talpha float32\n\tbeta1 float32\n\tbeta2 float32\n\teps   float32\n\tms    map[*Tensor]*Tensor\n\tvs    map[*Tensor]*Tensor\n\tt     float32\n\n\tb1 []float32\n\tb2 []float32\n}\n\nfunc NewAdam(params []*Tensor, alpha float32) Optimizer {\n\tbufSize := 0\n\tfor _, p := range params {\n\t\tbufSize = max(bufSize, len(p.data))\n\t}\n\treturn &Adam{\n\t\tbaseOptimizer: baseOptimizer{params: params},\n\t\talpha:         alpha,\n\t\tbeta1:         0.9,\n\t\tbeta2:         0.999,\n\t\teps:           1e-8,\n\t\tms:            make(map[*Tensor]*Tensor),\n\t\tvs:            make(map[*Tensor]*Tensor),\n\t\tb1:            make([]float32, bufSize),\n\t\tb2:            make([]float32, bufSize),\n\t}\n}\n\nfunc (a *Adam) Step() {\n\ta.t++\n\n\tfix1 := 1 - math32.Pow(a.beta1, a.t)\n\tfix2 := 1 - math32.Pow(a.beta2, a.t)\n\tlr := a.alpha * math32.Sqrt(fix2) / fix1\n\n\tfor _, p := range a.params {\n\t\tif _, ok := a.ms[p]; !ok {\n\t\t\ta.ms[p] = Zeros(p.shape...)\n\t\t\ta.vs[p] = Zeros(p.shape...)\n\t\t}\n\t\tm, v := a.ms[p], a.vs[p]\n\t\tb1, b2 := a.b1[:len(p.data)], a.b2[:len(p.data)]\n\n\t\tparts := partitionAligned(len(p.data), a.jobs, 32)\n\t\tvar wg sync.WaitGroup\n\t\tfor _, part := range parts {\n\t\t\ti, j := part.A, part.B\n\t\t\twg.Go(func() {\n\t\t\t\t// grad = grad + wd * param.data\n\t\t\t\tfloats.MulConstAddTo(p.data[i:j], a.wd, p.grad.data[i:j], b1[i:j])\n\t\t\t\t// m += (1 - beta1) * (grad - m)\n\t\t\t\tfloats.SubTo(b1[i:j], m.data[i:j], b2[i:j])\n\t\t\t\tfloats.MulConstAdd(b2[i:j], 1-a.beta1, m.data[i:j])\n\t\t\t\t// v += (1 - beta2) * (grad * grad - v)\n\t\t\t\tfloats.MulTo(b1[i:j], b1[i:j], b2[i:j])\n\t\t\t\tfloats.Sub(b2[i:j], v.data[i:j])\n\t\t\t\tfloats.MulConstAdd(b2[i:j], 1-a.beta2, v.data[i:j])\n\t\t\t\t// param.data -= self.lr * m / (xp.sqrt(v) + eps)\n\t\t\t\tfloats.SqrtTo(v.data[i:j], b2[i:j])\n\t\t\t\tfloats.AddConst(b2[i:j], a.eps)\n\t\t\t\tfloats.DivTo(m.data[i:j], b2[i:j], b1[i:j])\n\t\t\t\tfloats.MulConstAdd(b1[i:j], -lr, p.data[i:j])\n\t\t\t})\n\t\t}\n\t\twg.Wait()\n\t}\n}\n\n// partitionAligned partitions n-size slice into m parts. Each part is aligned to k elements except the last part. For example:\n// split(10, 3, 2) = [(0, 4), (4, 8), (8, 10)]\nfunc partitionAligned(n, m, k int) []lo.Tuple2[int, int] {\n\tif n <= 0 {\n\t\treturn nil\n\t}\n\tif m <= 0 {\n\t\treturn []lo.Tuple2[int, int]{{0, n}}\n\t}\n\tif k <= 0 {\n\t\tk = 1\n\t}\n\t// calculate the size of each part\n\tpartSize := n / m\n\tif partSize%k != 0 {\n\t\tpartSize += k - partSize%k\n\t}\n\tif partSize == 0 {\n\t\tpartSize = k\n\t}\n\t// split the slice into m parts\n\tparts := make([]lo.Tuple2[int, int], 0, m)\n\tstart := 0\n\tfor start < n {\n\t\tend := start + partSize\n\t\tif end > n {\n\t\t\tend = n\n\t\t}\n\t\tparts = append(parts, lo.Tuple2[int, int]{A: start, B: end})\n\t\tstart = end\n\t}\n\treturn parts\n}\n"
  },
  {
    "path": "common/nn/tensor.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage nn\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"fmt\"\n\t\"math\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\"github.com/chewxy/math32\"\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/gorse-io/gorse/common/floats\"\n\t\"github.com/gorse-io/gorse/common/parallel\"\n\t\"github.com/gorse-io/gorse/protocol\"\n\t\"github.com/samber/lo\"\n\t\"golang.org/x/exp/slices\"\n)\n\n// inferenceMode disables gradient computation when enabled, improving performance during model evaluation.\nvar inferenceMode = atomic.Bool{}\n\n// SetInferenceMode enables or disables inference mode, which disables gradient computation to improve performance during model evaluation.\nfunc SetInferenceMode(enabled bool) {\n\tinferenceMode.Store(enabled)\n}\n\ntype Tensor struct {\n\tdata  []float32\n\tshape []int\n\tgrad  *Tensor\n\top    op\n}\n\nfunc NewTensor(data []float32, shape ...int) *Tensor {\n\tsize := 1\n\tfor i := range shape {\n\t\tsize *= shape[i]\n\t}\n\tif len(data) != size {\n\t\tpanic(fmt.Sprintf(\"shape %v does not match data size %v\", shape, len(data)))\n\t}\n\treturn &Tensor{\n\t\tdata:  data,\n\t\tshape: shape,\n\t}\n}\n\nfunc NewScalar(data float32) *Tensor {\n\treturn &Tensor{\n\t\tdata:  []float32{data},\n\t\tshape: []int{},\n\t}\n}\n\nfunc LinSpace(start, end float32, shape ...int) *Tensor {\n\tn := 1\n\tfor _, s := range shape {\n\t\tn *= s\n\t}\n\tdata := make([]float32, n)\n\tdelta := (end - start) / float32(n-1)\n\tfor i := range data {\n\t\tdata[i] = start + delta*float32(i)\n\t}\n\treturn &Tensor{\n\t\tdata:  data,\n\t\tshape: shape,\n\t}\n}\n\nfunc Rand(shape ...int) *Tensor {\n\tn := 1\n\tfor _, s := range shape {\n\t\tn *= s\n\t}\n\tdata := make([]float32, n)\n\tfor i := range data {\n\t\tdata[i] = rand.Float32()\n\t}\n\treturn &Tensor{\n\t\tdata:  data,\n\t\tshape: shape,\n\t}\n}\n\nfunc Uniform(low, high float32, shape ...int) *Tensor {\n\tn := 1\n\tfor _, s := range shape {\n\t\tn *= s\n\t}\n\tdata := make([]float32, n)\n\tfor i := range data {\n\t\tdata[i] = rand.Float32()*(high-low) + low\n\t}\n\treturn &Tensor{\n\t\tdata:  data,\n\t\tshape: shape,\n\t}\n}\n\nfunc Normal(mean, std float32, shape ...int) *Tensor {\n\tn := 1\n\tfor _, s := range shape {\n\t\tn *= s\n\t}\n\tdata := make([]float32, n)\n\tfor i := range data {\n\t\tdata[i] = float32(rand.NormFloat64())*std + mean\n\t}\n\treturn &Tensor{\n\t\tdata:  data,\n\t\tshape: shape,\n\t}\n}\n\n// Ones creates a tensor filled with ones.\nfunc Ones(shape ...int) *Tensor {\n\tn := 1\n\tfor _, s := range shape {\n\t\tn *= s\n\t}\n\tdata := make([]float32, n)\n\tfor i := range data {\n\t\tdata[i] = 1\n\t}\n\treturn &Tensor{\n\t\tdata:  data,\n\t\tshape: shape,\n\t}\n}\n\n// Zeros creates a tensor filled with zeros.\nfunc Zeros(shape ...int) *Tensor {\n\tn := 1\n\tfor _, s := range shape {\n\t\tn *= s\n\t}\n\tdata := make([]float32, n)\n\treturn &Tensor{\n\t\tdata:  data,\n\t\tshape: shape,\n\t}\n}\n\nfunc (t *Tensor) generation() int {\n\tif t.op != nil {\n\t\treturn t.op.generation()\n\t}\n\treturn 0\n}\n\nfunc (t *Tensor) IsScalar() bool {\n\treturn len(t.shape) == 0\n}\n\n// NoGrad convert a node tensor to a leaf tensor.\nfunc (t *Tensor) NoGrad() *Tensor {\n\tif t.op != nil {\n\t\tt.op = nil\n\t}\n\treturn t\n}\n\nfunc (t *Tensor) Shape() []int {\n\treturn t.shape\n}\n\n// Slice returns a slice of the tensor.\nfunc (t *Tensor) Slice(start, end int) *Tensor {\n\tif len(t.shape) < 1 {\n\t\tpanic(\"slice requires at least 1-D tensor\")\n\t}\n\tif start < 0 || end > t.shape[0] {\n\t\tpanic(\"slice out of range\")\n\t}\n\tsubSize := 1\n\tfor i := 1; i < len(t.shape); i++ {\n\t\tsubSize *= t.shape[i]\n\t}\n\treturn &Tensor{\n\t\tdata:  t.data[start*subSize : end*subSize],\n\t\tshape: append([]int{end - start}, t.shape[1:]...),\n\t}\n}\n\nfunc (t *Tensor) SliceIndices(indices ...int) *Tensor {\n\tshape := []int{len(indices)}\n\tsubSize := 1\n\tfor i := range t.shape[1:] {\n\t\tshape = append(shape, t.shape[i+1])\n\t\tsubSize *= t.shape[i+1]\n\t}\n\tdata := make([]float32, len(indices)*subSize)\n\tfor i, index := range indices {\n\t\tcopy(data[i*subSize:(i+1)*subSize], t.data[index*subSize:(index+1)*subSize])\n\t}\n\treturn &Tensor{\n\t\tdata:  data,\n\t\tshape: shape,\n\t}\n}\n\n// Get returns the value of the tensor at the given indices.\nfunc (t *Tensor) Get(indices ...int) float32 {\n\tif len(indices) != len(t.shape) {\n\t\tpanic(\"the number of indices does not match the shape of the tensor\")\n\t}\n\tindex := 0\n\tfor i := range indices {\n\t\tif indices[i] < 0 || indices[i] >= t.shape[i] {\n\t\t\tpanic(\"index out of range\")\n\t\t}\n\t\tindex = index*t.shape[i] + indices[i]\n\t}\n\treturn t.data[index]\n}\n\nfunc (t *Tensor) String() string {\n\t// Print scalar value\n\tif len(t.shape) == 0 {\n\t\treturn fmt.Sprint(t.data[0])\n\t}\n\n\tbuilder := strings.Builder{}\n\tbuilder.WriteString(\"[\")\n\tif len(t.data) <= 10 {\n\t\tfor i := 0; i < len(t.data); i++ {\n\t\t\tfmt.Fprint(&builder, t.data[i])\n\t\t\tif i != len(t.data)-1 {\n\t\t\t\tbuilder.WriteString(\", \")\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tfmt.Fprint(&builder, t.data[i])\n\t\t\tbuilder.WriteString(\", \")\n\t\t}\n\t\tbuilder.WriteString(\"..., \")\n\t\tfor i := len(t.data) - 5; i < len(t.data); i++ {\n\t\t\tfmt.Fprint(&builder, t.data[i])\n\t\t\tif i != len(t.data)-1 {\n\t\t\t\tbuilder.WriteString(\", \")\n\t\t\t}\n\t\t}\n\t}\n\tbuilder.WriteString(\"]\")\n\treturn builder.String()\n}\n\nfunc (t *Tensor) Backward() {\n\tt.grad = Ones(t.shape...)\n\tops := &opHeap{t.op}\n\tseen := mapset.NewSet[op](t.op)\n\tfor ops.Len() > 0 {\n\t\top := heap.Pop(ops).(op)\n\t\tinputs, output := op.inputsAndOutput()\n\t\tgrads := op.backward(output.grad)\n\t\tfor i := range grads {\n\t\t\tif !slices.Equal(inputs[i].shape, grads[i].shape) {\n\t\t\t\tpanic(fmt.Sprintf(\"%s: shape %v does not match shape %v\", op.String(), inputs[i].shape, grads[i].shape))\n\t\t\t}\n\t\t\tif inputs[i].grad == nil {\n\t\t\t\tinputs[i].grad = grads[i]\n\t\t\t} else {\n\t\t\t\tinputs[i].grad.add(grads[i])\n\t\t\t}\n\t\t\tif inputs[i].op != nil && !seen.Contains(inputs[i].op) {\n\t\t\t\theap.Push(ops, inputs[i].op)\n\t\t\t\tseen.Add(inputs[i].op)\n\t\t\t}\n\t\t}\n\t\toutput.grad = nil\n\t}\n}\n\nfunc (t *Tensor) Grad() *Tensor {\n\treturn t.grad\n}\n\nfunc (t *Tensor) Data() []float32 {\n\treturn t.data\n}\n\nfunc (t *Tensor) clone() *Tensor {\n\tnewData := make([]float32, len(t.data))\n\tcopy(newData, t.data)\n\treturn &Tensor{\n\t\tdata:  newData,\n\t\tshape: t.shape,\n\t}\n}\n\nfunc (t *Tensor) add(other *Tensor) *Tensor {\n\twSize := 1\n\tfor i := range other.shape {\n\t\twSize *= other.shape[i]\n\t}\n\tif wSize == 1 {\n\t\tfloats.AddConst(t.data, other.data[0])\n\t} else {\n\t\tfor i := 0; i < len(t.data); i += wSize {\n\t\t\tfloats.Add(t.data[i:i+wSize], other.data)\n\t\t}\n\t}\n\treturn t\n}\n\n// sub returns the element-wise addition of two tensors. The shape\n// of the second tensor must be a suffix sequence of the shape of\n// the first tensor: (...,m,n) - (m,n) = (...,m,n).\nfunc (t *Tensor) sub(other *Tensor) *Tensor {\n\twSize := 1\n\tfor i := range other.shape {\n\t\twSize *= other.shape[i]\n\t}\n\tfor i := range t.data {\n\t\tt.data[i] -= other.data[i%wSize]\n\t}\n\treturn t\n}\n\n// bSub returns the element-wise addition of two tensors. The shape\n// of the second tensor must be a prefix sequence of the shape of\n// the first tensor: (m,n,...) - (m,n) = (m,n,...).\nfunc (t *Tensor) bSub(other *Tensor) *Tensor {\n\tbSize := 1\n\tfor i := range t.shape {\n\t\tbSize *= t.shape[i]\n\t}\n\tfor i := range other.shape {\n\t\tbSize /= other.shape[i]\n\t}\n\tfor i := range t.data {\n\t\tt.data[i] -= other.data[i/bSize]\n\t}\n\treturn t\n}\n\nfunc (t *Tensor) mul(other *Tensor) *Tensor {\n\twSize := 1\n\tfor i := range other.shape {\n\t\twSize *= other.shape[i]\n\t}\n\tfor i := range t.data {\n\t\tt.data[i] *= other.data[i%wSize]\n\t}\n\treturn t\n}\n\n// div returns the element-wise division of two tensors. The shape\n// of the second tensor must be a suffix sequence of the shape of\n// the first tensor: (...,m,n) / (m,n) = (...,m,n).\nfunc (t *Tensor) div(other *Tensor) *Tensor {\n\twSize := 1\n\tfor i := range other.shape {\n\t\twSize *= other.shape[i]\n\t}\n\tfor i := range t.data {\n\t\tt.data[i] /= other.data[i%wSize]\n\t}\n\treturn t\n}\n\n// bDiv returns the element-wise division of two tensors. The shape\n// of the second tensor must be a prefix sequence of the shape of\n// the first tensor: (m,n,...) / (m,n) = (m,n,...).\nfunc (t *Tensor) bDiv(other *Tensor) *Tensor {\n\tbSize := 1\n\tfor i := range t.shape {\n\t\tbSize *= t.shape[i]\n\t}\n\tfor i := range other.shape {\n\t\tbSize /= other.shape[i]\n\t}\n\tfor i := range t.data {\n\t\tt.data[i] /= other.data[i/bSize]\n\t}\n\treturn t\n}\n\nfunc (t *Tensor) square() *Tensor {\n\tfloats.MulTo(t.data, t.data, t.data)\n\treturn t\n}\n\nfunc (t *Tensor) pow(other *Tensor) *Tensor {\n\twSize := 1\n\tfor i := range other.shape {\n\t\twSize *= other.shape[i]\n\t}\n\tfor i := range t.data {\n\t\tt.data[i] = math32.Pow(t.data[i], other.data[i%wSize])\n\t}\n\treturn t\n}\n\nfunc (t *Tensor) exp() *Tensor {\n\tfor i := range t.data {\n\t\tt.data[i] = float32(math.Exp(float64(t.data[i])))\n\t}\n\treturn t\n}\n\nfunc (t *Tensor) log() *Tensor {\n\tfor i := range t.data {\n\t\tt.data[i] = math32.Log(t.data[i])\n\t}\n\treturn t\n}\n\nfunc (t *Tensor) sin() *Tensor {\n\tfor i := range t.data {\n\t\tt.data[i] = math32.Sin(t.data[i])\n\t}\n\treturn t\n}\n\nfunc (t *Tensor) cos() *Tensor {\n\tfor i := range t.data {\n\t\tt.data[i] = math32.Cos(t.data[i])\n\t}\n\treturn t\n}\n\nfunc (t *Tensor) tanh() *Tensor {\n\tfor i := range t.data {\n\t\tt.data[i] = math32.Tanh(t.data[i])\n\t}\n\treturn t\n}\n\nfunc (t *Tensor) neg() *Tensor {\n\tfor i := range t.data {\n\t\tt.data[i] = -t.data[i]\n\t}\n\treturn t\n}\n\nfunc partition(n, p int) []lo.Tuple2[int, int] {\n\t// If n is less than or equal to 0, return nil.\n\tif n <= 0 {\n\t\treturn nil\n\t}\n\n\t// If p is less than or equal to 1, return a single part covering the whole range.\n\tif p <= 1 {\n\t\treturn []lo.Tuple2[int, int]{{A: 0, B: n}}\n\t}\n\n\t// If n is less than or equal to p, return each index as a separate part.\n\tif n <= p {\n\t\treturn lo.Map(lo.Range(n), func(i int, _ int) lo.Tuple2[int, int] {\n\t\t\treturn lo.Tuple2[int, int]{A: i, B: i + 1}\n\t\t})\n\t}\n\n\t// Otherwise, split n into p parts as evenly as possible.\n\tminPartSize := n / p\n\tmaxPartCount := n % p\n\tparts := make([]lo.Tuple2[int, int], 0, p)\n\tfor i := 0; i < n; {\n\t\tpartSize := minPartSize\n\t\tif maxPartCount > 0 {\n\t\t\tpartSize++\n\t\t\tmaxPartCount--\n\t\t}\n\t\tend := i + partSize\n\t\tif end > n {\n\t\t\tend = n\n\t\t}\n\t\tparts = append(parts, lo.Tuple2[int, int]{A: i, B: end})\n\t\ti = end\n\t}\n\treturn parts\n}\n\nfunc (t *Tensor) matMul(other *Tensor, transpose1, transpose2 bool, jobs int) *Tensor {\n\tif len(t.shape) != 2 || len(other.shape) != 2 {\n\t\tpanic(\"matMul requires 2-D tensors\")\n\t}\n\tvar m, n, k int\n\tvar result []float32\n\tvar wg sync.WaitGroup\n\tif !transpose1 && !transpose2 {\n\t\tif t.shape[1] != other.shape[0] {\n\t\t\tpanic(fmt.Sprintf(\"matMul requires the shapes of tensors are compatible, but got %v and %v\", t.shape, other.shape))\n\t\t}\n\t\tm, n, k = t.shape[0], other.shape[1], t.shape[1]\n\t\tresult = make([]float32, m*n)\n\t\tfor _, p := range partition(m, jobs) {\n\t\t\twg.Go(func() {\n\t\t\t\tfloats.MM(transpose1, transpose2, p.B-p.A, n, k, t.data[p.A*k:], k, other.data, n, result[p.A*n:], n)\n\t\t\t})\n\t\t}\n\t} else if transpose1 && !transpose2 {\n\t\tif t.shape[0] != other.shape[0] {\n\t\t\tpanic(fmt.Sprintf(\"matMul requires the shapes of tensors are compatible, but got %v and %v\", t.shape, other.shape))\n\t\t}\n\t\tm, n, k = t.shape[1], other.shape[1], t.shape[0]\n\t\tresult = make([]float32, m*n)\n\t\tfor _, p := range partition(m, jobs) {\n\t\t\twg.Go(func() {\n\t\t\t\tfloats.MM(transpose1, transpose2, p.B-p.A, n, k, t.data[p.A:], m, other.data, n, result[p.A*n:], n)\n\t\t\t})\n\t\t}\n\t} else if !transpose1 && transpose2 {\n\t\tif t.shape[1] != other.shape[1] {\n\t\t\tpanic(fmt.Sprintf(\"matMul requires the shapes of tensors are compatible, but got %v and %v\", t.shape, other.shape))\n\t\t}\n\t\tm, n, k = t.shape[0], other.shape[0], t.shape[1]\n\t\tresult = make([]float32, m*n)\n\t\tfor _, p := range partition(m, jobs) {\n\t\t\twg.Go(func() {\n\t\t\t\tfloats.MM(transpose1, transpose2, p.B-p.A, n, k, t.data[p.A*k:], k, other.data, k, result[p.A*n:], n)\n\t\t\t})\n\t\t}\n\t} else {\n\t\tif t.shape[0] != other.shape[1] {\n\t\t\tpanic(fmt.Sprintf(\"matMul requires the shapes of tensors are compatible, but got %v and %v\", t.shape, other.shape))\n\t\t}\n\t\tm, n, k = t.shape[1], other.shape[0], t.shape[0]\n\t\tresult = make([]float32, m*n)\n\t\tfor _, p := range partition(m, jobs) {\n\t\t\twg.Go(func() {\n\t\t\t\tfloats.MM(transpose1, transpose2, p.B-p.A, n, k, t.data[p.A:], m, other.data, k, result[p.A*n:], n)\n\t\t\t})\n\t\t}\n\t}\n\twg.Wait()\n\treturn &Tensor{\n\t\tdata:  result,\n\t\tshape: []int{m, n},\n\t}\n}\n\nfunc (t *Tensor) batchMatMul(other *Tensor, transpose1, transpose2 bool, jobs int) *Tensor {\n\tif len(t.shape) != 3 || len(other.shape) != 3 {\n\t\tpanic(\"BatchMatMul requires 3-D tensors\")\n\t}\n\tvar b, m, n, k int\n\tvar result []float32\n\tif !transpose1 && !transpose2 {\n\t\tif t.shape[0] != other.shape[0] || t.shape[2] != other.shape[1] {\n\t\t\tpanic(\"BatchMatMul requires the shapes of tensors are compatible\")\n\t\t}\n\t\tb, m, n, k = t.shape[0], t.shape[1], other.shape[2], t.shape[2]\n\t\tresult = make([]float32, b*m*n)\n\t\t_ = parallel.For(context.Background(), b, jobs, func(i int) {\n\t\t\tfloats.MM(transpose1, transpose2, m, n, k, t.data[i*m*k:(i+1)*m*k], k, other.data[i*n*k:(i+1)*n*k], n, result[i*m*n:(i+1)*m*n], n)\n\t\t})\n\t} else if transpose1 && !transpose2 {\n\t\tif t.shape[0] != other.shape[0] || t.shape[1] != other.shape[1] {\n\t\t\tpanic(\"batchMatMul requires the shapes of tensors are compatible\")\n\t\t}\n\t\tb, m, n, k = t.shape[0], t.shape[2], other.shape[2], t.shape[1]\n\t\tresult = make([]float32, b*m*n)\n\t\t_ = parallel.For(context.Background(), b, jobs, func(i int) {\n\t\t\tfloats.MM(transpose1, transpose2, m, n, k, t.data[i*m*k:(i+1)*m*k], m, other.data[i*n*k:(i+1)*n*k], n, result[i*m*n:(i+1)*m*n], n)\n\t\t})\n\t} else if !transpose1 && transpose2 {\n\t\tif t.shape[0] != other.shape[0] || t.shape[2] != other.shape[2] {\n\t\t\tpanic(\"batchMatMul requires the shapes of tensors are compatible\")\n\t\t}\n\t\tb, m, n, k = t.shape[0], t.shape[1], other.shape[1], t.shape[2]\n\t\tresult = make([]float32, b*m*n)\n\t\t_ = parallel.For(context.Background(), b, jobs, func(i int) {\n\t\t\tfloats.MM(transpose1, transpose2, m, n, k, t.data[i*m*k:(i+1)*m*k], k, other.data[i*n*k:(i+1)*n*k], k, result[i*m*n:(i+1)*m*n], n)\n\t\t})\n\t} else {\n\t\tif t.shape[0] != other.shape[0] || t.shape[1] != other.shape[2] {\n\t\t\tpanic(\"batchMatMul requires the shapes of tensors are compatible\")\n\t\t}\n\t\tb, m, n, k = t.shape[0], t.shape[2], other.shape[1], t.shape[1]\n\t\tresult = make([]float32, b*m*n)\n\t\t_ = parallel.For(context.Background(), b, jobs, func(i int) {\n\t\t\tfloats.MM(transpose1, transpose2, m, n, k, t.data[i*m*k:(i+1)*m*k], m, other.data[i*n*k:(i+1)*n*k], k, result[i*m*n:(i+1)*m*n], n)\n\t\t})\n\t}\n\treturn &Tensor{\n\t\tdata:  result,\n\t\tshape: []int{b, m, n},\n\t}\n}\n\nfunc (t *Tensor) maximum(other *Tensor) {\n\tif other.IsScalar() {\n\t\tfor i := range t.data {\n\t\t\tt.data[i] = max(t.data[i], other.data[0])\n\t\t}\n\t} else {\n\t\tfor i := range t.data {\n\t\t\tt.data[i] = max(t.data[i], other.data[i])\n\t\t}\n\t}\n}\n\nfunc (t *Tensor) gt(other *Tensor) *Tensor {\n\tif other.IsScalar() {\n\t\tfor i := range t.data {\n\t\t\tif t.data[i] > other.data[0] {\n\t\t\t\tt.data[i] = 1\n\t\t\t} else {\n\t\t\t\tt.data[i] = 0\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor i := range t.data {\n\t\t\tif t.data[i] > other.data[i] {\n\t\t\t\tt.data[i] = 1\n\t\t\t} else {\n\t\t\t\tt.data[i] = 0\n\t\t\t}\n\t\t}\n\t}\n\treturn t\n}\n\nfunc (t *Tensor) transpose() *Tensor {\n\tif len(t.shape) < 2 {\n\t\tpanic(\"transpose requires at least 2-D tensor\")\n\t}\n\tshape := make([]int, 0, len(t.shape))\n\tbatchSize := 1\n\tfor i := 0; i < len(t.shape)-2; i++ {\n\t\tbatchSize *= t.shape[i]\n\t\tshape = append(shape, t.shape[i])\n\t}\n\tm, n := t.shape[len(t.shape)-2], t.shape[len(t.shape)-1]\n\tshape = append(shape, n, m)\n\tdata := make([]float32, batchSize*m*n)\n\tfor b := 0; b < batchSize; b++ {\n\t\tfor i := 0; i < m; i++ {\n\t\t\tfor j := 0; j < n; j++ {\n\t\t\t\tdata[b*m*n+j*m+i] = t.data[b*m*n+i*n+j]\n\t\t\t}\n\t\t}\n\t}\n\treturn &Tensor{\n\t\tdata:  data,\n\t\tshape: shape,\n\t}\n}\n\nfunc (t *Tensor) max(axis int, keepDim bool) *Tensor {\n\tif axis < 0 || axis >= len(t.shape) {\n\t\tpanic(\"axis out of range\")\n\t}\n\tif len(t.shape) == 1 {\n\t\treturn NewScalar(lo.Max(t.data))\n\t}\n\tshape := make([]int, 0, len(t.shape)-1)\n\ta, b, c := 1, 1, 1\n\tfor i := 0; i < len(t.shape); i++ {\n\t\tif i < axis {\n\t\t\tshape = append(shape, t.shape[i])\n\t\t\ta *= t.shape[i]\n\t\t} else if i == axis {\n\t\t\tif keepDim {\n\t\t\t\tshape = append(shape, 1)\n\t\t\t}\n\t\t\tb = t.shape[i]\n\t\t} else {\n\t\t\tshape = append(shape, t.shape[i])\n\t\t\tc *= t.shape[i]\n\t\t}\n\t}\n\tdata := make([]float32, a*c)\n\tfor i := 0; i < a; i++ {\n\t\tfor j := 0; j < c; j++ {\n\t\t\tmaxValue := t.data[i*b*c+j]\n\t\t\tfor k := 1; k < b; k++ {\n\t\t\t\tmaxValue = max(maxValue, t.data[i*b*c+j+k*c])\n\t\t\t}\n\t\t\tdata[i*c+j] = maxValue\n\t\t}\n\t}\n\treturn &Tensor{\n\t\tdata:  data,\n\t\tshape: shape,\n\t}\n}\n\nfunc (t *Tensor) sum(axis int, keepDim bool) *Tensor {\n\tif axis < 0 || axis >= len(t.shape) {\n\t\tpanic(\"axis out of range\")\n\t}\n\tif len(t.shape) == 1 {\n\t\treturn NewScalar(lo.Sum(t.data))\n\t}\n\tshape := make([]int, 0, len(t.shape)-1)\n\ta, b, c := 1, 1, 1\n\tfor i := 0; i < len(t.shape); i++ {\n\t\tif i < axis {\n\t\t\tshape = append(shape, t.shape[i])\n\t\t\ta *= t.shape[i]\n\t\t} else if i == axis {\n\t\t\tif keepDim {\n\t\t\t\tshape = append(shape, 1)\n\t\t\t}\n\t\t\tb = t.shape[i]\n\t\t} else {\n\t\t\tshape = append(shape, t.shape[i])\n\t\t\tc *= t.shape[i]\n\t\t}\n\t}\n\tdata := make([]float32, a*c)\n\tfor i := 0; i < a; i++ {\n\t\tfor j := 0; j < c; j++ {\n\t\t\tsumValue := t.data[i*b*c+j]\n\t\t\tfor k := 1; k < b; k++ {\n\t\t\t\tsumValue += t.data[i*b*c+j+k*c]\n\t\t\t}\n\t\t\tdata[i*c+j] = sumValue\n\t\t}\n\t}\n\treturn &Tensor{\n\t\tdata:  data,\n\t\tshape: shape,\n\t}\n}\n\nfunc (t *Tensor) argmax() []int {\n\tif len(t.data) == 0 {\n\t\treturn nil\n\t}\n\tmaxValue := t.data[0]\n\tmaxIndex := 0\n\tfor i := 1; i < len(t.data); i++ {\n\t\tif t.data[i] > maxValue {\n\t\t\tmaxValue = t.data[i]\n\t\t\tmaxIndex = i\n\t\t}\n\t}\n\tindices := make([]int, len(t.shape))\n\tfor i := len(t.shape) - 1; i >= 0; i-- {\n\t\tindices[i] = maxIndex % t.shape[i]\n\t\tmaxIndex /= t.shape[i]\n\t}\n\treturn indices\n}\n\nfunc (t *Tensor) toPB() *protocol.Tensor {\n\treturn &protocol.Tensor{\n\t\tShape: lo.Map(t.shape, func(i, _ int) int32 { return int32(i) }),\n\t\tData:  t.data,\n\t}\n}\n\nfunc (t *Tensor) fromPB(pb *protocol.Tensor) {\n\tt.shape = make([]int, len(pb.Shape))\n\tfor i := range t.shape {\n\t\tt.shape[i] = int(pb.Shape[i])\n\t}\n\tt.data = pb.Data\n}\n\nfunc NormalInit(t *Tensor, mean, std float32) {\n\tfor i := range t.data {\n\t\tt.data[i] = float32(rand.NormFloat64())*(std) + (mean)\n\t}\n}\n"
  },
  {
    "path": "common/nn/tensor_test.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage nn\n\nimport (\n\t\"fmt\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"testing\"\n)\n\nfunc TestTensor_Slice(t *testing.T) {\n\tx := Rand(3, 4, 5)\n\ty := x.Slice(1, 3)\n\tassert.Equal(t, []int{2, 4, 5}, y.Shape())\n\tfor i := 0; i < 2; i++ {\n\t\tfor j := 0; j < 4; j++ {\n\t\t\tfor k := 0; k < 5; k++ {\n\t\t\t\tassert.Equal(t, x.Get(i+1, j, k), y.Get(i, j, k))\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestTensor_SliceIndices(t *testing.T) {\n\tx := NewTensor([]float32{1, 2, 3, 4, 5, 6}, 3, 2)\n\ty := x.SliceIndices(2, 0)\n\tassert.Equal(t, []int{2, 2}, y.Shape())\n\tassert.Equal(t, []float32{5, 6, 1, 2}, y.Data())\n}\n\nfunc TestTensor_Max(t *testing.T) {\n\tx := NewTensor([]float32{3, 2, 5, 6, 0, 0}, 6)\n\ty := x.max(0, false)\n\tassert.Len(t, y.shape, 0)\n\tassert.Equal(t, []float32{6}, y.data)\n\n\tassert.Panics(t, func() { x.max(-1, false) })\n\tassert.Panics(t, func() { x.max(2, false) })\n\n\tx = NewTensor([]float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, 3, 2, 2)\n\ty = x.max(1, false)\n\tassert.Equal(t, []int{3, 2}, y.shape)\n\tassert.Equal(t, []float32{3, 4, 7, 8, 11, 12}, y.data)\n\ty = x.max(1, true)\n\tassert.Equal(t, []int{3, 1, 2}, y.shape)\n\tassert.Equal(t, []float32{3, 4, 7, 8, 11, 12}, y.data)\n}\n\nfunc TestTensor_Sum(t *testing.T) {\n\tx := NewTensor([]float32{1, 2, 3, 4, 5, 6}, 6)\n\ty := x.sum(0, false)\n\tassert.Len(t, y.shape, 0)\n\tassert.Equal(t, []float32{21}, y.data)\n\n\tassert.Panics(t, func() { x.sum(-1, false) })\n\tassert.Panics(t, func() { x.sum(2, false) })\n\n\tx = NewTensor([]float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, 3, 2, 2)\n\ty = x.sum(1, false)\n\tassert.Equal(t, []int{3, 2}, y.shape)\n\tassert.Equal(t, []float32{4, 6, 12, 14, 20, 22}, y.data)\n\ty = x.sum(1, true)\n\tassert.Equal(t, []int{3, 1, 2}, y.shape)\n\tassert.Equal(t, []float32{4, 6, 12, 14, 20, 22}, y.data)\n}\n\nfunc TestTensor_Transpose(t *testing.T) {\n\tx := NewTensor([]float32{1, 2, 3, 4, 5, 6}, 3, 2)\n\ty := x.transpose()\n\tassert.Equal(t, []int{2, 3}, y.Shape())\n\tassert.Equal(t, []float32{1, 3, 5, 2, 4, 6}, y.Data())\n}\n\nfunc (t *Tensor) matMulLegacy(other *Tensor, transpose1, transpose2 bool) *Tensor {\n\tif !transpose1 && !transpose2 {\n\t\tif len(t.shape) != 2 || len(other.shape) != 2 {\n\t\t\tpanic(\"matMul requires 2-D tensors\")\n\t\t}\n\t\tif t.shape[1] != other.shape[0] {\n\t\t\tpanic(\"matMul requires the shapes of tensors are compatible\")\n\t\t}\n\t\tm, n, p := t.shape[0], t.shape[1], other.shape[1]\n\t\tresult := make([]float32, m*p)\n\t\tfor i := 0; i < m; i++ {\n\t\t\tfor j := 0; j < p; j++ {\n\t\t\t\tfor k := 0; k < n; k++ {\n\t\t\t\t\tresult[i*p+j] += t.data[i*n+k] * other.data[k*p+j]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn &Tensor{\n\t\t\tdata:  result,\n\t\t\tshape: []int{m, p},\n\t\t}\n\t} else if transpose1 && !transpose2 {\n\t\tif len(t.shape) != 2 || len(other.shape) != 2 {\n\t\t\tpanic(\"matMul requires 2-D tensors\")\n\t\t}\n\t\tif t.shape[0] != other.shape[0] {\n\t\t\tpanic(\"matMul requires the shapes of tensors are compatible\")\n\t\t}\n\t\tm, n, p := t.shape[1], t.shape[0], other.shape[1]\n\t\tresult := make([]float32, m*p)\n\t\tfor i := 0; i < m; i++ {\n\t\t\tfor j := 0; j < p; j++ {\n\t\t\t\tfor k := 0; k < n; k++ {\n\t\t\t\t\tresult[i*p+j] += t.data[k*m+i] * other.data[k*p+j]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn &Tensor{\n\t\t\tdata:  result,\n\t\t\tshape: []int{m, p},\n\t\t}\n\t} else if !transpose1 && transpose2 {\n\t\tif len(t.shape) != 2 || len(other.shape) != 2 {\n\t\t\tpanic(\"matMul requires 2-D tensors\")\n\t\t}\n\t\tif t.shape[1] != other.shape[1] {\n\t\t\tpanic(\"matMul requires the shapes of tensors are compatible\")\n\t\t}\n\t\tm, n, p := t.shape[0], t.shape[1], other.shape[0]\n\t\tresult := make([]float32, m*p)\n\t\tfor i := 0; i < m; i++ {\n\t\t\tfor j := 0; j < p; j++ {\n\t\t\t\tfor k := 0; k < n; k++ {\n\t\t\t\t\tresult[i*p+j] += t.data[i*n+k] * other.data[j*n+k]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn &Tensor{\n\t\t\tdata:  result,\n\t\t\tshape: []int{m, p},\n\t\t}\n\t} else {\n\t\tif len(t.shape) != 2 || len(other.shape) != 2 {\n\t\t\tpanic(\"matMul requires 2-D tensors\")\n\t\t}\n\t\tif t.shape[0] != other.shape[0] {\n\t\t\tpanic(\"matMul requires the shapes of tensors are compatible\")\n\t\t}\n\t\tm, n, p := t.shape[1], t.shape[0], other.shape[1]\n\t\tresult := make([]float32, m*p)\n\t\tfor i := 0; i < m; i++ {\n\t\t\tfor j := 0; j < p; j++ {\n\t\t\t\tfor k := 0; k < n; k++ {\n\t\t\t\t\tresult[i*p+j] += t.data[k*m+i] * other.data[j*n+k]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn &Tensor{\n\t\t\tdata:  result,\n\t\t\tshape: []int{m, p},\n\t\t}\n\t}\n}\n\nfunc (t *Tensor) batchMatMulLegacy(other *Tensor, transpose1, transpose2 bool) *Tensor {\n\tif !transpose1 && !transpose2 {\n\t\tif len(t.shape) != 3 || len(other.shape) != 3 {\n\t\t\tpanic(\"BatchMatMul requires 3-D tensors\")\n\t\t}\n\t\tif t.shape[0] != other.shape[0] || t.shape[2] != other.shape[1] {\n\t\t\tpanic(\"BatchMatMul requires the shapes of tensors are compatible\")\n\t\t}\n\t\tm, n, p := t.shape[0], t.shape[1], other.shape[2]\n\t\tresult := make([]float32, m*n*p)\n\t\tfor i := 0; i < m; i++ {\n\t\t\tfor j := 0; j < n; j++ {\n\t\t\t\tfor k := 0; k < p; k++ {\n\t\t\t\t\tfor l := 0; l < t.shape[2]; l++ {\n\t\t\t\t\t\tresult[i*n*p+j*p+k] += t.data[i*n*t.shape[2]+j*t.shape[2]+l] * other.data[i*other.shape[1]*other.shape[2]+l*other.shape[2]+k]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn &Tensor{\n\t\t\tdata:  result,\n\t\t\tshape: []int{m, n, p},\n\t\t}\n\t} else if transpose1 && !transpose2 {\n\t\tif len(t.shape) != 3 || len(other.shape) != 3 {\n\t\t\tpanic(\"batchMatMul requires 3-D tensors\")\n\t\t}\n\t\tif t.shape[0] != other.shape[0] || t.shape[1] != other.shape[1] {\n\t\t\tpanic(\"batchMatMul requires the shapes of tensors are compatible\")\n\t\t}\n\t\tm, n, p := t.shape[0], t.shape[2], other.shape[2]\n\t\tresult := make([]float32, m*n*p)\n\t\tfor i := 0; i < m; i++ {\n\t\t\tfor j := 0; j < n; j++ {\n\t\t\t\tfor k := 0; k < p; k++ {\n\t\t\t\t\tfor l := 0; l < t.shape[1]; l++ {\n\t\t\t\t\t\tresult[i*n*p+j*p+k] += t.data[i*t.shape[1]*t.shape[2]+l*t.shape[2]+j] * other.data[i*other.shape[1]*other.shape[2]+l*other.shape[2]+k]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn &Tensor{\n\t\t\tdata:  result,\n\t\t\tshape: []int{m, n, p},\n\t\t}\n\t} else if !transpose1 && transpose2 {\n\t\tif len(t.shape) != 3 || len(other.shape) != 3 {\n\t\t\tpanic(\"batchMatMul requires 3-D tensors\")\n\t\t}\n\t\tif t.shape[0] != other.shape[0] || t.shape[2] != other.shape[2] {\n\t\t\tpanic(\"batchMatMul requires the shapes of tensors are compatible\")\n\t\t}\n\t\tm, n, p := t.shape[0], t.shape[1], other.shape[1]\n\t\tresult := make([]float32, m*n*p)\n\t\tfor i := 0; i < m; i++ {\n\t\t\tfor j := 0; j < n; j++ {\n\t\t\t\tfor k := 0; k < p; k++ {\n\t\t\t\t\tfor l := 0; l < t.shape[2]; l++ {\n\t\t\t\t\t\tresult[i*n*p+j*p+k] += t.data[i*n*t.shape[2]+j*t.shape[2]+l] * other.data[i*other.shape[1]*other.shape[2]+k*other.shape[2]+l]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn &Tensor{\n\t\t\tdata:  result,\n\t\t\tshape: []int{m, n, p},\n\t\t}\n\t} else {\n\t\tif len(t.shape) != 3 || len(other.shape) != 3 {\n\t\t\tpanic(\"batchMatMul requires 3-D tensors\")\n\t\t}\n\t\tif t.shape[0] != other.shape[0] || t.shape[2] != other.shape[2] {\n\t\t\tpanic(\"batchMatMul requires the shapes of tensors are compatible\")\n\t\t}\n\t\tm, n, p := t.shape[1], t.shape[2], other.shape[2]\n\t\tresult := make([]float32, m*n*p)\n\t\tfor i := 0; i < m; i++ {\n\t\t\tfor j := 0; j < n; j++ {\n\t\t\t\tfor k := 0; k < p; k++ {\n\t\t\t\t\tfor l := 0; l < t.shape[0]; l++ {\n\t\t\t\t\t\tresult[i*n*p+j*p+k] += t.data[l*t.shape[1]*t.shape[2]+i*t.shape[2]+j] * other.data[l*other.shape[1]*other.shape[2]+j*other.shape[2]+k]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn &Tensor{\n\t\t\tdata:  result,\n\t\t\tshape: []int{m, n, p},\n\t\t}\n\t}\n}\n\nfunc BenchmarkMatMulLegacy64(b *testing.B) {\n\tx := Rand(64, 64)\n\ty := Rand(64, 64)\n\tfor t1 := 0; t1 < 2; t1++ {\n\t\tfor t2 := 0; t2 < 2; t2++ {\n\t\t\tb.Run(fmt.Sprintf(\"(%d,%d)\", t1, t2), func(b *testing.B) {\n\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\tx.matMulLegacy(y, t1 == 1, t2 == 1)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc BenchmarkMatMul64(b *testing.B) {\n\tx := Rand(64, 64)\n\ty := Rand(64, 64)\n\tfor t1 := 0; t1 < 2; t1++ {\n\t\tfor t2 := 0; t2 < 2; t2++ {\n\t\t\tb.Run(fmt.Sprintf(\"(%d,%d)\", t1, t2), func(b *testing.B) {\n\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\tx.matMul(y, t1 == 1, t2 == 1, 0)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc BenchmarkBatchMatMulLegacy64(b *testing.B) {\n\tx := Rand(64, 64, 64)\n\ty := Rand(64, 64, 64)\n\tfor t1 := 0; t1 < 2; t1++ {\n\t\tfor t2 := 0; t2 < 2; t2++ {\n\t\t\tb.Run(fmt.Sprintf(\"(%d,%d)\", t1, t2), func(b *testing.B) {\n\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\tx.batchMatMulLegacy(y, t1 == 1, t2 == 1)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc BenchmarkBatchMatMul64(b *testing.B) {\n\tx := Rand(64, 64, 64)\n\ty := Rand(64, 64, 64)\n\tfor t1 := 0; t1 < 2; t1++ {\n\t\tfor t2 := 0; t2 < 2; t2++ {\n\t\t\tb.Run(fmt.Sprintf(\"(%d,%d)\", t1, t2), func(b *testing.B) {\n\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\tx.batchMatMul(y, t1 == 1, t2 == 1, 0)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "common/parallel/parallel.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage parallel\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\t\"github.com/gorse-io/gorse/common/util\"\n\t\"github.com/juju/errors\"\n\t\"github.com/samber/lo\"\n)\n\nconst chanSize = 1024\n\n/* Parallel Schedulers */\n\n// Parallel schedules and runs tasks in parallel. nTask is the number of tasks. nJob is\n// the number of executors. worker is the executed function which passed a range of task\n// Names (begin, end). The ctx argument allows callers to cancel outstanding work.\nfunc Parallel(ctx context.Context, nJobs, nWorkers int, worker func(workerId, jobId int) error) error {\n\tif nWorkers <= 1 {\n\t\tfor i := 0; i < nJobs; i++ {\n\t\t\tif err := ctx.Err(); err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t\tif err := worker(0, i); err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tc := make(chan int, chanSize)\n\t\t// producer\n\t\tgo func() {\n\t\t\tdefer close(c)\n\t\t\tfor i := 0; i < nJobs; i++ {\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\treturn\n\t\t\t\tcase c <- i:\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t\t// consumer\n\t\tvar wg sync.WaitGroup\n\t\terrs := make([]error, nJobs)\n\t\tfor j := 0; j < nWorkers; j++ {\n\t\t\t// start workers\n\t\t\tworkerId := j\n\t\t\twg.Go(func() {\n\t\t\t\tdefer util.CheckPanic()\n\t\t\t\tfor {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\t\treturn\n\t\t\t\t\tcase jobId, ok := <-c:\n\t\t\t\t\t\tif !ok {\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif err := ctx.Err(); err != nil {\n\t\t\t\t\t\t\terrs[jobId] = err\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// run job\n\t\t\t\t\t\tif err := worker(workerId, jobId); err != nil {\n\t\t\t\t\t\t\terrs[jobId] = err\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t\twg.Wait()\n\t\t// check errors\n\t\tfor _, err := range errs {\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t}\n\t}\n\treturn ctx.Err()\n}\n\nfunc For(ctx context.Context, nJobs, nWorkers int, worker func(int)) error {\n\tif nWorkers <= 1 {\n\t\tfor i := 0; i < nJobs; i++ {\n\t\t\tif err := ctx.Err(); err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t\tworker(i)\n\t\t}\n\t} else {\n\t\tc := make(chan int, chanSize)\n\t\t// producer\n\t\tgo func() {\n\t\t\tdefer close(c)\n\t\t\tfor i := 0; i < nJobs; i++ {\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\treturn\n\t\t\t\tcase c <- i:\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t\t// consumer\n\t\tvar wg sync.WaitGroup\n\t\tfor j := 0; j < nWorkers; j++ {\n\t\t\t// start workers\n\t\t\twg.Go(func() {\n\t\t\t\tfor {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\t\treturn\n\t\t\t\t\tcase jobId, ok := <-c:\n\t\t\t\t\t\tif !ok {\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif err := ctx.Err(); err != nil {\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tworker(jobId)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t\twg.Wait()\n\t}\n\treturn ctx.Err()\n}\n\nfunc ForEach[T any](ctx context.Context, a []T, nWorkers int, worker func(int, T)) error {\n\tif nWorkers <= 1 {\n\t\tfor i, v := range a {\n\t\t\tif err := ctx.Err(); err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t\tworker(i, v)\n\t\t}\n\t} else {\n\t\tc := make(chan lo.Tuple2[int, T], chanSize)\n\t\t// producer\n\t\tgo func() {\n\t\t\tdefer close(c)\n\t\t\tfor i, v := range a {\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\treturn\n\t\t\t\tcase c <- lo.Tuple2[int, T]{A: i, B: v}:\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t\t// consumer\n\t\tvar wg sync.WaitGroup\n\t\tfor j := 0; j < nWorkers; j++ {\n\t\t\t// start workers\n\t\t\twg.Go(func() {\n\t\t\t\tfor {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\t\treturn\n\t\t\t\t\tcase job, ok := <-c:\n\t\t\t\t\t\tif !ok {\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif err := ctx.Err(); err != nil {\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tworker(job.A, job.B)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t\twg.Wait()\n\t}\n\treturn ctx.Err()\n}\n\n// Split a slice into n slices and keep the order of elements.\nfunc Split[T any](a []T, n int) [][]T {\n\tif len(a) == 0 {\n\t\treturn nil\n\t}\n\tif n > len(a) {\n\t\tn = len(a)\n\t}\n\tminChunkSize := len(a) / n\n\tmaxChunkNum := len(a) % n\n\tchunks := make([][]T, n)\n\tfor i, j := 0, 0; i < n; i++ {\n\t\tchunkSize := minChunkSize\n\t\tif i < maxChunkNum {\n\t\t\tchunkSize++\n\t\t}\n\t\tchunks[i] = a[j : j+chunkSize]\n\t\tj += chunkSize\n\t}\n\treturn chunks\n}\n\ntype Context struct {\n\tsem         chan struct{}\n\tdetachedSem chan struct{}\n\tdetached    bool\n}\n\nfunc (ctx *Context) Detach() {\n\tif ctx == nil || ctx.detached {\n\t\treturn\n\t}\n\tctx.detachedSem <- struct{}{}\n\tctx.detached = true\n\t<-ctx.sem\n}\n\nfunc (ctx *Context) Attach() {\n\tif ctx == nil || !ctx.detached {\n\t\treturn\n\t}\n\tctx.detached = false\n\t<-ctx.detachedSem\n\tctx.sem <- struct{}{}\n}\n\nfunc Detachable(ctx context.Context, nJobs, nWorkers, nMaxDetached int, worker func(*Context, int)) error {\n\tsem := make(chan struct{}, nWorkers)\n\tdetachedSem := make(chan struct{}, nMaxDetached)\n\tvar wg sync.WaitGroup\n\tfor i := 0; i < nJobs; i++ {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\twg.Wait()\n\t\t\treturn ctx.Err()\n\t\tcase sem <- struct{}{}:\n\t\t}\n\n\t\twg.Go(func() {\n\t\t\tif ctx.Err() != nil {\n\t\t\t\t<-sem\n\t\t\t\treturn\n\t\t\t}\n\t\t\tc := &Context{sem: sem, detachedSem: detachedSem}\n\t\t\tworker(c, i)\n\t\t\tif c.detached {\n\t\t\t\t<-c.detachedSem\n\t\t\t} else {\n\t\t\t\t<-sem\n\t\t\t}\n\t\t})\n\t}\n\twg.Wait()\n\treturn ctx.Err()\n}\n"
  },
  {
    "path": "common/parallel/parallel_test.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage parallel\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"testing/synctest\"\n\t\"time\"\n\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/gorse-io/gorse/common/util\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestParallel(t *testing.T) {\n\tsynctest.Test(t, func(t *testing.T) {\n\t\ta := util.RangeInt(10000)\n\t\tb := make([]int, len(a))\n\t\tworkerIds := make([]int, len(a))\n\t\t// multiple threads\n\t\t_ = Parallel(t.Context(), len(a), 4, func(workerId, jobId int) error {\n\t\t\tb[jobId] = a[jobId]\n\t\t\tworkerIds[jobId] = workerId\n\t\t\ttime.Sleep(time.Microsecond)\n\t\t\treturn nil\n\t\t})\n\t\tworkersSet := mapset.NewSet(workerIds...)\n\t\tassert.Equal(t, a, b)\n\t\tassert.GreaterOrEqual(t, 4, workersSet.Cardinality())\n\t\tassert.Less(t, 1, workersSet.Cardinality())\n\t\t// single thread\n\t\t_ = Parallel(t.Context(), len(a), 1, func(workerId, jobId int) error {\n\t\t\tb[jobId] = a[jobId]\n\t\t\tworkerIds[jobId] = workerId\n\t\t\treturn nil\n\t\t})\n\t\tworkersSet = mapset.NewSet(workerIds...)\n\t\tassert.Equal(t, a, b)\n\t\tassert.Equal(t, 1, workersSet.Cardinality())\n\t})\n}\n\nfunc TestFor(t *testing.T) {\n\tsynctest.Test(t, func(t *testing.T) {\n\t\t// multiple threads\n\t\ta := util.RangeInt(10000)\n\t\tb := make([]int, len(a))\n\t\terr := For(t.Context(), len(a), 4, func(jobId int) {\n\t\t\tb[jobId] = a[jobId]\n\t\t\ttime.Sleep(time.Microsecond)\n\t\t})\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, a, b)\n\t\t// single thread\n\t\terr = For(t.Context(), len(a), 1, func(jobId int) {\n\t\t\tb[jobId] = a[jobId]\n\t\t\ttime.Sleep(time.Microsecond)\n\t\t})\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, a, b)\n\t})\n}\n\nfunc TestForCancel(t *testing.T) {\n\tsynctest.Test(t, func(t *testing.T) {\n\t\tctx, cancel := context.WithCancel(t.Context())\n\t\tvar count atomic.Int32\n\n\t\terr := For(ctx, 1000, 4, func(jobId int) {\n\t\t\tif jobId == 0 {\n\t\t\t\tcancel()\n\t\t\t}\n\t\t\tcount.Add(1)\n\t\t\ttime.Sleep(100 * time.Microsecond)\n\t\t})\n\n\t\tassert.ErrorIs(t, err, context.Canceled)\n\t\tassert.Less(t, int(count.Load()), 1000)\n\t})\n}\n\nfunc TestForEach(t *testing.T) {\n\tsynctest.Test(t, func(t *testing.T) {\n\t\ta := util.RangeInt(10000)\n\t\tb := make([]int, len(a))\n\t\t// multiple threads\n\t\terr := ForEach(t.Context(), a, 4, func(i, v int) {\n\t\t\tassert.Equal(t, i, v)\n\t\t\tb[i] = v\n\t\t\ttime.Sleep(time.Microsecond)\n\t\t})\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, a, b)\n\t\t// single thread\n\t\terr = ForEach(t.Context(), a, 1, func(i, v int) {\n\t\t\tassert.Equal(t, i, v)\n\t\t\tb[i] = v\n\t\t\ttime.Sleep(time.Microsecond)\n\t\t})\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, a, b)\n\t})\n}\n\nfunc TestForEachCancel(t *testing.T) {\n\tsynctest.Test(t, func(t *testing.T) {\n\t\tctx, cancel := context.WithCancel(t.Context())\n\t\tvar count atomic.Int32\n\n\t\terr := ForEach(ctx, util.RangeInt(1000), 4, func(i, v int) {\n\t\t\tif i == 0 {\n\t\t\t\tcancel()\n\t\t\t}\n\t\t\tcount.Add(1)\n\t\t\ttime.Sleep(100 * time.Microsecond)\n\t\t})\n\n\t\tassert.ErrorIs(t, err, context.Canceled)\n\t\tassert.Less(t, int(count.Load()), 1000)\n\t})\n}\n\nfunc TestParallelFail(t *testing.T) {\n\t// multiple threads\n\terr := Parallel(t.Context(), 10000, 4, func(workerId, jobId int) error {\n\t\tif jobId%2 == 1 {\n\t\t\treturn fmt.Errorf(\"error from %d\", jobId)\n\t\t}\n\t\treturn nil\n\t})\n\tassert.Error(t, err)\n\t// single thread\n\terr = Parallel(t.Context(), 10000, 1, func(workerId, jobId int) error {\n\t\tif jobId%2 == 1 {\n\t\t\treturn fmt.Errorf(\"error from %d\", jobId)\n\t\t}\n\t\treturn nil\n\t})\n\tassert.Error(t, err)\n}\n\nfunc TestParallelCancel(t *testing.T) {\n\tsynctest.Test(t, func(t *testing.T) {\n\t\tctx, cancel := context.WithCancel(t.Context())\n\t\tvar count atomic.Int32\n\n\t\terr := Parallel(ctx, 10000, 4, func(_, jobId int) error {\n\t\t\tif jobId == 0 {\n\t\t\t\tcancel()\n\t\t\t}\n\t\t\tcount.Add(1)\n\t\t\ttime.Sleep(time.Second)\n\t\t\treturn nil\n\t\t})\n\n\t\tassert.ErrorIs(t, err, context.Canceled)\n\t\tassert.Less(t, int(count.Load()), 10000)\n\t})\n}\n\nfunc TestSplit(t *testing.T) {\n\ta := []int{1, 2, 3, 4, 5, 6}\n\tb := Split(a, 3)\n\tassert.Equal(t, [][]int{{1, 2}, {3, 4}, {5, 6}}, b)\n\n\ta = []int{1, 2, 3, 4, 5, 6, 7}\n\tb = Split(a, 3)\n\tassert.Equal(t, [][]int{{1, 2, 3}, {4, 5}, {6, 7}}, b)\n}\n\nfunc TestDetachable(t *testing.T) {\n\tsynctest.Test(t, func(t *testing.T) {\n\t\tstart := time.Now()\n\t\terr := Detachable(t.Context(), 100, 1, 100, func(ctx *Context, jobId int) {\n\t\t\tctx.Detach()\n\t\t\ttime.Sleep(time.Second)\n\t\t\tctx.Attach()\n\t\t})\n\t\tassert.NoError(t, err)\n\t\tassert.Less(t, time.Since(start), time.Second*2)\n\t})\n\n\tsynctest.Test(t, func(t *testing.T) {\n\t\tstart := time.Now()\n\t\terr := Detachable(t.Context(), 100, 1, 10, func(ctx *Context, jobId int) {\n\t\t\tctx.Detach()\n\t\t\ttime.Sleep(time.Second)\n\t\t\tctx.Attach()\n\t\t})\n\t\tassert.NoError(t, err)\n\t\tassert.Less(t, time.Since(start), time.Second*11)\n\t})\n}\n\nfunc TestDetachableCancel(t *testing.T) {\n\tsynctest.Test(t, func(t *testing.T) {\n\t\tctx, cancel := context.WithCancel(t.Context())\n\t\tvar count atomic.Int32\n\n\t\terr := Detachable(ctx, 100, 4, 10, func(c *Context, jobId int) {\n\t\t\tif jobId == 0 {\n\t\t\t\tcancel()\n\t\t\t}\n\t\t\tcount.Add(1)\n\t\t\ttime.Sleep(10 * time.Millisecond)\n\t\t})\n\n\t\tassert.ErrorIs(t, err, context.Canceled)\n\t\tassert.Less(t, int(count.Load()), 20)\n\t})\n}\n"
  },
  {
    "path": "common/parallel/ratelimit.go",
    "content": "package parallel\n\nimport (\n\t\"time\"\n\n\t\"github.com/juju/ratelimit\"\n)\n\nvar (\n\tChatCompletionBackoff                     = time.Duration(0)\n\tChatCompletionRequestsLimiter RateLimiter = &Unlimited{}\n\tChatCompletionTokensLimiter   RateLimiter = &Unlimited{}\n\tEmbeddingBackoff                          = time.Duration(0)\n\tEmbeddingRequestsLimiter      RateLimiter = &Unlimited{}\n\tEmbeddingTokensLimiter        RateLimiter = &Unlimited{}\n)\n\nfunc InitChatCompletionLimiters(rpm, tpm int) {\n\tif rpm > 0 {\n\t\tChatCompletionBackoff = time.Minute / time.Duration(rpm)\n\t\tChatCompletionRequestsLimiter = ratelimit.NewBucketWithQuantum(time.Second, int64(rpm/60), int64(rpm/60))\n\t}\n\tif tpm > 0 {\n\t\tChatCompletionTokensLimiter = ratelimit.NewBucketWithQuantum(time.Second, int64(tpm/60), int64(tpm/60))\n\t}\n}\n\nfunc InitEmbeddingLimiters(rpm, tpm int) {\n\tif rpm > 0 {\n\t\tEmbeddingBackoff = time.Minute / time.Duration(rpm)\n\t\tEmbeddingRequestsLimiter = ratelimit.NewBucketWithQuantum(time.Second, int64(rpm/60), int64(rpm/60))\n\t}\n\tif tpm > 0 {\n\t\tEmbeddingTokensLimiter = ratelimit.NewBucketWithQuantum(time.Second, int64(tpm/60), int64(tpm/60))\n\t}\n}\n\ntype RateLimiter interface {\n\tTake(count int64) time.Duration\n}\n\ntype Unlimited struct{}\n\nfunc (n *Unlimited) Take(count int64) time.Duration {\n\treturn 0\n}\n"
  },
  {
    "path": "common/parallel/ratelimit_test.go",
    "content": "package parallel\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestUnlimited(t *testing.T) {\n\trateLimiter := &Unlimited{}\n\tassert.Zero(t, rateLimiter.Take(1))\n}\n\nfunc TestInitEmbeddingLimiters(t *testing.T) {\n\tInitEmbeddingLimiters(120, 180)\n\tassert.Equal(t, time.Duration(0), EmbeddingRequestsLimiter.Take(1))\n\tassert.InDelta(t, time.Second, EmbeddingRequestsLimiter.Take(2), float64(time.Millisecond))\n\tassert.Equal(t, time.Duration(0), EmbeddingTokensLimiter.Take(2))\n\tassert.InDelta(t, 2*time.Second, EmbeddingTokensLimiter.Take(5), float64(time.Millisecond))\n}\n\nfunc TestInitChatCompletionLimiters(t *testing.T) {\n\tInitChatCompletionLimiters(120, 180)\n\tassert.Equal(t, time.Duration(0), ChatCompletionRequestsLimiter.Take(1))\n\tassert.InDelta(t, time.Second, ChatCompletionRequestsLimiter.Take(2), float64(time.Millisecond))\n\tassert.Equal(t, time.Duration(0), ChatCompletionTokensLimiter.Take(2))\n\tassert.InDelta(t, 2*time.Second, ChatCompletionTokensLimiter.Take(5), float64(time.Millisecond))\n}\n"
  },
  {
    "path": "common/rc/rc.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage rc\n\nimport (\n\t\"io\"\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\"github.com/modern-go/reflect2\"\n)\n\ntype DropFunc func()\n\ntype rcPointer[T io.Closer] struct {\n\tpointer   T\n\treference atomic.Int32\n}\n\ntype Rc[T io.Closer] struct {\n\tpointer *rcPointer[T]\n\tdrop    DropFunc\n\tmu      sync.RWMutex\n}\n\nfunc (r *Rc[T]) Reset(pointer T) {\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\tif r.pointer != nil {\n\t\tr.drop()\n\t}\n\tif reflect2.IsNil(pointer) {\n\t\tr.pointer = nil\n\t} else {\n\t\tr.pointer = &rcPointer[T]{pointer: pointer}\n\t\t_, r.drop = r.get()\n\t}\n}\n\nfunc (r *Rc[T]) Get() (T, DropFunc) {\n\tr.mu.RLock()\n\tdefer r.mu.RUnlock()\n\treturn r.get()\n}\n\nfunc (r *Rc[T]) get() (T, DropFunc) {\n\tif r.pointer == nil {\n\t\tvar niL T\n\t\treturn niL, func() {}\n\t}\n\tp := r.pointer\n\tp.reference.Add(1)\n\treturn p.pointer, func() {\n\t\t// Atomically decrement and check if we are the last to drop\n\t\tif p.reference.Add(-1) == 0 {\n\t\t\t// Use CAS to ensure only one goroutine calls Close()\n\t\t\tif p.reference.CompareAndSwap(0, -1) {\n\t\t\t\t_ = p.pointer.Close()\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (r *Rc[T]) Apply(f func(T)) {\n\tp, drop := r.Get()\n\tdefer drop()\n\tf(p)\n}\n"
  },
  {
    "path": "common/rc/rc_test.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage rc\n\nimport (\n\t\"math/rand/v2\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n)\n\ntype Foo struct {\n\tmock.Mock\n}\n\nfunc NewFoo() *Foo {\n\tf := &Foo{}\n\tf.On(\"Close\").Return(nil)\n\treturn f\n}\n\nfunc (f *Foo) Close() error {\n\tf.Called()\n\treturn nil\n}\n\nfunc TestRc_Get(t *testing.T) {\n\tf := NewFoo()\n\tvar rc Rc[*Foo]\n\trc.Reset(f)\n\t_, drop := rc.Get()\n\tdrop()\n\trc.Reset(nil)\n\tf.AssertNumberOfCalls(t, \"Close\", 1)\n}\n\nfunc TestRc_Apply(t *testing.T) {\n\tf := NewFoo()\n\tvar rc Rc[*Foo]\n\trc.Reset(f)\n\trc.Apply(func(f *Foo) {\n\t\tassert.NotNil(t, f)\n\t})\n\trc.Reset(nil)\n\tf.AssertNumberOfCalls(t, \"Close\", 1)\n}\n\nfunc TestRc_Concurrent(t *testing.T) {\n\tf := make([]*Foo, 1000)\n\tfor i := range f {\n\t\tf[i] = NewFoo()\n\t}\n\n\tvar rc Rc[*Foo]\n\tdrops := make([]DropFunc, 0)\n\tfor i := range f {\n\t\trc.Reset(f[i])\n\t\tcnt := rand.IntN(10)\n\t\tfor j := 0; j < cnt; j++ {\n\t\t\t_, drop := rc.Get()\n\t\t\tdrops = append(drops, drop)\n\t\t}\n\t}\n\n\trc.Reset(nil)\n\tvar wg sync.WaitGroup\n\tfor _, drop := range drops {\n\t\twg.Go(func() {\n\t\t\tdrop()\n\t\t})\n\t}\n\twg.Wait()\n\tfor i := range f {\n\t\tf[i].AssertNumberOfCalls(t, \"Close\", 1)\n\t}\n}\n"
  },
  {
    "path": "common/reranker/client.go",
    "content": "// Copyright 2026 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage reranker\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\n\t\"github.com/emicklei/go-restful/v3\"\n)\n\ntype Client struct {\n\tauthToken  string\n\turl        string\n\thttpClient *http.Client\n}\n\nfunc NewClient(apiKey, endpoint string) *Client {\n\treturn &Client{\n\t\tauthToken:  apiKey,\n\t\turl:        endpoint,\n\t\thttpClient: &http.Client{},\n\t}\n}\n\ntype RerankRequest struct {\n\tModel     string   `json:\"model\"`\n\tQuery     string   `json:\"query\"`\n\tTopN      int      `json:\"top_n,omitempty\"`\n\tDocuments []string `json:\"documents\"`\n}\n\ntype RerankResponse struct {\n\tModel   string         `json:\"model\"`\n\tUsage   Usage          `json:\"usage\"`\n\tResults []RerankResult `json:\"results\"`\n}\n\ntype RerankResult struct {\n\tIndex          int     `json:\"index\"`\n\tRelevanceScore float64 `json:\"relevance_score\"`\n}\n\ntype Usage struct {\n\tTotalTokens int `json:\"total_tokens\"`\n}\n\nfunc (c *Client) Rerank(ctx context.Context, req RerankRequest) (*RerankResponse, error) {\n\tvar body []byte\n\tvar err error\n\n\tbody, err = json.Marshal(req)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpReq, err := http.NewRequestWithContext(ctx, \"POST\", c.url, bytes.NewBuffer(body))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+c.authToken)\n\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := c.httpClient.Do(httpReq)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"rerank request failed with status: %d, body: %s\", resp.StatusCode, string(respBody))\n\t}\n\n\tvar rerankResp RerankResponse\n\tif err := json.Unmarshal(respBody, &rerankResp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &rerankResp, nil\n}\n\ntype MockServer struct {\n\tlistener   net.Listener\n\thttpServer *http.Server\n\tapiKey     string\n\tready      chan struct{}\n}\n\nfunc NewMockServer() *MockServer {\n\ts := &MockServer{}\n\tws := new(restful.WebService)\n\tws.Path(\"/\").\n\t\tConsumes(restful.MIME_JSON).\n\t\tProduces(restful.MIME_JSON)\n\tws.Route(ws.POST(\"/v1/rerank\").\n\t\tReads(RerankRequest{}).\n\t\tWrites(RerankResponse{}).\n\t\tTo(s.rerank))\n\tcontainer := restful.NewContainer()\n\tcontainer.Add(ws)\n\ts.httpServer = &http.Server{Handler: container}\n\ts.apiKey = \"dashscope\"\n\ts.ready = make(chan struct{})\n\treturn s\n}\n\nfunc (s *MockServer) Start() error {\n\tvar err error\n\ts.listener, err = net.Listen(\"tcp\", \"127.0.0.1:0\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tclose(s.ready)\n\treturn s.httpServer.Serve(s.listener)\n}\n\nfunc (s *MockServer) URL() string {\n\treturn fmt.Sprintf(\"http://%s/v1/rerank\", s.listener.Addr().String())\n}\n\nfunc (s *MockServer) AuthToken() string {\n\treturn s.apiKey\n}\n\nfunc (s *MockServer) Ready() {\n\t<-s.ready\n}\n\nfunc (s *MockServer) Close() error {\n\treturn s.httpServer.Close()\n}\n\nfunc (s *MockServer) rerank(req *restful.Request, resp *restful.Response) {\n\tvar r RerankRequest\n\terr := req.ReadEntity(&r)\n\tif err != nil {\n\t\t_ = resp.WriteError(http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\tresults := make([]RerankResult, len(r.Documents))\n\tfor i := range r.Documents {\n\t\tresults[i] = RerankResult{\n\t\t\tIndex:          i,\n\t\t\tRelevanceScore: 1.0 / float64(i+1),\n\t\t}\n\t}\n\n\t_ = resp.WriteEntity(RerankResponse{\n\t\tModel: r.Model,\n\t\tUsage: Usage{\n\t\t\tTotalTokens: 100,\n\t\t},\n\t\tResults: results,\n\t})\n}\n"
  },
  {
    "path": "common/reranker/client_test.go",
    "content": "// Copyright 2026 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage reranker\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/suite\"\n)\n\ntype ClientTestSuite struct {\n\tsuite.Suite\n\ts *MockServer\n}\n\nfunc (suite *ClientTestSuite) SetupSuite() {\n\tsuite.s = NewMockServer()\n\tgo func() {\n\t\terr := suite.s.Start()\n\t\tsuite.ErrorIs(err, http.ErrServerClosed)\n\t}()\n\tsuite.s.Ready()\n}\n\nfunc (suite *ClientTestSuite) TearDownSuite() {\n\tsuite.s.Close()\n}\n\nfunc (suite *ClientTestSuite) TestRerank() {\n\tclient := NewClient(suite.s.AuthToken(), suite.s.URL())\n\n\treq := RerankRequest{\n\t\tModel: \"jina-reranker-v2-base-multilingual\",\n\t\tQuery: \"What is the capital of France?\",\n\t\tDocuments: []string{\n\t\t\t\"Paris is the capital of France.\",\n\t\t\t\"Lyon is a city in France.\",\n\t\t},\n\t}\n\n\tresp, err := client.Rerank(suite.T().Context(), req)\n\tsuite.NoError(err)\n\tsuite.Equal(2, len(resp.Results))\n\tsuite.Equal(0, resp.Results[0].Index)\n\tsuite.Equal(1.0, resp.Results[0].RelevanceScore)\n\tsuite.Equal(1, resp.Results[1].Index)\n\tsuite.Equal(0.5, resp.Results[1].RelevanceScore)\n}\n\nfunc TestClient(t *testing.T) {\n\tsuite.Run(t, new(ClientTestSuite))\n}\n"
  },
  {
    "path": "common/sizeof/size.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage sizeof\n\nimport (\n\t\"math\"\n\t\"reflect\"\n)\n\n// DeepSize reports the size of v in bytes, as reflect.Size, but also including\n// all recursive substructures of v via maps, pointers, and slices. If v\n// contains any cycles, the size of each pointer (re)introducing the cycle is\n// included but the acyclic substructure is counted only once.\n//\n// Only values whose size and structure can be obtained by the reflect package\n// are counted.  Some values have components that are not visible by\n// reflection, and so are not counted or may be undercounted. In particular:\n//\n// The space occupied by code and data, reachable through variables captured in\n// the closure of a function pointer, are not counted. A value of function type\n// is counted only as a pointer.\n//\n// The unused buckets of a map cannot be inspected by the reflect package.\n// Their size is estimated by assuming unfilled slots contain zeroes of their\n// type.\n//\n// The unused capacity of the array under a slice is estimated by assuming the\n// unused slots contain zeroes of their type. It is possible they contain non\n// zero values from sharing or reslicing, but without explicitly reslicing the\n// reflect package cannot touch them.\nfunc DeepSize(v any) int {\n\treturn int(valueSize(reflect.ValueOf(v), make(map[uintptr]bool)))\n}\n\nfunc valueSize(v reflect.Value, seen map[uintptr]bool) uintptr {\n\tbase := v.Type().Size()\n\tswitch v.Kind() {\n\tcase reflect.Ptr:\n\t\tp := v.Pointer()\n\t\tif !seen[p] && !v.IsNil() {\n\t\t\tseen[p] = true\n\t\t\treturn base + valueSize(v.Elem(), seen)\n\t\t}\n\n\tcase reflect.Slice:\n\t\tn := v.Len()\n\t\tfor i := 0; i < n; i++ {\n\t\t\tbase += valueSize(v.Index(i), seen)\n\t\t}\n\n\t\t// Account for the parts of the array not covered by this slice.  Since\n\t\t// we can't get the values directly, assume they're zeroes. That may be\n\t\t// incorrect, in which case we may underestimate.\n\t\tif cap := v.Cap(); cap > n {\n\t\t\tbase += v.Type().Size() * uintptr(cap-n)\n\t\t}\n\n\tcase reflect.Map:\n\t\t// A map m has len(m) / 6.5 buckets, rounded up to a power of two, and\n\t\t// a minimum of one bucket. Each bucket is 16 bytes + 8*(keysize + valsize).\n\t\t//\n\t\t// We can't tell which keys are in which bucket by reflection, however,\n\t\t// so here we count the 16-byte header for each bucket, and then just add\n\t\t// in the computed key and value sizes.\n\t\tnb := uintptr(math.Pow(2, math.Ceil(math.Log(float64(v.Len())/6.5)/math.Log(2))))\n\t\tif nb == 0 {\n\t\t\tnb = 1\n\t\t}\n\t\tbase = 16 * nb\n\t\tfor _, key := range v.MapKeys() {\n\t\t\tbase += valueSize(key, seen)\n\t\t\tbase += valueSize(v.MapIndex(key), seen)\n\t\t}\n\n\t\t// We have nb buckets of 8 slots each, and v.Len() slots are filled.\n\t\t// The remaining slots we will assume contain zero key/value pairs.\n\t\tzk := v.Type().Key().Size()  // a zero key\n\t\tzv := v.Type().Elem().Size() // a zero value\n\t\tbase += (8*nb - uintptr(v.Len())) * (zk + zv)\n\n\tcase reflect.Struct:\n\t\t// Chase pointer and slice fields and add the size of their members.\n\t\tfor i := 0; i < v.NumField(); i++ {\n\t\t\tf := v.Field(i)\n\t\t\tswitch f.Kind() {\n\t\t\tcase reflect.Ptr:\n\t\t\t\tp := f.Pointer()\n\t\t\t\tif !seen[p] && !f.IsNil() {\n\t\t\t\t\tseen[p] = true\n\t\t\t\t\tbase += valueSize(f.Elem(), seen)\n\t\t\t\t}\n\t\t\tcase reflect.Slice:\n\t\t\t\tbase += valueSize(f, seen)\n\t\t\t}\n\t\t}\n\n\tcase reflect.String:\n\t\treturn base + uintptr(v.Len())\n\n\t}\n\treturn base\n}\n"
  },
  {
    "path": "common/sizeof/size_test.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage sizeof\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCyclic(t *testing.T) {\n\ttype V struct {\n\t\tZ int\n\t\tE *V\n\t}\n\n\tv := &V{Z: 25}\n\twant := DeepSize(v)\n\tv.E = v // induce a cycle\n\tgot := DeepSize(v)\n\tif got != want {\n\t\tt.Errorf(\"Cyclic size: got %d, want %d\", got, want)\n\t}\n}\n\nfunc TestDeepSize(t *testing.T) {\n\t// matrix\n\ta := [][]int64{{1}, {2}, {3}, {4}}\n\tassert.Equal(t, 5*24+4*8, DeepSize(a))\n\tb := [][]int32{{1}, {2}, {3}, {4}}\n\tassert.Equal(t, 5*24+4*4, DeepSize(b))\n\tc := [][]int16{{1}, {2}, {3}, {4}}\n\tassert.Equal(t, 5*24+4*2, DeepSize(c))\n\td := [][]int8{{1}, {2}, {3}, {4}}\n\tassert.Equal(t, 5*24+4, DeepSize(d))\n\n\t// strings\n\te := []string{\"abc\", \"de\", \"f\"}\n\tassert.Equal(t, 24+16*3+6, DeepSize(e))\n\tf := []string{\"♥♥♥\", \"♥♥\", \"♥\"}\n\tassert.Equal(t, 24+16*3+18, DeepSize(f))\n\n\t// slice\n\tg := []int64{1, 2, 3, 4}\n\tassert.Equal(t, 7*8, DeepSize(g))\n\th := []int32{1, 2, 3, 4}\n\tassert.Equal(t, 3*8+4*4, DeepSize(h))\n\ti := []int16{1, 2, 3, 4}\n\tassert.Equal(t, 3*8+2*4, DeepSize(i))\n\tj := []int8{1, 2, 3, 4}\n\tassert.Equal(t, 3*8+4, DeepSize(j))\n}\n"
  },
  {
    "path": "common/util/random.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage util\n\nimport (\n\t\"math/rand\"\n\t\"sync\"\n\n\tmapset \"github.com/deckarep/golang-set/v2\"\n)\n\n// RandomGenerator is the random generator for gorse.\ntype RandomGenerator struct {\n\t*rand.Rand\n}\n\n// NewRandomGenerator creates a RandomGenerator.\nfunc NewRandomGenerator(seed int64) RandomGenerator {\n\treturn RandomGenerator{rand.New(rand.NewSource(int64(seed)))}\n}\n\n// UniformVector makes a vec filled with uniform random floats,\nfunc (rng RandomGenerator) UniformVector(size int, low, high float32) []float32 {\n\tret := make([]float32, size)\n\tscale := high - low\n\tfor i := 0; i < len(ret); i++ {\n\t\tret[i] = rng.Float32()*scale + low\n\t}\n\treturn ret\n}\n\n// NewNormalVector makes a vec filled with normal random floats.\nfunc (rng RandomGenerator) NewNormalVector(size int, mean, stdDev float32) []float32 {\n\tret := make([]float32, size)\n\tfor i := 0; i < len(ret); i++ {\n\t\tret[i] = float32(rng.NormFloat64())*stdDev + mean\n\t}\n\treturn ret\n}\n\n// NormalMatrix makes a matrix filled with normal random floats.\nfunc (rng RandomGenerator) NormalMatrix(row, col int, mean, stdDev float32) [][]float32 {\n\tret := make([][]float32, row)\n\tfor i := range ret {\n\t\tret[i] = rng.NewNormalVector(col, mean, stdDev)\n\t}\n\treturn ret\n}\n\nfunc (rng RandomGenerator) NormalVector(size int, mean, stdDev float32) []float32 {\n\tret := make([]float32, size)\n\tfor i := 0; i < len(ret); i++ {\n\t\tret[i] = float32(rng.NormFloat64())*stdDev + mean\n\t}\n\treturn ret\n}\n\n// UniformMatrix makes a matrix filled with uniform random floats.\nfunc (rng RandomGenerator) UniformMatrix(row, col int, low, high float32) [][]float32 {\n\tret := make([][]float32, row)\n\tfor i := range ret {\n\t\tret[i] = rng.UniformVector(col, low, high)\n\t}\n\treturn ret\n}\n\n// NormalVector64 makes a vec filled with normal random floats.\nfunc (rng RandomGenerator) NormalVector64(size int, mean, stdDev float64) []float64 {\n\tret := make([]float64, size)\n\tfor i := 0; i < len(ret); i++ {\n\t\tret[i] = rng.NormFloat64()*stdDev + mean\n\t}\n\treturn ret\n}\n\n// Sample n values between low and high, but not in exclude.\nfunc (rng RandomGenerator) Sample(low, high, n int, exclude ...mapset.Set[int]) []int {\n\tintervalLength := high - low\n\texcludeSet := mapset.NewSet[int]()\n\tfor _, set := range exclude {\n\t\texcludeSet = excludeSet.Union(set)\n\t}\n\tsampled := make([]int, 0, n)\n\tif n >= intervalLength-excludeSet.Cardinality() {\n\t\tfor i := low; i < high; i++ {\n\t\t\tif !excludeSet.Contains(i) {\n\t\t\t\tsampled = append(sampled, i)\n\t\t\t\texcludeSet.Add(i)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor len(sampled) < n {\n\t\t\tv := rng.Intn(intervalLength) + low\n\t\t\tif !excludeSet.Contains(v) {\n\t\t\t\tsampled = append(sampled, v)\n\t\t\t\texcludeSet.Add(v)\n\t\t\t}\n\t\t}\n\t}\n\treturn sampled\n}\n\n// SampleInt32 n 32bit values between low and high, but not in exclude.\nfunc (rng RandomGenerator) SampleInt32(low, high int32, n int, exclude ...mapset.Set[int32]) []int32 {\n\tintervalLength := high - low\n\texcludeSet := mapset.NewSet[int32]()\n\tfor _, set := range exclude {\n\t\texcludeSet = excludeSet.Union(set)\n\t}\n\tsampled := make([]int32, 0, n)\n\tif n >= int(intervalLength)-excludeSet.Cardinality() {\n\t\tfor i := low; i < high; i++ {\n\t\t\tif !excludeSet.Contains(i) {\n\t\t\t\tsampled = append(sampled, i)\n\t\t\t\texcludeSet.Add(i)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor len(sampled) < n {\n\t\t\tv := rng.Int31n(intervalLength) + low\n\t\t\tif !excludeSet.Contains(v) {\n\t\t\t\tsampled = append(sampled, v)\n\t\t\t\texcludeSet.Add(v)\n\t\t\t}\n\t\t}\n\t}\n\treturn sampled\n}\n\n// lockedSource allows a random number generator to be used by multiple goroutines concurrently.\n// The code is very similar to math/rand.lockedSource, which is unfortunately not exposed.\ntype lockedSource struct {\n\tmut sync.Mutex\n\tsrc rand.Source\n}\n\n// NewRand returns a rand.Rand that is threadsafe.\nfunc NewRand(seed int64) *rand.Rand {\n\treturn rand.New(&lockedSource{src: rand.NewSource(seed)})\n}\n\nfunc (r *lockedSource) Int63() (n int64) {\n\tr.mut.Lock()\n\tn = r.src.Int63()\n\tr.mut.Unlock()\n\treturn\n}\n\nfunc (r *lockedSource) Seed(seed int64) {\n\tr.mut.Lock()\n\tr.src.Seed(seed)\n\tr.mut.Unlock()\n}\n"
  },
  {
    "path": "common/util/random_test.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage util\n\nimport (\n\t\"testing\"\n\n\t\"github.com/chewxy/math32\"\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/samber/lo\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nconst randomEpsilon = 0.1\n\nfunc TestRandomGenerator_MakeNormalMatrix(t *testing.T) {\n\trng := NewRandomGenerator(0)\n\tvec := rng.NormalMatrix(1, 1000, 1, 2)[0]\n\tassert.False(t, math32.Abs(mean(vec)-1) > randomEpsilon)\n\tassert.False(t, math32.Abs(stdDev(vec)-2) > randomEpsilon)\n}\n\nfunc TestRandomGenerator_MakeUniformMatrix(t *testing.T) {\n\trng := NewRandomGenerator(0)\n\tvec := rng.UniformMatrix(1, 1000, 1, 2)[0]\n\tassert.False(t, lo.Min(vec) < 1)\n\tassert.False(t, lo.Max(vec) > 2)\n}\n\nfunc TestRandomGenerator_Sample(t *testing.T) {\n\texcludeSet := mapset.NewSet(0, 1, 2, 3, 4)\n\trng := NewRandomGenerator(0)\n\tfor i := 1; i <= 10; i++ {\n\t\tsampled := rng.Sample(0, 10, i, excludeSet)\n\t\tfor j := range sampled {\n\t\t\tassert.False(t, excludeSet.Contains(sampled[j]))\n\t\t}\n\t}\n}\n\nfunc TestRandomGenerator_SampleInt32(t *testing.T) {\n\texcludeSet := mapset.NewSet[int32](0, 1, 2, 3, 4)\n\trng := NewRandomGenerator(0)\n\tfor i := 1; i <= 10; i++ {\n\t\tsampled := rng.SampleInt32(0, 10, i, excludeSet)\n\t\tfor j := range sampled {\n\t\t\tassert.False(t, excludeSet.Contains(sampled[j]))\n\t\t}\n\t}\n}\n\n// mean of a slice of 32-bit floats.\nfunc mean(x []float32) float32 {\n\treturn lo.Sum(x) / float32(len(x))\n}\n\n// stdDev returns the sample standard deviation.\nfunc stdDev(x []float32) float32 {\n\t_, variance := meanVariance(x)\n\treturn math32.Sqrt(variance)\n}\n\n// meanVariance computes the sample mean and unbiased variance, where the mean and variance are\n//\n//\t\\sum_i w_i * x_i / (sum_i w_i)\n//\t\\sum_i w_i (x_i - mean)^2 / (sum_i w_i - 1)\n//\n// respectively.\n// If weights is nil then all of the weights are 1. If weights is not nil, then\n// len(x) must equal len(weights).\n// When weights sum to 1 or less, a biased variance estimator should be used.\nfunc meanVariance(x []float32) (m, variance float32) {\n\t// This uses the corrected two-pass algorithm (1.7), from \"Algorithms for computing\n\t// the sample variance: Analysis and recommendations\" by Chan, Tony F., Gene H. Golub,\n\t// and Randall J. LeVeque.\n\n\t// note that this will panic if the slice lengths do not match\n\tm = mean(x)\n\tvar (\n\t\tss           float32\n\t\tcompensation float32\n\t)\n\tfor _, v := range x {\n\t\td := v - m\n\t\tss += d * d\n\t\tcompensation += d\n\t}\n\tvariance = (ss - compensation*compensation/float32(len(x))) / float32(len(x)-1)\n\treturn\n}\n"
  },
  {
    "path": "common/util/strconv.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage util\n\nimport (\n\t\"reflect\"\n\t\"strconv\"\n\n\t\"golang.org/x/exp/constraints\"\n)\n\nfunc ParseFloat[T constraints.Float](s string) (T, error) {\n\tv, err := strconv.ParseFloat(s, reflect.TypeOf(T(0)).Bits())\n\treturn T(v), err\n}\n\nfunc ParseUInt[T constraints.Unsigned](s string) (T, error) {\n\tv, err := strconv.ParseUint(s, 10, reflect.TypeOf(T(0)).Bits())\n\treturn T(v), err\n}\n\nfunc ParseInt[T constraints.Signed](s string) (T, error) {\n\tv, err := strconv.ParseInt(s, 10, reflect.TypeOf(T(0)).Bits())\n\treturn T(v), err\n}\n\nfunc FormatInt[T constraints.Signed](i T) string {\n\treturn strconv.FormatInt(int64(i), 10)\n}\n"
  },
  {
    "path": "common/util/tls.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage util\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"github.com/juju/errors\"\n\t\"google.golang.org/grpc/credentials\"\n\t\"google.golang.org/grpc/security/advancedtls\"\n\t\"os\"\n)\n\ntype TLSConfig struct {\n\tSSLCA   string\n\tSSLCert string\n\tSSLKey  string\n}\n\nfunc NewServerCreds(o *TLSConfig) (credentials.TransportCredentials, error) {\n\t// Load certification authority\n\tca := x509.NewCertPool()\n\tpem, err := os.ReadFile(o.SSLCA)\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\tif !ca.AppendCertsFromPEM(pem) {\n\t\treturn nil, errors.New(\"failed to append certificate\")\n\t}\n\t// Load certification\n\tcertificate, err := tls.LoadX509KeyPair(o.SSLCert, o.SSLKey)\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\t// Create server credentials\n\treturn advancedtls.NewServerCreds(&advancedtls.Options{\n\t\tIdentityOptions: advancedtls.IdentityCertificateOptions{\n\t\t\tCertificates: []tls.Certificate{certificate},\n\t\t},\n\t\tRootOptions: advancedtls.RootCertificateOptions{\n\t\t\tRootCertificates: ca,\n\t\t},\n\t\tRequireClientCert: true,\n\t\tVerificationType:  advancedtls.CertVerification,\n\t})\n}\n\nfunc NewClientCreds(o *TLSConfig) (credentials.TransportCredentials, error) {\n\t// Load certification authority\n\tca := x509.NewCertPool()\n\tpem, err := os.ReadFile(o.SSLCA)\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\tif !ca.AppendCertsFromPEM(pem) {\n\t\treturn nil, errors.New(\"failed to append certificate\")\n\t}\n\t// Load certification\n\tcertificate, err := tls.LoadX509KeyPair(o.SSLCert, o.SSLKey)\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\t// Create client credentials\n\treturn advancedtls.NewClientCreds(&advancedtls.Options{\n\t\tIdentityOptions: advancedtls.IdentityCertificateOptions{\n\t\t\tCertificates: []tls.Certificate{certificate},\n\t\t},\n\t\tRootOptions: advancedtls.RootCertificateOptions{\n\t\t\tRootCertificates: ca,\n\t\t},\n\t\tVerificationType: advancedtls.CertVerification,\n\t})\n}\n"
  },
  {
    "path": "common/util/util.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage util\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"go.uber.org/zap\"\n)\n\n// RangeInt generate a slice [0, ..., n-1].\nfunc RangeInt(n int) []int {\n\ta := make([]int, n)\n\tfor i := range a {\n\t\ta[i] = i\n\t}\n\treturn a\n}\n\n// RepeatFloat32s repeats value n times.\nfunc RepeatFloat32s(n int, value float32) []float32 {\n\ta := make([]float32, n)\n\tfor i := range a {\n\t\ta[i] = value\n\t}\n\treturn a\n}\n\n// NewMatrix32 creates a 2D matrix of 32-bit floats.\nfunc NewMatrix32(row, col int) [][]float32 {\n\tret := make([][]float32, row)\n\tfor i := range ret {\n\t\tret[i] = make([]float32, col)\n\t}\n\treturn ret\n}\n\n// NewTensor32 creates a 3D tensor of 32-bit floats.\nfunc NewTensor32(a, b, c int) [][][]float32 {\n\tret := make([][][]float32, a)\n\tfor i := range ret {\n\t\tret[i] = NewMatrix32(b, c)\n\t}\n\treturn ret\n}\n\n// NewMatrixInt creates a 2D matrix of integers.\nfunc NewMatrixInt(row, col int) [][]int {\n\tret := make([][]int, row)\n\tfor i := range ret {\n\t\tret[i] = make([]int, col)\n\t}\n\treturn ret\n}\n\n// CheckPanic catches panic.\nfunc CheckPanic() {\n\tif r := recover(); r != nil {\n\t\tlog.Logger().Error(\"panic recovered\", zap.Any(\"panic\", r))\n\t}\n}\n\n// ValidateId validates user/item id. Id cannot be empty and contain [/,].\nfunc ValidateId(text string) error {\n\ttext = strings.TrimSpace(text)\n\tif text == \"\" {\n\t\treturn fmt.Errorf(\"id cannot be empty\")\n\t} else if strings.Contains(text, \"/\") {\n\t\treturn fmt.Errorf(\"id cannot contain `/`\")\n\t}\n\treturn nil\n}\n\n// MD5 computes the MD5 hash of unordered strings.\nfunc MD5(s ...string) string {\n\thash := md5.New()\n\tsort.Strings(s)\n\tfor _, str := range s {\n\t\thash.Write([]byte(str))\n\t}\n\treturn hex.EncodeToString(hash.Sum(nil)[:])\n}\n"
  },
  {
    "path": "common/util/util_test.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage util\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNewMatrix32(t *testing.T) {\n\ta := NewMatrix32(3, 4)\n\tassert.Equal(t, 3, len(a))\n\tassert.Equal(t, 4, len(a[0]))\n\tassert.Equal(t, 4, len(a[0]))\n\tassert.Equal(t, 4, len(a[0]))\n}\n\nfunc TestRangeInt(t *testing.T) {\n\ta := RangeInt(7)\n\tassert.Equal(t, 7, len(a))\n\tfor i := range a {\n\t\tassert.Equal(t, i, a[i])\n\t}\n}\n\nfunc TestRepeatFloat32s(t *testing.T) {\n\ta := RepeatFloat32s(3, 0.1)\n\tassert.Equal(t, []float32{0.1, 0.1, 0.1}, a)\n}\n\nfunc TestNewMatrixInt(t *testing.T) {\n\tm := NewMatrixInt(4, 3)\n\tassert.Equal(t, 4, len(m))\n\tfor _, v := range m {\n\t\tassert.Equal(t, 3, len(v))\n\t}\n}\n\nfunc TestNewTensor32(t *testing.T) {\n\ta := NewTensor32(3, 4, 5)\n\tassert.Equal(t, 3, len(a))\n\tassert.Equal(t, 4, len(a[0]))\n\tassert.Equal(t, 5, len(a[0][0]))\n}\n\nfunc TestValidateId(t *testing.T) {\n\tassert.NotNil(t, ValidateId(\"\"))\n\tassert.NotNil(t, ValidateId(\"/\"))\n\tassert.Nil(t, ValidateId(\"abc\"))\n}\n\nfunc TestMD5(t *testing.T) {\n\thash1 := MD5(\"b\", \"a\", \"c\")\n\thash2 := MD5(\"c\", \"b\", \"a\")\n\tassert.Equal(t, hash1, hash2)\n}\n"
  },
  {
    "path": "config/config.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage config\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t_ \"embed\"\n\t\"encoding/hex\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strings\"\n\t\"time\"\n\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/expr-lang/expr/parser\"\n\t\"github.com/go-playground/locales/en\"\n\tut \"github.com/go-playground/universal-translator\"\n\t\"github.com/go-playground/validator/v10\"\n\ten_translations \"github.com/go-playground/validator/v10/translations/en\"\n\t\"github.com/go-viper/mapstructure/v2\"\n\t\"github.com/gorse-io/gorse/common/expression\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/common/util\"\n\t\"github.com/gorse-io/gorse/storage\"\n\t\"github.com/juju/errors\"\n\t\"github.com/samber/lo\"\n\t\"github.com/spf13/viper\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlptrace\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp\"\n\t\"go.opentelemetry.io/otel/exporters/zipkin\"\n\t\"go.opentelemetry.io/otel/sdk/resource\"\n\ttracesdk \"go.opentelemetry.io/otel/sdk/trace\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.8.0\"\n\t\"go.opentelemetry.io/otel/trace\"\n\t\"go.uber.org/zap\"\n)\n\nfunc init() {\n\tviper.SetOptions(viper.WithDecodeHook(mapstructure.ComposeDecodeHookFunc(\n\t\tmapstructure.StringToTimeDurationHookFunc(),\n\t\tStringToFeedbackTypeHookFunc(),\n\t)))\n}\n\n// Config is the configuration for the engine.\ntype Config struct {\n\tDatabase  DatabaseConfig  `mapstructure:\"database\"`\n\tMaster    MasterConfig    `mapstructure:\"master\"`\n\tServer    ServerConfig    `mapstructure:\"server\"`\n\tRecommend RecommendConfig `mapstructure:\"recommend\"`\n\tTracing   TracingConfig   `mapstructure:\"tracing\"`\n\tOIDC      OIDCConfig      `mapstructure:\"oidc\"`\n\tOpenAI    OpenAIConfig    `mapstructure:\"openai\"`\n\tBlob      BlobConfig      `mapstructure:\"blob\"`\n}\n\n// DatabaseConfig is the configuration for the database.\ntype DatabaseConfig struct {\n\tDataStore        string      `mapstructure:\"data_store\" validate:\"required,data_store\"`   // database for data store\n\tCacheStore       string      `mapstructure:\"cache_store\" validate:\"required,cache_store\"` // database for cache store\n\tTablePrefix      string      `mapstructure:\"table_prefix\"`\n\tDataTablePrefix  string      `mapstructure:\"data_table_prefix\"`\n\tCacheTablePrefix string      `mapstructure:\"cache_table_prefix\"`\n\tMySQL            MySQLConfig `mapstructure:\"mysql\"`\n\tPostgres         SQLConfig   `mapstructure:\"postgres\"`\n\tRedis            RedisConfig `mapstructure:\"redis\"`\n}\n\ntype MySQLConfig struct {\n\tIsolationLevel  string        `mapstructure:\"isolation_level\" validate:\"oneof=READ-UNCOMMITTED READ-COMMITTED REPEATABLE-READ SERIALIZABLE\"`\n\tMaxOpenConns    int           `mapstructure:\"max_open_conns\" validate:\"gte=0\"`\n\tMaxIdleConns    int           `mapstructure:\"max_idle_conns\" validate:\"gte=0\"`\n\tConnMaxLifetime time.Duration `mapstructure:\"conn_max_lifetime\" validate:\"gte=0\"`\n}\n\ntype SQLConfig struct {\n\tMaxOpenConns    int           `mapstructure:\"max_open_conns\" validate:\"gte=0\"`\n\tMaxIdleConns    int           `mapstructure:\"max_idle_conns\" validate:\"gte=0\"`\n\tConnMaxLifetime time.Duration `mapstructure:\"conn_max_lifetime\" validate:\"gte=0\"`\n}\n\ntype RedisConfig struct {\n\tMaxSearchResults int `mapstructure:\"max_search_results\" validate:\"gt=0\"`\n}\n\nfunc (db *DatabaseConfig) StorageOptions(path string) []storage.Option {\n\tif strings.HasPrefix(path, storage.MySQLPrefix) {\n\t\treturn []storage.Option{\n\t\t\tstorage.WithIsolationLevel(db.MySQL.IsolationLevel),\n\t\t\tstorage.WithMaxOpenConns(db.MySQL.MaxOpenConns),\n\t\t\tstorage.WithMaxIdleConns(db.MySQL.MaxIdleConns),\n\t\t\tstorage.WithConnMaxLifetime(db.MySQL.ConnMaxLifetime),\n\t\t}\n\t}\n\tif strings.HasPrefix(path, storage.PostgresPrefix) || strings.HasPrefix(path, storage.PostgreSQLPrefix) {\n\t\treturn []storage.Option{\n\t\t\tstorage.WithMaxOpenConns(db.Postgres.MaxOpenConns),\n\t\t\tstorage.WithMaxIdleConns(db.Postgres.MaxIdleConns),\n\t\t\tstorage.WithConnMaxLifetime(db.Postgres.ConnMaxLifetime),\n\t\t}\n\t}\n\tif strings.HasPrefix(path, storage.RedisPrefix) || strings.HasPrefix(path, storage.RedissPrefix) ||\n\t\tstrings.HasPrefix(path, storage.RedisClusterPrefix) || strings.HasPrefix(path, storage.RedissClusterPrefix) {\n\t\treturn []storage.Option{\n\t\t\tstorage.WithMaxSearchResults(db.Redis.MaxSearchResults),\n\t\t}\n\t}\n\treturn nil\n}\n\n// MasterConfig is the configuration for the master.\ntype MasterConfig struct {\n\tPort              int           `mapstructure:\"port\" validate:\"gte=0\"`        // master port\n\tHost              string        `mapstructure:\"host\"`                         // master host\n\tSSLMode           bool          `mapstructure:\"ssl_mode\"`                     // enable SSL mode\n\tSSLCA             string        `mapstructure:\"ssl_ca\"`                       // SSL CA file\n\tSSLCert           string        `mapstructure:\"ssl_cert\"`                     // SSL certificate file\n\tSSLKey            string        `mapstructure:\"ssl_key\"`                      // SSL key file\n\tHttpPort          int           `mapstructure:\"http_port\" validate:\"gte=0\"`   // HTTP port\n\tHttpHost          string        `mapstructure:\"http_host\"`                    // HTTP host\n\tHttpCorsDomains   []string      `mapstructure:\"http_cors_domains\"`            // add allowed cors domains\n\tHttpCorsMethods   []string      `mapstructure:\"http_cors_methods\"`            // add allowed cors methods\n\tNumJobs           int           `mapstructure:\"n_jobs\" validate:\"gt=0\"`       // number of working jobs\n\tMetaTimeout       time.Duration `mapstructure:\"meta_timeout\" validate:\"gt=0\"` // cluster meta timeout (second)\n\tDashboardUserName string        `mapstructure:\"dashboard_user_name\"`          // dashboard user name\n\tDashboardPassword string        `mapstructure:\"dashboard_password\"`           // dashboard password\n\tDashboardRedacted bool          `mapstructure:\"dashboard_redacted\"`\n\tAdminAPIKey       string        `mapstructure:\"admin_api_key\"`\n}\n\n// ServerConfig is the configuration for the server.\ntype ServerConfig struct {\n\tAPIKey         string        `mapstructure:\"api_key\"`                      // default number of returned items\n\tDefaultN       int           `mapstructure:\"default_n\" validate:\"gt=0\"`    // secret key for RESTful APIs (SSL required)\n\tClockError     time.Duration `mapstructure:\"clock_error\" validate:\"gte=0\"` // clock error in the cluster in seconds\n\tAutoInsertUser bool          `mapstructure:\"auto_insert_user\"`             // insert new users while inserting feedback\n\tAutoInsertItem bool          `mapstructure:\"auto_insert_item\"`             // insert new items while inserting feedback\n\tCacheExpire    time.Duration `mapstructure:\"cache_expire\" validate:\"gt=0\"` // server-side cache expire time\n}\n\n// RecommendConfig is the configuration of recommendation setup.\ntype RecommendConfig struct {\n\tCacheSize       int                     `mapstructure:\"cache_size\" validate:\"gt=0\"`\n\tCacheExpire     time.Duration           `mapstructure:\"cache_expire\" validate:\"gt=0\"`\n\tContextSize     int                     `mapstructure:\"context_size\" validate:\"gt=0\"`\n\tActiveUserTTL   int                     `mapstructure:\"active_user_ttl\" validate:\"gte=0\"`\n\tDataSource      DataSourceConfig        `mapstructure:\"data_source\"`\n\tNonPersonalized []NonPersonalizedConfig `mapstructure:\"non-personalized\" validate:\"dive\"`\n\tItemToItem      []ItemToItemConfig      `mapstructure:\"item-to-item\" validate:\"dive\"`\n\tUserToUser      []UserToUserConfig      `mapstructure:\"user-to-user\" validate:\"dive\"`\n\tCollaborative   CollaborativeConfig     `mapstructure:\"collaborative\"`\n\tExternal        []ExternalConfig        `mapstructure:\"external\" validate:\"dive\"`\n\tReplacement     ReplacementConfig       `mapstructure:\"replacement\"`\n\tRanker          RankerConfig            `mapstructure:\"ranker\"`\n\tFallback        FallbackConfig          `mapstructure:\"fallback\"`\n}\n\nfunc (r *RecommendConfig) ListRecommenders() []string {\n\trecommenders := make([]string, 0)\n\tfor _, rec := range r.NonPersonalized {\n\t\trecommenders = append(recommenders, rec.FullName())\n\t}\n\tfor _, rec := range r.ItemToItem {\n\t\trecommenders = append(recommenders, rec.FullName())\n\t}\n\tfor _, rec := range r.UserToUser {\n\t\trecommenders = append(recommenders, rec.FullName())\n\t}\n\tfor _, rec := range r.External {\n\t\trecommenders = append(recommenders, rec.FullName())\n\t}\n\trecommenders = append(recommenders, r.Collaborative.FullName())\n\trecommenders = append(recommenders, \"latest\")\n\treturn recommenders\n}\n\nfunc (r *RecommendConfig) Hash() string {\n\trecommenders := mapset.NewSet(r.Ranker.Recommenders...)\n\tif recommenders.IsEmpty() {\n\t\trecommenders.Append((r.ListRecommenders())...)\n\t}\n\tvar digests []string\n\tfor _, rec := range r.NonPersonalized {\n\t\tif recommenders.Contains(rec.FullName()) {\n\t\t\tdigests = append(digests, rec.Hash())\n\t\t}\n\t}\n\tfor _, rec := range r.ItemToItem {\n\t\tif recommenders.Contains(rec.FullName()) {\n\t\t\tdigests = append(digests, rec.Hash(r))\n\t\t}\n\t}\n\tfor _, rec := range r.UserToUser {\n\t\tif recommenders.Contains(rec.FullName()) {\n\t\t\tdigests = append(digests, rec.Hash(r))\n\t\t}\n\t}\n\tfor _, rec := range r.External {\n\t\tif recommenders.Contains(rec.FullName()) {\n\t\t\tdigests = append(digests, rec.Hash())\n\t\t}\n\t}\n\tif recommenders.Contains(r.Collaborative.FullName()) {\n\t\tdigests = append(digests, r.Collaborative.Hash(r))\n\t}\n\tif recommenders.Contains(\"latest\") {\n\t\tdigests = append(digests, \"latest\")\n\t}\n\treturn util.MD5(digests...)\n}\n\nfunc StringToFeedbackTypeHookFunc() mapstructure.DecodeHookFunc {\n\treturn func(\n\t\tf reflect.Type,\n\t\tt reflect.Type,\n\t\tdata interface{},\n\t) (interface{}, error) {\n\t\tif f.Kind() == reflect.String && t == reflect.TypeOf(expression.FeedbackTypeExpression{}) {\n\t\t\tvar expr expression.FeedbackTypeExpression\n\t\t\tif err := expr.FromString(data.(string)); err != nil {\n\t\t\t\treturn nil, errors.Trace(err)\n\t\t\t}\n\t\t\treturn expr, nil // only convert string to FeedbackType\n\t\t}\n\t\treturn data, nil\n\t}\n}\n\ntype DataSourceConfig struct {\n\tPositiveFeedbackTypes []expression.FeedbackTypeExpression `mapstructure:\"positive_feedback_types\"`                // positive feedback type\n\tReadFeedbackTypes     []expression.FeedbackTypeExpression `mapstructure:\"read_feedback_types\"`                    // feedback type for read event\n\tPositiveFeedbackTTL   uint                                `mapstructure:\"positive_feedback_ttl\" validate:\"gte=0\"` // time-to-live of positive feedbacks\n\tItemTTL               uint                                `mapstructure:\"item_ttl\" validate:\"gte=0\"`              // item-to-live of items\n}\n\ntype NonPersonalizedConfig struct {\n\tName   string `mapstructure:\"name\" json:\"name\"`\n\tScore  string `mapstructure:\"score\" json:\"score\" validate:\"required,item_expr\"`\n\tFilter string `mapstructure:\"filter\" json:\"filter\" validate:\"item_expr\"`\n}\n\nfunc (config *NonPersonalizedConfig) FullName() string {\n\treturn \"non-personalized/\" + config.Name\n}\n\nfunc (config *NonPersonalizedConfig) Hash() string {\n\thash := md5.New()\n\thash.Write([]byte(config.Name))\n\thash.Write([]byte(config.Score))\n\thash.Write([]byte(config.Filter))\n\treturn hex.EncodeToString(hash.Sum(nil)[:])\n}\n\ntype ItemToItemConfig struct {\n\tName   string `mapstructure:\"name\" json:\"name\"`\n\tType   string `mapstructure:\"type\" json:\"type\" validate:\"oneof=embedding tags users chat auto\"`\n\tColumn string `mapstructure:\"column\" json:\"column\" validate:\"item_expr\"`\n\tPrompt string `mapstructure:\"prompt\" json:\"prompt\"`\n}\n\nfunc (config *ItemToItemConfig) FullName() string {\n\treturn \"item-to-item/\" + config.Name\n}\n\nfunc (config *ItemToItemConfig) Hash(cfg *RecommendConfig) string {\n\thash := md5.New()\n\thash.Write([]byte(config.Name))\n\thash.Write([]byte(config.Type))\n\thash.Write([]byte(config.Column))\n\tif config.Type == \"users\" {\n\t\tfor _, expr := range cfg.DataSource.PositiveFeedbackTypes {\n\t\t\thash.Write([]byte(expr.String()))\n\t\t}\n\t}\n\treturn hex.EncodeToString(hash.Sum(nil)[:])\n}\n\ntype UserToUserConfig struct {\n\tName   string `mapstructure:\"name\" json:\"name\"`\n\tType   string `mapstructure:\"type\" json:\"type\" validate:\"oneof=embedding tags items auto\"`\n\tColumn string `mapstructure:\"column\" json:\"column\" validate:\"item_expr\"`\n}\n\nfunc (config *UserToUserConfig) FullName() string {\n\treturn \"user-to-user/\" + config.Name\n}\n\nfunc (config *UserToUserConfig) Hash(cfg *RecommendConfig) string {\n\thash := md5.New()\n\thash.Write([]byte(config.Name))\n\thash.Write([]byte(config.Type))\n\thash.Write([]byte(config.Column))\n\tif config.Type == \"items\" {\n\t\tfor _, expr := range cfg.DataSource.PositiveFeedbackTypes {\n\t\t\thash.Write([]byte(expr.String()))\n\t\t}\n\t}\n\treturn hex.EncodeToString(hash.Sum(nil)[:])\n}\n\ntype CollaborativeConfig struct {\n\tType           string              `mapstructure:\"type\" validate:\"oneof=none mf\"`\n\tFitPeriod      time.Duration       `mapstructure:\"fit_period\" validate:\"gt=0\"`\n\tFitEpoch       int                 `mapstructure:\"fit_epoch\" validate:\"gt=0\"`\n\tOptimizePeriod time.Duration       `mapstructure:\"optimize_period\" validate:\"gte=0\"`\n\tOptimizeTrials int                 `mapstructure:\"optimize_trials\" validate:\"gt=0\"`\n\tEarlyStopping  EarlyStoppingConfig `mapstructure:\"early_stopping\"`\n}\n\nfunc (config *CollaborativeConfig) FullName() string {\n\treturn \"collaborative\"\n}\n\nfunc (config *CollaborativeConfig) Hash(cfg *RecommendConfig) string {\n\thash := md5.New()\n\tfor _, expr := range cfg.DataSource.PositiveFeedbackTypes {\n\t\thash.Write([]byte(expr.String()))\n\t}\n\treturn hex.EncodeToString(hash.Sum(nil)[:])\n}\n\ntype EarlyStoppingConfig struct {\n\tPatience int `mapstructure:\"patience\"`\n}\n\ntype ExternalConfig struct {\n\tName   string `mapstructure:\"name\" json:\"name\"`\n\tScript string `mapstructure:\"script\" json:\"script\"`\n}\n\nfunc (config *ExternalConfig) FullName() string {\n\treturn \"external/\" + config.Name\n}\n\nfunc (config *ExternalConfig) Hash() string {\n\thash := md5.New()\n\thash.Write([]byte(config.Name))\n\thash.Write([]byte(config.Script))\n\treturn hex.EncodeToString(hash.Sum(nil)[:])\n}\n\ntype ReplacementConfig struct {\n\tEnableReplacement        bool    `mapstructure:\"enable_replacement\"`\n\tPositiveReplacementDecay float64 `mapstructure:\"positive_replacement_decay\" validate:\"gt=0\"`\n\tReadReplacementDecay     float64 `mapstructure:\"read_replacement_decay\" validate:\"gt=0\"`\n}\n\ntype RankerConfig struct {\n\tType             string              `mapstructure:\"type\" validate:\"oneof=none fm llm\"`\n\tRecommenders     []string            `mapstructure:\"recommenders\"`\n\tCacheExpire      time.Duration       `mapstructure:\"cache_expire\" validate:\"gt=0\"`\n\tFitPeriod        time.Duration       `mapstructure:\"fit_period\" validate:\"gt=0\"`\n\tFitEpoch         int                 `mapstructure:\"fit_epoch\" validate:\"gt=0\"`\n\tOptimizePeriod   time.Duration       `mapstructure:\"optimize_period\" validate:\"gte=0\"`\n\tOptimizeTrials   int                 `mapstructure:\"optimize_trials\" validate:\"gt=0\"`\n\tQueryTemplate    string              `mapstructure:\"query_template\"`\n\tDocumentTemplate string              `mapstructure:\"document_template\"`\n\tEarlyStopping    EarlyStoppingConfig `mapstructure:\"early_stopping\"`\n\tRerankerAPI      RerankerAPIConfig   `mapstructure:\"reranker_api\"`\n}\n\ntype FallbackConfig struct {\n\tRecommenders []string `mapstructure:\"recommenders\"`\n}\n\ntype TracingConfig struct {\n\tEnableTracing     bool    `mapstructure:\"enable_tracing\"`\n\tExporter          string  `mapstructure:\"exporter\" validate:\"oneof=zipkin otlp otlphttp\"`\n\tCollectorEndpoint string  `mapstructure:\"collector_endpoint\"`\n\tSampler           string  `mapstructure:\"sampler\"`\n\tRatio             float64 `mapstructure:\"ratio\"`\n}\n\ntype OIDCConfig struct {\n\tEnable       bool   `mapstructure:\"enable\"`\n\tIssuer       string `mapstructure:\"issuer\"`\n\tClientID     string `mapstructure:\"client_id\"`\n\tClientSecret string `mapstructure:\"client_secret\"`\n\tRedirectURL  string `mapstructure:\"redirect_url\" validate:\"omitempty,endswith=/callback/oauth2\"`\n}\n\ntype RerankerAPIConfig struct {\n\tAuthToken string `mapstructure:\"auth_token\"`\n\tModel     string `mapstructure:\"model\"`\n\tURL       string `mapstructure:\"url\"`\n}\n\ntype OpenAIConfig struct {\n\tBaseURL             string `mapstructure:\"base_url\"`\n\tAuthToken           string `mapstructure:\"auth_token\"`\n\tChatCompletionModel string `mapstructure:\"chat_completion_model\"`\n\tChatCompletionRPM   int    `mapstructure:\"chat_completion_rpm\"`\n\tChatCompletionTPM   int    `mapstructure:\"chat_completion_tpm\"`\n\tEmbeddingModel      string `mapstructure:\"embedding_model\"`\n\tEmbeddingDimensions int    `mapstructure:\"embedding_dimensions\"`\n\tEmbeddingRPM        int    `mapstructure:\"embedding_rpm\"`\n\tEmbeddingTPM        int    `mapstructure:\"embedding_tpm\"`\n\tLogFile             string `mapstructure:\"log_file\"`\n}\n\ntype BlobConfig struct {\n\tURI   string          `mapstructure:\"uri\" validate:\"required\"`\n\tS3    S3Config        `mapstructure:\"s3\"`\n\tGCS   GCSConfig       `mapstructure:\"gcs\"`\n\tAzure AzureBlobConfig `mapstructure:\"azure\"`\n}\n\ntype S3Config struct {\n\tEndpoint        string `mapstructure:\"endpoint\"`\n\tAccessKeyID     string `mapstructure:\"access_key_id\"`\n\tSecretAccessKey string `mapstructure:\"secret_access_key\"`\n}\n\ntype GCSConfig struct {\n\tCredentialsFile string `mapstructure:\"credentials_file\"`\n}\n\ntype AzureBlobConfig struct {\n\tEndpoint         string `mapstructure:\"endpoint\"`\n\tAccountName      string `mapstructure:\"account_name\"`\n\tAccountKey       string `mapstructure:\"account_key\"`\n\tConnectionString string `mapstructure:\"connection_string\"`\n}\n\nfunc GetDefaultConfig() *Config {\n\treturn &Config{\n\t\tDatabase: DatabaseConfig{\n\t\t\tDataStore:  \"sqlite://\" + filepath.Join(MkDir(), \"data.sqlite\"),\n\t\t\tCacheStore: \"sqlite://\" + filepath.Join(MkDir(), \"cache.sqlite\"),\n\t\t\tMySQL: MySQLConfig{\n\t\t\t\tIsolationLevel:  \"READ-UNCOMMITTED\",\n\t\t\t\tMaxOpenConns:    0,\n\t\t\t\tMaxIdleConns:    0,\n\t\t\t\tConnMaxLifetime: 0,\n\t\t\t},\n\t\t\tPostgres: SQLConfig{\n\t\t\t\tMaxOpenConns:    64,\n\t\t\t\tMaxIdleConns:    64,\n\t\t\t\tConnMaxLifetime: time.Minute,\n\t\t\t},\n\t\t\tRedis: RedisConfig{\n\t\t\t\tMaxSearchResults: 10000,\n\t\t\t},\n\t\t},\n\t\tMaster: MasterConfig{\n\t\t\tPort:            8086,\n\t\t\tHost:            \"0.0.0.0\",\n\t\t\tHttpPort:        8088,\n\t\t\tHttpHost:        \"0.0.0.0\",\n\t\t\tHttpCorsDomains: []string{\".*\"},\n\t\t\tHttpCorsMethods: []string{\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\"},\n\t\t\tNumJobs:         1,\n\t\t\tMetaTimeout:     10 * time.Second,\n\t\t},\n\t\tServer: ServerConfig{\n\t\t\tDefaultN:       10,\n\t\t\tClockError:     5 * time.Second,\n\t\t\tAutoInsertUser: true,\n\t\t\tAutoInsertItem: true,\n\t\t\tCacheExpire:    10 * time.Second,\n\t\t},\n\t\tRecommend: RecommendConfig{\n\t\t\tCacheSize:   100,\n\t\t\tCacheExpire: 72 * time.Hour,\n\t\t\tContextSize: 100,\n\t\t\tCollaborative: CollaborativeConfig{\n\t\t\t\tType:           \"none\",\n\t\t\t\tFitPeriod:      60 * time.Minute,\n\t\t\t\tFitEpoch:       100,\n\t\t\t\tOptimizePeriod: 0,\n\t\t\t\tOptimizeTrials: 10,\n\t\t\t},\n\t\t\tReplacement: ReplacementConfig{\n\t\t\t\tEnableReplacement:        false,\n\t\t\t\tPositiveReplacementDecay: 0.8,\n\t\t\t\tReadReplacementDecay:     0.6,\n\t\t\t},\n\t\t\tRanker: RankerConfig{\n\t\t\t\tType:           \"none\",\n\t\t\t\tCacheExpire:    120 * time.Hour,\n\t\t\t\tFitPeriod:      60 * time.Minute,\n\t\t\t\tFitEpoch:       100,\n\t\t\t\tOptimizePeriod: 0,\n\t\t\t\tOptimizeTrials: 10,\n\t\t\t\tRecommenders:   []string{\"latest\"},\n\t\t\t},\n\t\t},\n\t\tTracing: TracingConfig{\n\t\t\tExporter: \"otlp\",\n\t\t\tSampler:  \"always\",\n\t\t},\n\t\tBlob: BlobConfig{\n\t\t\tURI: MkDir(\"blob\"),\n\t\t},\n\t}\n}\n\n//go:embed config.toml\nvar ConfigTOML string\n\nfunc (config *Config) Now() *time.Time {\n\treturn lo.ToPtr(time.Now().Add(config.Server.ClockError))\n}\n\nfunc (config *TracingConfig) NewTracerProvider() (trace.TracerProvider, error) {\n\tif !config.EnableTracing {\n\t\treturn trace.NewNoopTracerProvider(), nil\n\t}\n\n\tvar exporter tracesdk.SpanExporter\n\tvar err error\n\tswitch config.Exporter {\n\tcase \"zipkin\":\n\t\texporter, err = zipkin.New(config.CollectorEndpoint)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\tcase \"otlp\":\n\t\tclient := otlptracegrpc.NewClient(otlptracegrpc.WithInsecure(), otlptracegrpc.WithEndpoint(config.CollectorEndpoint))\n\t\texporter, err = otlptrace.New(context.TODO(), client)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\tcase \"otlphttp\":\n\t\tclient := otlptracehttp.NewClient(otlptracehttp.WithInsecure(), otlptracehttp.WithEndpoint(config.CollectorEndpoint))\n\t\texporter, err = otlptrace.New(context.TODO(), client)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\tdefault:\n\t\treturn nil, errors.NotSupportedf(\"exporter %s\", config.Exporter)\n\t}\n\n\tvar sampler tracesdk.Sampler\n\tswitch config.Sampler {\n\tcase \"always\":\n\t\tsampler = tracesdk.AlwaysSample()\n\tcase \"never\":\n\t\tsampler = tracesdk.NeverSample()\n\tcase \"ratio\":\n\t\tsampler = tracesdk.TraceIDRatioBased(config.Ratio)\n\tdefault:\n\t\treturn nil, errors.NotSupportedf(\"sampler %s\", config.Sampler)\n\t}\n\n\treturn tracesdk.NewTracerProvider(\n\t\ttracesdk.WithSampler(sampler),\n\t\ttracesdk.WithBatcher(exporter),\n\t\ttracesdk.WithResource(resource.NewWithAttributes(\n\t\t\tsemconv.SchemaURL,\n\t\t\tsemconv.ServiceNameKey.String(\"gorse\"),\n\t\t)),\n\t), nil\n}\n\nfunc (config *TracingConfig) Equal(other TracingConfig) bool {\n\tif config == nil {\n\t\treturn false\n\t}\n\treturn config.EnableTracing == other.EnableTracing &&\n\t\tconfig.Exporter == other.Exporter &&\n\t\tconfig.CollectorEndpoint == other.CollectorEndpoint &&\n\t\tconfig.Sampler == other.Sampler &&\n\t\tconfig.Ratio == other.Ratio\n}\n\nfunc setDefault() {\n\tdefaultConfig := GetDefaultConfig()\n\t// [database]\n\tviper.SetDefault(\"database.data_store\", defaultConfig.Database.DataStore)\n\tviper.SetDefault(\"database.cache_store\", defaultConfig.Database.CacheStore)\n\t// [database.mysql]\n\tviper.SetDefault(\"database.mysql.isolation_level\", defaultConfig.Database.MySQL.IsolationLevel)\n\tviper.SetDefault(\"database.mysql.max_open_conns\", defaultConfig.Database.MySQL.MaxOpenConns)\n\tviper.SetDefault(\"database.mysql.max_idle_conns\", defaultConfig.Database.MySQL.MaxIdleConns)\n\tviper.SetDefault(\"database.mysql.conn_max_lifetime\", defaultConfig.Database.MySQL.ConnMaxLifetime)\n\t// [database.postgres]\n\tviper.SetDefault(\"database.postgres.max_open_conns\", defaultConfig.Database.Postgres.MaxOpenConns)\n\tviper.SetDefault(\"database.postgres.max_idle_conns\", defaultConfig.Database.Postgres.MaxIdleConns)\n\tviper.SetDefault(\"database.postgres.conn_max_lifetime\", defaultConfig.Database.Postgres.ConnMaxLifetime)\n\t// [database.redis]\n\tviper.SetDefault(\"database.redis.max_search_results\", defaultConfig.Database.Redis.MaxSearchResults)\n\t// [master]\n\tviper.SetDefault(\"master.port\", defaultConfig.Master.Port)\n\tviper.SetDefault(\"master.host\", defaultConfig.Master.Host)\n\tviper.SetDefault(\"master.http_port\", defaultConfig.Master.HttpPort)\n\tviper.SetDefault(\"master.http_host\", defaultConfig.Master.HttpHost)\n\tviper.SetDefault(\"master.http_cors_domains\", defaultConfig.Master.HttpCorsDomains)\n\tviper.SetDefault(\"master.http_cors_methods\", defaultConfig.Master.HttpCorsMethods)\n\tviper.SetDefault(\"master.n_jobs\", defaultConfig.Master.NumJobs)\n\tviper.SetDefault(\"master.meta_timeout\", defaultConfig.Master.MetaTimeout)\n\t// [server]\n\tviper.SetDefault(\"server.api_key\", defaultConfig.Server.APIKey)\n\tviper.SetDefault(\"server.default_n\", defaultConfig.Server.DefaultN)\n\tviper.SetDefault(\"server.clock_error\", defaultConfig.Server.ClockError)\n\tviper.SetDefault(\"server.auto_insert_user\", defaultConfig.Server.AutoInsertUser)\n\tviper.SetDefault(\"server.auto_insert_item\", defaultConfig.Server.AutoInsertItem)\n\tviper.SetDefault(\"server.cache_expire\", defaultConfig.Server.CacheExpire)\n\t// [recommend]\n\tviper.SetDefault(\"recommend.cache_size\", defaultConfig.Recommend.CacheSize)\n\tviper.SetDefault(\"recommend.cache_expire\", defaultConfig.Recommend.CacheExpire)\n\tviper.SetDefault(\"recommend.context_size\", defaultConfig.Recommend.ContextSize)\n\t// [recommend.collaborative]\n\tviper.SetDefault(\"recommend.collaborative.type\", defaultConfig.Recommend.Collaborative.Type)\n\tviper.SetDefault(\"recommend.collaborative.fit_period\", defaultConfig.Recommend.Collaborative.FitPeriod)\n\tviper.SetDefault(\"recommend.collaborative.fit_epoch\", defaultConfig.Recommend.Collaborative.FitEpoch)\n\tviper.SetDefault(\"recommend.collaborative.optimize_period\", defaultConfig.Recommend.Collaborative.OptimizePeriod)\n\tviper.SetDefault(\"recommend.collaborative.optimize_trials\", defaultConfig.Recommend.Collaborative.OptimizeTrials)\n\t// [recommend.replacement]\n\tviper.SetDefault(\"recommend.replacement.enable_replacement\", defaultConfig.Recommend.Replacement.EnableReplacement)\n\tviper.SetDefault(\"recommend.replacement.positive_replacement_decay\", defaultConfig.Recommend.Replacement.PositiveReplacementDecay)\n\tviper.SetDefault(\"recommend.replacement.read_replacement_decay\", defaultConfig.Recommend.Replacement.ReadReplacementDecay)\n\t// [recommend.ranker]\n\tviper.SetDefault(\"recommend.ranker.type\", defaultConfig.Recommend.Ranker.Type)\n\tviper.SetDefault(\"recommend.ranker.cache_expire\", defaultConfig.Recommend.Ranker.CacheExpire)\n\tviper.SetDefault(\"recommend.ranker.fit_period\", defaultConfig.Recommend.Ranker.FitPeriod)\n\tviper.SetDefault(\"recommend.ranker.fit_epoch\", defaultConfig.Recommend.Ranker.FitEpoch)\n\tviper.SetDefault(\"recommend.ranker.optimize_period\", defaultConfig.Recommend.Ranker.OptimizePeriod)\n\tviper.SetDefault(\"recommend.ranker.optimize_trials\", defaultConfig.Recommend.Ranker.OptimizeTrials)\n\tviper.SetDefault(\"recommend.ranker.recommenders\", defaultConfig.Recommend.Ranker.Recommenders)\n\t// [recommend.fallback]\n\tviper.SetDefault(\"recommend.fallback\", defaultConfig.Recommend.Fallback)\n\t// [tracing]\n\tviper.SetDefault(\"tracing.exporter\", defaultConfig.Tracing.Exporter)\n\tviper.SetDefault(\"tracing.sampler\", defaultConfig.Tracing.Sampler)\n\t// [blob]\n\tviper.SetDefault(\"blob.uri\", defaultConfig.Blob.URI)\n}\n\ntype configBinding struct {\n\tkey string\n\tenv string\n}\n\nvar bindings = []configBinding{\n\t{\"database.cache_store\", \"GORSE_CACHE_STORE\"},\n\t{\"database.data_store\", \"GORSE_DATA_STORE\"},\n\t{\"database.table_prefix\", \"GORSE_TABLE_PREFIX\"},\n\t{\"database.cache_table_prefix\", \"GORSE_CACHE_TABLE_PREFIX\"},\n\t{\"database.data_table_prefix\", \"GORSE_DATA_TABLE_PREFIX\"},\n\t{\"master.port\", \"GORSE_MASTER_PORT\"},\n\t{\"master.host\", \"GORSE_MASTER_HOST\"},\n\t{\"master.ssl_mode\", \"GORSE_MASTER_SSL_MODE\"},\n\t{\"master.ssl_ca\", \"GORSE_MASTER_SSL_CA\"},\n\t{\"master.ssl_cert\", \"GORSE_MASTER_SSL_CERT\"},\n\t{\"master.ssl_key\", \"GORSE_MASTER_SSL_KEY\"},\n\t{\"master.http_port\", \"GORSE_MASTER_HTTP_PORT\"},\n\t{\"master.http_host\", \"GORSE_MASTER_HTTP_HOST\"},\n\t{\"master.n_jobs\", \"GORSE_MASTER_JOBS\"},\n\t{\"master.dashboard_user_name\", \"GORSE_DASHBOARD_USER_NAME\"},\n\t{\"master.dashboard_password\", \"GORSE_DASHBOARD_PASSWORD\"},\n\t{\"master.dashboard_auth_server\", \"GORSE_DASHBOARD_AUTH_SERVER\"},\n\t{\"master.dashboard_redacted\", \"GORSE_DASHBOARD_REDACTED\"},\n\t{\"master.admin_api_key\", \"GORSE_ADMIN_API_KEY\"},\n\t{\"server.api_key\", \"GORSE_SERVER_API_KEY\"},\n\t{\"oidc.enable\", \"GORSE_OIDC_ENABLE\"},\n\t{\"oidc.issuer\", \"GORSE_OIDC_ISSUER\"},\n\t{\"oidc.client_id\", \"GORSE_OIDC_CLIENT_ID\"},\n\t{\"oidc.client_secret\", \"GORSE_OIDC_CLIENT_SECRET\"},\n\t{\"oidc.redirect_url\", \"GORSE_OIDC_REDIRECT_URL\"},\n\t{\"blob.uri\", \"GORSE_BLOB_URI\"},\n\t{\"blob.s3.endpoint\", \"S3_ENDPOINT\"},\n\t{\"blob.s3.access_key_id\", \"S3_ACCESS_KEY_ID\"},\n\t{\"blob.s3.secret_access_key\", \"S3_SECRET_ACCESS_KEY\"},\n\t{\"blob.gcs.credentials_file\", \"GCS_CREDENTIALS_FILE\"},\n\t{\"blob.azure.endpoint\", \"AZURE_STORAGE_ENDPOINT\"},\n\t{\"blob.azure.account_name\", \"AZURE_STORAGE_ACCOUNT_NAME\"},\n\t{\"blob.azure.account_key\", \"AZURE_STORAGE_ACCOUNT_KEY\"},\n\t{\"blob.azure.connection_string\", \"AZURE_STORAGE_CONNECTION_STRING\"},\n\t{\"openai.base_url\", \"OPENAI_BASE_URL\"},\n\t{\"openai.auth_token\", \"OPENAI_AUTH_TOKEN\"},\n\t{\"openai.chat_completion_model\", \"OPENAI_CHAT_COMPLETION_MODEL\"},\n\t{\"recommend.ranker.reranker_api.url\", \"RERANKER_URL\"},\n\t{\"recommend.ranker.reranker_api.model\", \"RERANKER_MODEL\"},\n\t{\"recommend.ranker.reranker_api.auth_token\", \"RERANKER_AUTH_TOKEN\"},\n}\n\n// LoadConfig loads configuration from toml file.\nfunc LoadConfig(path string) (*Config, error) {\n\t// set default config\n\tsetDefault()\n\n\t// bind environment bindings\n\tfor _, binding := range bindings {\n\t\terr := viper.BindEnv(binding.key, binding.env)\n\t\tif err != nil {\n\t\t\tlog.Logger().Fatal(\"failed to bind a Viper key to a ENV variable\", zap.Error(err))\n\t\t}\n\t}\n\n\t// load config file if provided\n\tif path != \"\" {\n\t\t// check if file exist\n\t\tif _, err := os.Stat(path); err != nil {\n\t\t\tif os.IsNotExist(err) {\n\t\t\t\tlog.Logger().Warn(\"config file not found, use default config\", zap.String(\"path\", path))\n\t\t\t} else {\n\t\t\t\treturn nil, errors.Trace(err)\n\t\t\t}\n\t\t} else {\n\t\t\t// load config file\n\t\t\tviper.SetConfigFile(path)\n\t\t\tif err := viper.ReadInConfig(); err != nil {\n\t\t\t\treturn nil, errors.Trace(err)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tlog.Logger().Info(\"no config file provided, use defaults and environment variables\")\n\t}\n\n\t// unmarshal config file\n\tvar conf Config\n\tif err := viper.Unmarshal(&conf); err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\n\t// validate config file\n\tif err := conf.Validate(); err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\n\t// apply table prefix\n\tif conf.Database.CacheTablePrefix == \"\" {\n\t\tconf.Database.CacheTablePrefix = conf.Database.TablePrefix\n\t}\n\tif conf.Database.DataTablePrefix == \"\" {\n\t\tconf.Database.DataTablePrefix = conf.Database.TablePrefix\n\t}\n\treturn &conf, nil\n}\n\nfunc (config *Config) Validate() error {\n\t// Check non-personalized recommenders\n\tnonPersonalizedNames := mapset.NewSet[string]()\n\tfor _, nonPersonalized := range config.Recommend.NonPersonalized {\n\t\tif nonPersonalizedNames.Contains(nonPersonalized.Name) {\n\t\t\treturn errors.Errorf(\"non-personalized recommender %v is duplicated\", nonPersonalized.Name)\n\t\t}\n\t\tnonPersonalizedNames.Add(nonPersonalized.Name)\n\t}\n\n\t// Check item-to-item recommenders\n\titemToItemNames := mapset.NewSet[string]()\n\tfor _, itemToItem := range config.Recommend.ItemToItem {\n\t\tif itemToItemNames.Contains(itemToItem.Name) {\n\t\t\treturn errors.Errorf(\"item-to-item recommender %v is duplicated\", itemToItem.Name)\n\t\t}\n\t\titemToItemNames.Add(itemToItem.Name)\n\t}\n\n\t// Check recommender existence and collaborative enabled\n\tavailableRecommenders := mapset.NewSet[string]()\n\tfor _, rec := range config.Recommend.NonPersonalized {\n\t\tavailableRecommenders.Add(rec.FullName())\n\t}\n\tfor _, rec := range config.Recommend.ItemToItem {\n\t\tavailableRecommenders.Add(rec.FullName())\n\t}\n\tfor _, rec := range config.Recommend.UserToUser {\n\t\tavailableRecommenders.Add(rec.FullName())\n\t}\n\tfor _, rec := range config.Recommend.External {\n\t\tavailableRecommenders.Add(rec.FullName())\n\t}\n\tavailableRecommenders.Add(\"latest\")\n\tif config.Recommend.Collaborative.Type != \"none\" {\n\t\tavailableRecommenders.Add(config.Recommend.Collaborative.FullName())\n\t}\n\tcheckRecommenders := func(recommenders []string) error {\n\t\tfor _, recommender := range recommenders {\n\t\t\tif recommender == config.Recommend.Collaborative.FullName() && config.Recommend.Collaborative.Type == \"none\" {\n\t\t\t\treturn errors.New(\"collaborative recommender is disabled\")\n\t\t\t}\n\t\t\tif !availableRecommenders.Contains(recommender) {\n\t\t\t\treturn errors.Errorf(\"recommender %v doesn't exist\", recommender)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tvalidate := validator.New()\n\tif err := validate.RegisterValidation(\"data_store\", func(fl validator.FieldLevel) bool {\n\t\tprefixes := []string{\n\t\t\tstorage.MongoPrefix,\n\t\t\tstorage.MongoSrvPrefix,\n\t\t\tstorage.MySQLPrefix,\n\t\t\tstorage.PostgresPrefix,\n\t\t\tstorage.PostgreSQLPrefix,\n\t\t\tstorage.ClickhousePrefix,\n\t\t\tstorage.CHHTTPPrefix,\n\t\t\tstorage.CHHTTPSPrefix,\n\t\t\tstorage.SQLitePrefix,\n\t\t}\n\t\tfor _, prefix := range prefixes {\n\t\t\tif strings.HasPrefix(fl.Field().String(), prefix) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif err := validate.RegisterValidation(\"cache_store\", func(fl validator.FieldLevel) bool {\n\t\tprefixes := []string{\n\t\t\tstorage.RedisPrefix,\n\t\t\tstorage.RedissPrefix,\n\t\t\tstorage.MongoPrefix,\n\t\t\tstorage.MongoSrvPrefix,\n\t\t\tstorage.MySQLPrefix,\n\t\t\tstorage.PostgresPrefix,\n\t\t\tstorage.PostgreSQLPrefix,\n\t\t\tstorage.SQLitePrefix,\n\t\t}\n\t\tfor _, prefix := range prefixes {\n\t\t\tif strings.HasPrefix(fl.Field().String(), prefix) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif err := validate.RegisterValidation(\"item_expr\", func(fl validator.FieldLevel) bool {\n\t\tif fl.Field().String() == \"\" {\n\t\t\t// Empty expression is legal.\n\t\t\treturn true\n\t\t}\n\t\t_, err := parser.Parse(fl.Field().String())\n\t\treturn err == nil\n\t}); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tvalidate.RegisterTagNameFunc(func(fld reflect.StructField) string {\n\t\treturn strings.SplitN(fld.Tag.Get(\"mapstructure\"), \",\", 2)[0]\n\t})\n\terr := validate.Struct(config)\n\tif err != nil {\n\t\t// translate errors\n\t\ttrans := ut.New(en.New()).GetFallback()\n\t\tif err := en_translations.RegisterDefaultTranslations(validate, trans); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\tif err := validate.RegisterTranslation(\"data_store\", trans, func(ut ut.Translator) error {\n\t\t\treturn ut.Add(\"data_store\", \"unsupported data storage backend\", true) // see universal-translator for details\n\t\t}, func(ut ut.Translator, fe validator.FieldError) string {\n\t\t\tt, _ := ut.T(\"data_store\", fe.Field())\n\t\t\treturn t\n\t\t}); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\tif err := validate.RegisterTranslation(\"cache_store\", trans, func(ut ut.Translator) error {\n\t\t\treturn ut.Add(\"cache_store\", \"unsupported cache storage backend\", true) // see universal-translator for details\n\t\t}, func(ut ut.Translator, fe validator.FieldError) string {\n\t\t\tt, _ := ut.T(\"cache_store\", fe.Field())\n\t\t\treturn t\n\t\t}); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\tif err := validate.RegisterTranslation(\"item_expr\", trans, func(ut ut.Translator) error {\n\t\t\treturn ut.Add(\"item_expr\", \"invalid item expression\", true)\n\t\t}, func(ut ut.Translator, fe validator.FieldError) string {\n\t\t\tt, _ := ut.T(\"item_expr\", fe.Field())\n\t\t\treturn t\n\t\t}); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\terrs := err.(validator.ValidationErrors)\n\t\tfor _, e := range errs {\n\t\t\treturn errors.New(e.Translate(trans))\n\t\t}\n\t}\n\n\tif len(config.Recommend.Ranker.Recommenders) == 0 {\n\t\treturn errors.New(\"ranker.recommenders must not be empty\")\n\t}\n\tif config.Recommend.Ranker.Type == \"none\" && len(config.Recommend.Ranker.Recommenders) > 1 {\n\t\treturn errors.New(\"ranker.recommenders must contain at most one recommender when ranker.type is none\")\n\t}\n\tif err := checkRecommenders(config.Recommend.Ranker.Recommenders); err != nil {\n\t\treturn err\n\t}\n\tif err := checkRecommenders(config.Recommend.Fallback.Recommenders); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nvar RootDir string\n\n// MkDir creates a directory under Gorse home directory.\nfunc MkDir(elem ...string) string {\n\tif RootDir == \"\" {\n\t\tRootDir = filepath.Join(lo.Must(os.UserHomeDir()), \".gorse\", \"var\", \"lib\")\n\t}\n\tpath := filepath.Join(RootDir, filepath.Join(elem...))\n\tlo.Must0(os.MkdirAll(path, 0755))\n\treturn path\n}\n"
  },
  {
    "path": "config/config.toml",
    "content": "[database]\n\n# The database for caching, support Redis, MySQL, Postgres and MongoDB:\n#   redis://<user>:<password>@<host>:<port>/<db_number>\n#   rediss://<user>:<password>@<host>:<port>/<db_number>\n#   redis+cluster://<user>:<password>@<host>:<port>[?addr=<host2>:<port2>&addr=<host3>:<port3>]\n#   rediss+cluster://<user>:<password>@<host>:<port>[?addr=<host2>:<port2>&addr=<host3>:<port3>]\n#   mysql://[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]\n#   postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full\n#   postgresql://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full\n#   mongodb://[username:password@]host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]]\n#   mongodb+srv://[username:password@]host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]]\n#   sqlite://<path>\ncache_store = \"redis://localhost:6379/0\"\n\n# The database for persist data, support MySQL, Postgres, ClickHouse and MongoDB:\n#   mysql://[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]\n#   postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full\n#   postgresql://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full\n#   clickhouse://user:password@host[:port]/database?param1=value1&...&paramN=valueN\n#   chhttp://user:password@host[:port]/database?param1=value1&...&paramN=valueN\n#   chhttps://user:password@host[:port]/database?param1=value1&...&paramN=valueN\n#   mongodb://[username:password@]host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]]\n#   mongodb+srv://[username:password@]host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]]\n#   sqlite://<path>\ndata_store = \"mysql://gorse:gorse_pass@tcp(localhost:3306)/gorse\"\n\n# The naming prefix for tables (collections, keys) in databases. The default value is empty.\ntable_prefix = \"\"\n\n# The naming prefix for tables (collections, keys) in cache storage databases. The default value is `table_prefix`.\ncache_table_prefix = \"\"\n\n# The naming prefix for tables (collections, keys) in data storage databases. The default value is `table_prefix`.\ndata_table_prefix = \"\"\n\n[database.mysql]\n\n# Transaction isolation level. The default value is \"READ-UNCOMMITTED\".\nisolation_level = \"READ-UNCOMMITTED\"\n\n# Maximum number of open connections to the database. Set to 0 to keep the driver default.\nmax_open_conns = 0\n\n# Maximum number of idle connections to the database. Set to 0 to keep the driver default.\nmax_idle_conns = 0\n\n# Maximum amount of time a connection may be reused. Set to \"0s\" to keep the driver default.\nconn_max_lifetime = \"0s\"\n\n[database.postgres]\n\n# Maximum number of open connections to the database. The default value is 64.\nmax_open_conns = 64\n\n# Maximum number of idle connections to the database. The default value is 64.\nmax_idle_conns = 64\n\n# Maximum amount of time a connection may be reused. The default value is \"1m\".\nconn_max_lifetime = \"1m\"\n\n[database.redis]\n\n# The maximum number of results to be returned by the FT.SEARCH command if LIMIT is used\nmax_search_results = 10000\n\n[master]\n\n# GRPC port of the master node. The default value is 8086.\nport = 8086\n\n# gRPC host of the master node. The default values is \"0.0.0.0\".\nhost = \"0.0.0.0\"\n\n# Enable SSL for the gRPC communication. The default value is false.\nssl_mode = false\n\n# SSL certification authority for the gRPC communication.\nssl_ca = \"\"\n\n# SSL certification for the gRPC communication.\nssl_cert = \"\"\n\n# SSL certification key for the gRPC communication.\nssl_key = \"\"\n\n# HTTP port of the master node. The default values is 8088.\nhttp_port = 8088\n\n# HTTP host of the master node. The default values is \"0.0.0.0\".\nhttp_host = \"0.0.0.0\"\n\n# AllowedDomains is a list of allowed values for Http Origin.\n# The list may contain the special wildcard string \".*\" ; all is allowed\n# If empty all are allowed.\nhttp_cors_domains = []\n\n# AllowedMethods is either empty or has a list of http methods names. Checking is case-insensitive.\nhttp_cors_methods = []\n\n# Number of working jobs in the master node. The default value is 1.\nn_jobs = 1\n\n# Meta information timeout. The default value is 10s.\nmeta_timeout = \"10s\"\n\n# Username for the master node dashboard.\ndashboard_user_name = \"\"\n\n# Password for the master node dashboard.\ndashboard_password = \"\"\n\n# Secret key for admin APIs (SSL required).\nadmin_api_key = \"\"\n\n[server]\n\n# Default number of returned items. The default value is 10.\ndefault_n = 10\n\n# Secret key for RESTful APIs (SSL required).\napi_key = \"\"\n\n# Clock error in the cluster. The default value is 5s.\nclock_error = \"5s\"\n\n# Insert new users while inserting feedback. The default value is true.\nauto_insert_user = true\n\n# Insert new items while inserting feedback. The default value is true.\nauto_insert_item = true\n\n# Server-side cache expire time. The default value is 10s.\ncache_expire = \"10s\"\n\n[recommend]\n\n# The cache size for recommended/popular/latest items. The default value is 10.\ncache_size = 100\n\n# Recommended cache expire time. The default value is 72h.\ncache_expire = \"72h\"\n\n# The context size for online recommendations. Online recommendations can't use all user feedbacks to generate\n# recommendations for efficiency consideration. Instead, only the latest `context_size` feedbacks are used.\n# The default value is 100.\ncontext_size = 100\n\n# The time-to-live (days) of active users, 0 means disabled. Recommendation won't be cached for inactive users. The default value is 0.\nactive_user_ttl = 0\n\n[recommend.data_source]\n\n# The feedback types for positive events.\npositive_feedback_types = [\"star\",\"like\",\"read>=3\"]\n\n# The feedback types for read events.\nread_feedback_types = [\"read\"]\n\n# The time-to-live (days) of positive feedback, 0 means disabled. The default value is 0.\npositive_feedback_ttl = 0\n\n# The time-to-live (days) of items, 0 means disabled. The default value is 0.\nitem_ttl = 0\n\n[[recommend.non-personalized]]\n\n# The name of the leaderboard.\nname = \"most_starred_weekly\"\n\n# The score function for items in the leaderboard.\nscore = \"count(feedback, .FeedbackType == 'star')\"\n\n# The filter for items in the leaderboard.\nfilter = \"(now() - item.Timestamp).Hours() < 168\"\n\n[[recommend.item-to-item]]\n\n# The name of the item-to-item recommender.\nname = \"neighbors\"\n\n# The type of the item-to-item recommender. There are three types:\n#   embedding: recommend by Euclidean distance of embeddings.\n#   tags: recommend by number of common tags.\n#   users: recommend by number of common users.\ntype = \"embedding\"\n\n# The column of the item embeddings. Leave blank if type is \"users\".\ncolumn = \"item.Labels.embedding\"\n\n[[recommend.user-to-user]]\n\n# The name of the user-to-user recommender.\nname = \"neighbors\"\n\n# The type of the user-to-user recommender. There are three types:\n#   embedding: recommend by Euclidean distance of embeddings.\n#   tags: recommend by number of common tags.\n#   items: recommend by number of common items.\ntype = \"items\"\n\n[[recommend.external]]\n\n# The name of the external recommender.\nname = \"trending\"\n\n# The script to fetch external recommended items. The script should return a list of item IDs.\nscript = \"\"\"\nconst response = fetch(\"https://cdn.jsdelivr.net/gh/isboyjc/github-trending-api/data/daily/all.json\");\nif (!response.ok) {\n  throw new Error(`${response.status} ${response.body}`);\n}\nconst data = JSON.parse(response.body);\ndata[\"items\"].map((item) => {\n  return item[\"title\"].toLowerCase().replace(\"/\", \":\");\n})\n\"\"\"\n\n[recommend.collaborative]\n\n# The type of collaborative filtering. Supported values:\n#   none: disable collaborative filtering.\n#   mf: matrix factorization.\ntype = \"mf\"\n\n# The time period for model fitting. The default value is \"60m\".\nfit_period = \"60m\"\n\n# The number of epochs for model fitting. The default value is 100.\nfit_epoch = 100\n\n# The time period for hyperparameter optimization, set to 0 to disable. The default value is \"0\".\noptimize_period = \"360m\"\n\n# The number of trials for hyperparameter optimization. The default value is 10.\noptimize_trials = 10\n\n[recommend.collaborative.early_stopping]\n\n# Number of epochs to wait if no improvement and then stop the training. The default value is 10.\npatience = 10\n\n[recommend.replacement]\n\n# Replace historical items back to recommendations. The default value is false.\nenable_replacement = false\n\n# Decay the weights of replaced items from positive feedbacks. The default value is 0.8.\npositive_replacement_decay = 0.8\n\n# Decay the weights of replaced items from read feedbacks. The default value is 0.6.\nread_replacement_decay = 0.6\n\n[recommend.ranker]\n\n# The type of the ranker. There are two types:\n#   none: no ranking (default).\n#   fm: factorization machines.\n#   llm: LLM-based reranker.\ntype = \"fm\"\n\n# The time period to refresh recommendation for inactive users. The default values is 120h.\ncache_expire = \"120h\"\n\n# The recommenders used to fetch candidate items before ranking. The default values is all recommenders.\nrecommenders = [\"latest\", \"collaborative\", \"non-personalized/most_starred_weekly\", \"item-to-item/neighbors\", \"user-to-user/neighbors\"]\n\n# The time period for model fitting. The default value is \"60m\".\nfit_period = \"60m\"\n\n# The number of epochs for model fitting. The default value is 100.\nfit_epoch = 100\n\n# The time period for hyperparameter optimization, set to 0 to disable. The default value is \"0\".\noptimize_period = \"360m\"\n\n# The number of trials for hyperparameter optimization. The default value is 10.\noptimize_trials = 10\n\n# The prompt template for query.\nquery_template = \"\"\"\nYou are a GitHub repository recommender system. Given a user is interested in the following repositories:\n{% for repo in feedback -%}\n- {{ repo.Comment }}\n{% endfor -%}\nPlease sort repositories by the user's interests.\n\"\"\"\n\n# The prompt template for documents.\ndocument_template = '{{ item.Comment | replace(\",\", \" \") | replace(\"\\n\", \" \") }}'\n\n[recommend.ranker.early_stopping]\n\n# Number of epochs to wait if no improvement and then stop the training. The default value is 10.\npatience = 10\n\n[recommend.ranker.reranker_api]\n\n# Auth token for the reranker API.\nauth_token = \"\"\n\n# The reranker model.\nmodel = \"qwen3-rerank\"\n\n# URL for the reranker API, supports Jina style.\nurl = \"https://dashscope.aliyuncs.com/compatible-api/v1/reranks\"\n\n[recommend.fallback]\n\n# The fallback recommenders are used when cached recommendation drained out. The default values is [\"latest\"].\nrecommenders = [\"item-to-item/neighbors\", \"latest\"]\n\n[tracing]\n\n# Enable tracing for REST APIs. The default value is false.\nenable_tracing = false\n\n# The type of tracing exporters should be one of \"zipkin\", \"otlp\" and \"otlphttp\". The default value is \"otlp\".\nexporter = \"otlp\"\n\n# The endpoint of tracing collector.\ncollector_endpoint = \"http://localhost:4317\"\n\n# The type of tracing sampler should be one of \"always\", \"never\" and \"ratio\". The default value is \"always\".\nsampler = \"always\"\n\n# The ratio of ratio based sampler. The default value is 1.\nratio = 1\n\n[oidc]\n\n# Enable OpenID Connect (OIDC) authentication. The default value is false.\nenable = false\n\n# The issuer of the OAuth provider.\nissuer = \"\"\n\n# Public identifier of the OAuth application.\nclient_id = \"\"\n\n# Token access to the OAuth application.\nclient_secret = \"\"\n\n# URL used by the OAuth provider to redirect users after they are successfully authenticated\n# (also referred to as the callback URL). You should set this to the concatenation of the\n# Gorse dashboard URL and \"/callback/oauth2\". For example, if the Gorse dashboard URL is\n# http://localhost:8088, the redirect URL should be: http://localhost:8088/callback/oauth2\nredirect_url = \"\"\n\n[blob]\n\n# Blob storage URI.\n#   - S3: s3://bucket/path\n#   - GCS: gs://my-bucket/my-database\n#   - Azure Blob: az://container/path\n#   - Local: /path/without/prefix\nuri = \"/var/lib/gorse/blob\"\n\n[blob.s3]\n\n# S3 endpoint.\nendpoint = \"\"\n\n# S3 access key ID.\naccess_key_id = \"\"\n\n# S3 secret access key.\nsecret_access_key = \"\"\n\n[blob.gcs]\n\n# GCS credentials file.\ncredentials_file = \"\"\n\n[blob.azure]\n\n# Azure Blob endpoint. Leave empty to use https://<account_name>.blob.core.windows.net/.\nendpoint = \"\"\n\n# Azure storage account name.\naccount_name = \"\"\n\n# Azure storage account key.\naccount_key = \"\"\n\n# Azure storage connection string (takes precedence over account_name/account_key).\nconnection_string = \"\"\n\n[openai]\n\n# Base URL of OpenAI API.\nbase_url = \"http://localhost:11434/v1\"\n\n# API key of OpenAI API.\nauth_token = \"ollama\"\n\n# Name of chat completion model.\nchat_completion_model = \"qwen2.5\"\n\n# Maximum requests per minute for chat completion.\nchat_completion_rpm = 15000\n\n# Maximum tokens per minute for chat completion.\nchat_completion_tpm = 1200000\n\n# Name of embedding model.\nembedding_model = \"mxbai-embed-large\"\n\n# Dimensions of embedding vectors.\nembedding_dimensions = 1024\n\n# Maximum requests per minute for embedding.\nembedding_rpm = 1800\n\n# Maximum tokens per minute for embedding.\nembedding_tpm = 1200000\n\n# Log file for OpenAI API.\nlog_file = \"openai.log\"\n"
  },
  {
    "path": "config/config_test.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage config\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/common/expression\"\n\t\"github.com/sclevine/yj/convert\"\n\t\"github.com/spf13/viper\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\nfunc TestUnmarshal(t *testing.T) {\n\ttext := ConfigTOML\n\ttext = strings.Replace(text, \"ssl_mode = false\", \"ssl_mode = true\", -1)\n\ttext = strings.Replace(text, \"ssl_ca = \\\"\\\"\", \"ssl_ca = \\\"ca.pem\\\"\", -1)\n\ttext = strings.Replace(text, \"ssl_cert = \\\"\\\"\", \"ssl_cert = \\\"cert.pem\\\"\", -1)\n\ttext = strings.Replace(text, \"ssl_key = \\\"\\\"\", \"ssl_key = \\\"key.pem\\\"\", -1)\n\ttext = strings.Replace(text, \"dashboard_user_name = \\\"\\\"\", \"dashboard_user_name = \\\"admin\\\"\", -1)\n\ttext = strings.Replace(text, \"dashboard_password = \\\"\\\"\", \"dashboard_password = \\\"password\\\"\", -1)\n\ttext = strings.Replace(text, \"admin_api_key = \\\"\\\"\", \"admin_api_key = \\\"super_api_key\\\"\", -1)\n\ttext = strings.Replace(text, \"api_key = \\\"\\\"\", \"api_key = \\\"19260817\\\"\", -1)\n\ttext = strings.Replace(text, \"table_prefix = \\\"\\\"\", \"table_prefix = \\\"gorse_\\\"\", -1)\n\ttext = strings.Replace(text, \"cache_table_prefix = \\\"gorse_\\\"\", \"cache_table_prefix = \\\"gorse_cache_\\\"\", -1)\n\ttext = strings.Replace(text, \"data_table_prefix = \\\"gorse_\\\"\", \"data_table_prefix = \\\"gorse_data_\\\"\", -1)\n\ttext = strings.Replace(text, \"http_cors_domains = []\", \"http_cors_domains = [\\\".*\\\"]\", -1)\n\ttext = strings.Replace(text, \"http_cors_methods = []\", \"http_cors_methods = [\\\"GET\\\",\\\"PATCH\\\",\\\"POST\\\"]\", -1)\n\ttext = strings.Replace(text, \"issuer = \\\"\\\"\", \"issuer = \\\"https://accounts.google.com\\\"\", -1)\n\ttext = strings.Replace(text, \"client_id = \\\"\\\"\", \"client_id = \\\"client_id\\\"\", -1)\n\ttext = strings.Replace(text, \"client_secret = \\\"\\\"\", \"client_secret = \\\"client_secret\\\"\", -1)\n\ttext = strings.Replace(text, \"redirect_url = \\\"\\\"\", \"redirect_url = \\\"http://localhost:8088/callback/oauth2\\\"\", -1)\n\ttext = strings.Replace(text, \"auth_token = \\\"\\\"\", \"auth_token = \\\"<reranker_auth_token>\\\"\", -1)\n\ttext = strings.Replace(text, \"url = \\\"https://dashscope.aliyuncs.com/compatible-api/v1/reranks\\\"\", \"url = \\\"<reranker_url>\\\"\", -1)\n\tr, err := convert.TOML{}.Decode(bytes.NewBufferString(text))\n\tassert.NoError(t, err)\n\n\tencodings := []convert.Encoding{convert.TOML{}, convert.YAML{}, convert.JSON{}}\n\tfor _, encoding := range encodings {\n\t\tt.Run(encoding.String(), func(t *testing.T) {\n\t\t\tfilePath := filepath.Join(os.TempDir(), fmt.Sprintf(\"config.%s\", strings.ToLower(encoding.String())))\n\t\t\tfp, err := os.Create(filePath)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = encoding.Encode(fp, r)\n\t\t\tassert.NoError(t, err)\n\n\t\t\tconfig, err := LoadConfig(filePath)\n\t\t\tassert.NoError(t, err)\n\t\t\t// [database]\n\t\t\tassert.Equal(t, \"redis://localhost:6379/0\", config.Database.CacheStore)\n\t\t\tassert.Equal(t, \"mysql://gorse:gorse_pass@tcp(localhost:3306)/gorse\", config.Database.DataStore)\n\t\t\tassert.Equal(t, \"gorse_\", config.Database.TablePrefix)\n\t\t\tassert.Equal(t, \"gorse_cache_\", config.Database.CacheTablePrefix)\n\t\t\tassert.Equal(t, \"gorse_data_\", config.Database.DataTablePrefix)\n\t\t\tassert.Equal(t, \"READ-UNCOMMITTED\", config.Database.MySQL.IsolationLevel)\n\t\t\tassert.Equal(t, 0, config.Database.MySQL.MaxOpenConns)\n\t\t\tassert.Equal(t, 0, config.Database.MySQL.MaxIdleConns)\n\t\t\tassert.Equal(t, time.Duration(0), config.Database.MySQL.ConnMaxLifetime)\n\t\t\tassert.Equal(t, 64, config.Database.Postgres.MaxOpenConns)\n\t\t\tassert.Equal(t, 64, config.Database.Postgres.MaxIdleConns)\n\t\t\tassert.Equal(t, time.Minute, config.Database.Postgres.ConnMaxLifetime)\n\t\t\tassert.Equal(t, 10000, config.Database.Redis.MaxSearchResults)\n\t\t\t// [master]\n\t\t\tassert.Equal(t, 8086, config.Master.Port)\n\t\t\tassert.Equal(t, \"0.0.0.0\", config.Master.Host)\n\t\t\tassert.Equal(t, true, config.Master.SSLMode)\n\t\t\tassert.Equal(t, \"ca.pem\", config.Master.SSLCA)\n\t\t\tassert.Equal(t, \"cert.pem\", config.Master.SSLCert)\n\t\t\tassert.Equal(t, \"key.pem\", config.Master.SSLKey)\n\t\t\tassert.Equal(t, 8088, config.Master.HttpPort)\n\t\t\tassert.Equal(t, \"0.0.0.0\", config.Master.HttpHost)\n\t\t\tassert.Equal(t, []string{\".*\"}, config.Master.HttpCorsDomains)\n\t\t\tassert.Equal(t, []string{\"GET\", \"PATCH\", \"POST\"}, config.Master.HttpCorsMethods)\n\t\t\tassert.Equal(t, 1, config.Master.NumJobs)\n\t\t\tassert.Equal(t, 10*time.Second, config.Master.MetaTimeout)\n\t\t\tassert.Equal(t, \"admin\", config.Master.DashboardUserName)\n\t\t\tassert.Equal(t, \"password\", config.Master.DashboardPassword)\n\t\t\tassert.Equal(t, \"super_api_key\", config.Master.AdminAPIKey)\n\t\t\t// [server]\n\t\t\tassert.Equal(t, 10, config.Server.DefaultN)\n\t\t\tassert.Equal(t, \"19260817\", config.Server.APIKey)\n\t\t\tassert.Equal(t, 5*time.Second, config.Server.ClockError)\n\t\t\tassert.True(t, config.Server.AutoInsertUser)\n\t\t\tassert.True(t, config.Server.AutoInsertItem)\n\t\t\tassert.Equal(t, 10*time.Second, config.Server.CacheExpire)\n\t\t\t// [recommend]\n\t\t\tassert.Equal(t, 100, config.Recommend.CacheSize)\n\t\t\tassert.Equal(t, 72*time.Hour, config.Recommend.CacheExpire)\n\t\t\tassert.Equal(t, 100, config.Recommend.ContextSize)\n\t\t\t// [recommend.data_source]\n\t\t\tassert.Equal(t, []expression.FeedbackTypeExpression{\n\t\t\t\texpression.MustParseFeedbackTypeExpression(\"star\"),\n\t\t\t\texpression.MustParseFeedbackTypeExpression(\"like\"),\n\t\t\t\texpression.MustParseFeedbackTypeExpression(\"read>=3\"),\n\t\t\t}, config.Recommend.DataSource.PositiveFeedbackTypes)\n\t\t\tassert.Equal(t, []expression.FeedbackTypeExpression{\n\t\t\t\texpression.MustParseFeedbackTypeExpression(\"read\"),\n\t\t\t}, config.Recommend.DataSource.ReadFeedbackTypes)\n\t\t\tassert.Equal(t, uint(0), config.Recommend.DataSource.PositiveFeedbackTTL)\n\t\t\tassert.Equal(t, uint(0), config.Recommend.DataSource.ItemTTL)\n\t\t\t// [recommend.non-personalized]\n\t\t\tassert.Len(t, config.Recommend.NonPersonalized, 1)\n\t\t\tassert.Equal(t, \"most_starred_weekly\", config.Recommend.NonPersonalized[0].Name)\n\t\t\tassert.Equal(t, \"count(feedback, .FeedbackType == 'star')\", config.Recommend.NonPersonalized[0].Score)\n\t\t\tassert.Equal(t, \"(now() - item.Timestamp).Hours() < 168\", config.Recommend.NonPersonalized[0].Filter)\n\t\t\t// [recommend.collaborative]\n\t\t\tassert.Equal(t, \"mf\", config.Recommend.Collaborative.Type)\n\t\t\tassert.Equal(t, 60*time.Minute, config.Recommend.Collaborative.FitPeriod)\n\t\t\tassert.Equal(t, 100, config.Recommend.Collaborative.FitEpoch)\n\t\t\tassert.Equal(t, 360*time.Minute, config.Recommend.Collaborative.OptimizePeriod)\n\t\t\tassert.Equal(t, 10, config.Recommend.Collaborative.OptimizeTrials)\n\t\t\t// [recommend.replacement]\n\t\t\tassert.False(t, config.Recommend.Replacement.EnableReplacement)\n\t\t\tassert.Equal(t, 0.8, config.Recommend.Replacement.PositiveReplacementDecay)\n\t\t\tassert.Equal(t, 0.6, config.Recommend.Replacement.ReadReplacementDecay)\n\t\t\t// [recommend.ranker]\n\t\t\tassert.Equal(t, \"fm\", config.Recommend.Ranker.Type)\n\t\t\tassert.Equal(t, 120*time.Hour, config.Recommend.Ranker.CacheExpire)\n\t\t\tassert.Equal(t, []string{\"latest\", \"collaborative\", \"non-personalized/most_starred_weekly\", \"item-to-item/neighbors\", \"user-to-user/neighbors\"}, config.Recommend.Ranker.Recommenders)\n\t\t\tassert.Equal(t, 60*time.Minute, config.Recommend.Ranker.FitPeriod)\n\t\t\tassert.Equal(t, 100, config.Recommend.Ranker.FitEpoch)\n\t\t\tassert.Equal(t, 360*time.Minute, config.Recommend.Ranker.OptimizePeriod)\n\t\t\tassert.Equal(t, 10, config.Recommend.Ranker.OptimizeTrials)\n\t\t\tassert.Equal(t, \"<reranker_auth_token>\", config.Recommend.Ranker.RerankerAPI.AuthToken)\n\t\t\tassert.Equal(t, \"qwen3-rerank\", config.Recommend.Ranker.RerankerAPI.Model)\n\t\t\tassert.Equal(t, \"<reranker_url>\", config.Recommend.Ranker.RerankerAPI.URL)\n\t\t\t// [recommend.fallback]\n\t\t\tassert.Equal(t, []string{\"item-to-item/neighbors\", \"latest\"}, config.Recommend.Fallback.Recommenders)\n\t\t\t// [tracing]\n\t\t\tassert.False(t, config.Tracing.EnableTracing)\n\t\t\tassert.Equal(t, \"otlp\", config.Tracing.Exporter)\n\t\t\tassert.Equal(t, \"http://localhost:4317\", config.Tracing.CollectorEndpoint)\n\t\t\tassert.Equal(t, \"always\", config.Tracing.Sampler)\n\t\t\tassert.Equal(t, 1.0, config.Tracing.Ratio)\n\t\t\t// [oauth2]\n\t\t\tassert.Equal(t, \"https://accounts.google.com\", config.OIDC.Issuer)\n\t\t\tassert.Equal(t, \"client_id\", config.OIDC.ClientID)\n\t\t\tassert.Equal(t, \"client_secret\", config.OIDC.ClientSecret)\n\t\t\tassert.Equal(t, \"http://localhost:8088/callback/oauth2\", config.OIDC.RedirectURL)\n\t\t\t// [openai]\n\t\t\tassert.Equal(t, \"http://localhost:11434/v1\", config.OpenAI.BaseURL)\n\t\t\tassert.Equal(t, \"ollama\", config.OpenAI.AuthToken)\n\t\t\tassert.Equal(t, \"qwen2.5\", config.OpenAI.ChatCompletionModel)\n\t\t\tassert.Equal(t, 15000, config.OpenAI.ChatCompletionRPM)\n\t\t\tassert.Equal(t, 1200000, config.OpenAI.ChatCompletionTPM)\n\t\t\tassert.Equal(t, \"mxbai-embed-large\", config.OpenAI.EmbeddingModel)\n\t\t\tassert.Equal(t, 1024, config.OpenAI.EmbeddingDimensions)\n\t\t\tassert.Equal(t, 1800, config.OpenAI.EmbeddingRPM)\n\t\t\tassert.Equal(t, 1200000, config.OpenAI.EmbeddingTPM)\n\t\t})\n\t}\n}\n\nfunc TestSetDefault(t *testing.T) {\n\tfor _, binding := range bindings {\n\t\tt.Setenv(binding.env, \"\")\n\t}\n\tsetDefault()\n\tviper.SetConfigType(\"toml\")\n\terr := viper.ReadConfig(strings.NewReader(\"\"))\n\tassert.NoError(t, err)\n\tvar config Config\n\terr = viper.Unmarshal(&config)\n\tassert.NoError(t, err)\n\tassert.Equal(t, GetDefaultConfig(), &config)\n}\n\ntype environmentVariable struct {\n\tkey   string\n\tvalue string\n}\n\nfunc TestBindEnv(t *testing.T) {\n\tvariables := []environmentVariable{\n\t\t{\"GORSE_CACHE_STORE\", \"redis://<cache_store>\"},\n\t\t{\"GORSE_DATA_STORE\", \"mysql://<data_store>\"},\n\t\t{\"GORSE_TABLE_PREFIX\", \"gorse_\"},\n\t\t{\"GORSE_DATA_TABLE_PREFIX\", \"gorse_data_\"},\n\t\t{\"GORSE_CACHE_TABLE_PREFIX\", \"gorse_cache_\"},\n\t\t{\"GORSE_MASTER_PORT\", \"123\"},\n\t\t{\"GORSE_MASTER_HOST\", \"<master_host>\"},\n\t\t{\"GORSE_MASTER_SSL_MODE\", \"true\"},\n\t\t{\"GORSE_MASTER_SSL_CA\", \"ca.pem\"},\n\t\t{\"GORSE_MASTER_SSL_CERT\", \"cert.pem\"},\n\t\t{\"GORSE_MASTER_SSL_KEY\", \"key.pem\"},\n\t\t{\"GORSE_MASTER_HTTP_PORT\", \"456\"},\n\t\t{\"GORSE_MASTER_HTTP_HOST\", \"<master_http_host>\"},\n\t\t{\"GORSE_MASTER_JOBS\", \"789\"},\n\t\t{\"GORSE_DASHBOARD_USER_NAME\", \"user_name\"},\n\t\t{\"GORSE_DASHBOARD_PASSWORD\", \"password\"},\n\t\t{\"GORSE_DASHBOARD_AUTH_SERVER\", \"http://127.0.0.1:8888\"},\n\t\t{\"GORSE_DASHBOARD_REDACTED\", \"true\"},\n\t\t{\"GORSE_ADMIN_API_KEY\", \"<admin_api_key>\"},\n\t\t{\"GORSE_SERVER_API_KEY\", \"<server_api_key>\"},\n\t\t{\"GORSE_OIDC_ENABLE\", \"true\"},\n\t\t{\"GORSE_OIDC_ISSUER\", \"https://accounts.google.com\"},\n\t\t{\"GORSE_OIDC_CLIENT_ID\", \"client_id\"},\n\t\t{\"GORSE_OIDC_CLIENT_SECRET\", \"client_secret\"},\n\t\t{\"GORSE_OIDC_REDIRECT_URL\", \"http://localhost:8088/callback/oauth2\"},\n\t\t{\"GORSE_BLOB_URI\", \"s3://<bucket>/path\"},\n\t\t{\"S3_ENDPOINT\", \"https://s3.example.com\"},\n\t\t{\"S3_ACCESS_KEY_ID\", \"<access_key_id>\"},\n\t\t{\"S3_SECRET_ACCESS_KEY\", \"<secret_access_key>\"},\n\t\t{\"GCS_CREDENTIALS_FILE\", \"/path/to/credentials.json\"},\n\t\t{\"AZURE_STORAGE_ENDPOINT\", \"https://<account>.blob.core.windows.net\"},\n\t\t{\"AZURE_STORAGE_ACCOUNT_NAME\", \"<account_name>\"},\n\t\t{\"AZURE_STORAGE_ACCOUNT_KEY\", \"<account_key>\"},\n\t\t{\"AZURE_STORAGE_CONNECTION_STRING\", \"DefaultEndpointsProtocol=https;AccountName=<account>;AccountKey=<key>\"},\n\t\t{\"OPENAI_BASE_URL\", \"https://api.openai.com/v1\"},\n\t\t{\"OPENAI_AUTH_TOKEN\", \"<auth_token>\"},\n\t\t{\"OPENAI_CHAT_COMPLETION_MODEL\", \"gpt-4\"},\n\t\t{\"RERANKER_AUTH_TOKEN\", \"<reranker_auth_token>\"},\n\t\t{\"RERANKER_URL\", \"<reranker_url>\"},\n\t\t{\"RERANKER_MODEL\", \"<reranker_model>\"},\n\t}\n\tfor _, variable := range variables {\n\t\tt.Setenv(variable.key, variable.value)\n\t}\n\n\tconfig, err := LoadConfig(\"config.toml\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"redis://<cache_store>\", config.Database.CacheStore)\n\tassert.Equal(t, \"mysql://<data_store>\", config.Database.DataStore)\n\tassert.Equal(t, \"gorse_\", config.Database.TablePrefix)\n\tassert.Equal(t, \"gorse_cache_\", config.Database.CacheTablePrefix)\n\tassert.Equal(t, \"gorse_data_\", config.Database.DataTablePrefix)\n\tassert.Equal(t, 123, config.Master.Port)\n\tassert.Equal(t, \"<master_host>\", config.Master.Host)\n\tassert.Equal(t, true, config.Master.SSLMode)\n\tassert.Equal(t, \"ca.pem\", config.Master.SSLCA)\n\tassert.Equal(t, \"cert.pem\", config.Master.SSLCert)\n\tassert.Equal(t, \"key.pem\", config.Master.SSLKey)\n\tassert.Equal(t, 456, config.Master.HttpPort)\n\tassert.Equal(t, \"<master_http_host>\", config.Master.HttpHost)\n\tassert.Equal(t, 789, config.Master.NumJobs)\n\tassert.Equal(t, \"user_name\", config.Master.DashboardUserName)\n\tassert.Equal(t, \"password\", config.Master.DashboardPassword)\n\tassert.Equal(t, true, config.Master.DashboardRedacted)\n\tassert.Equal(t, \"<admin_api_key>\", config.Master.AdminAPIKey)\n\tassert.Equal(t, \"<server_api_key>\", config.Server.APIKey)\n\tassert.Equal(t, true, config.OIDC.Enable)\n\tassert.Equal(t, \"https://accounts.google.com\", config.OIDC.Issuer)\n\tassert.Equal(t, \"client_id\", config.OIDC.ClientID)\n\tassert.Equal(t, \"client_secret\", config.OIDC.ClientSecret)\n\tassert.Equal(t, \"http://localhost:8088/callback/oauth2\", config.OIDC.RedirectURL)\n\tassert.Equal(t, \"s3://<bucket>/path\", config.Blob.URI)\n\tassert.Equal(t, \"https://s3.example.com\", config.Blob.S3.Endpoint)\n\tassert.Equal(t, \"<access_key_id>\", config.Blob.S3.AccessKeyID)\n\tassert.Equal(t, \"<secret_access_key>\", config.Blob.S3.SecretAccessKey)\n\tassert.Equal(t, \"/path/to/credentials.json\", config.Blob.GCS.CredentialsFile)\n\tassert.Equal(t, \"https://<account>.blob.core.windows.net\", config.Blob.Azure.Endpoint)\n\tassert.Equal(t, \"<account_name>\", config.Blob.Azure.AccountName)\n\tassert.Equal(t, \"<account_key>\", config.Blob.Azure.AccountKey)\n\tassert.Equal(t, \"DefaultEndpointsProtocol=https;AccountName=<account>;AccountKey=<key>\", config.Blob.Azure.ConnectionString)\n\tassert.Equal(t, \"https://api.openai.com/v1\", config.OpenAI.BaseURL)\n\tassert.Equal(t, \"<auth_token>\", config.OpenAI.AuthToken)\n\tassert.Equal(t, \"gpt-4\", config.OpenAI.ChatCompletionModel)\n\tassert.Equal(t, \"<reranker_auth_token>\", config.Recommend.Ranker.RerankerAPI.AuthToken)\n\tassert.Equal(t, \"<reranker_url>\", config.Recommend.Ranker.RerankerAPI.URL)\n\tassert.Equal(t, \"<reranker_model>\", config.Recommend.Ranker.RerankerAPI.Model)\n\n\t// check default values\n\tassert.Equal(t, 100, config.Recommend.CacheSize)\n}\n\nfunc TestTablePrefixCompat(t *testing.T) {\n\tdata, err := os.ReadFile(\"config.toml\")\n\tassert.NoError(t, err)\n\ttext := string(data)\n\ttext = strings.Replace(text, \"cache_table_prefix = \\\"\\\"\", \"\", -1)\n\ttext = strings.Replace(text, \"data_table_prefix = \\\"\\\"\", \"\", -1)\n\ttext = strings.Replace(text, \"table_prefix = \\\"\\\"\", \"table_prefix = \\\"gorse_\\\"\", -1)\n\tpath := filepath.Join(t.TempDir(), \"config.toml\")\n\terr = os.WriteFile(path, []byte(text), os.ModePerm)\n\tassert.NoError(t, err)\n\n\tconfig, err := LoadConfig(path)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"gorse_\", config.Database.TablePrefix)\n\tassert.Equal(t, \"gorse_\", config.Database.CacheTablePrefix)\n\tassert.Equal(t, \"gorse_\", config.Database.DataTablePrefix)\n}\n\nfunc TestNonPersonalizedConfig(t *testing.T) {\n\ta := NonPersonalizedConfig{}\n\tb := NonPersonalizedConfig{}\n\tassert.Equal(t, a.Hash(), b.Hash())\n\n\ta = NonPersonalizedConfig{Name: \"a\"}\n\tb = NonPersonalizedConfig{Name: \"b\"}\n\tassert.NotEqual(t, a.Hash(), b.Hash())\n\tassert.Equal(t, \"non-personalized/a\", a.FullName())\n\tassert.Equal(t, \"non-personalized/b\", b.FullName())\n\n\ta = NonPersonalizedConfig{Score: \"a\"}\n\tb = NonPersonalizedConfig{Score: \"b\"}\n\tassert.NotEqual(t, a.Hash(), b.Hash())\n\n\ta = NonPersonalizedConfig{Filter: \"a\"}\n\tb = NonPersonalizedConfig{Filter: \"b\"}\n\tassert.NotEqual(t, a.Hash(), b.Hash())\n}\n\nfunc TestItemToItemConfig(t *testing.T) {\n\ta := ItemToItemConfig{}\n\tb := ItemToItemConfig{}\n\tassert.Equal(t, a.Hash(nil), b.Hash(nil))\n\n\ta = ItemToItemConfig{Name: \"a\"}\n\tb = ItemToItemConfig{Name: \"b\"}\n\tassert.NotEqual(t, a.Hash(nil), b.Hash(nil))\n\tassert.Equal(t, \"item-to-item/a\", a.FullName())\n\tassert.Equal(t, \"item-to-item/b\", b.FullName())\n\n\ta = ItemToItemConfig{Type: \"a\"}\n\tb = ItemToItemConfig{Type: \"b\"}\n\tassert.NotEqual(t, a.Hash(nil), b.Hash(nil))\n\n\ta = ItemToItemConfig{Column: \"a\"}\n\tb = ItemToItemConfig{Column: \"b\"}\n\tassert.NotEqual(t, a.Hash(nil), b.Hash(nil))\n\n\tc := ItemToItemConfig{Type: \"users\"}\n\td := RecommendConfig{}\n\te := RecommendConfig{}\n\tassert.Equal(t, c.Hash(&d), c.Hash(&e))\n\n\td.DataSource.PositiveFeedbackTypes = []expression.FeedbackTypeExpression{expression.MustParseFeedbackTypeExpression(\"like\")}\n\te.DataSource.PositiveFeedbackTypes = []expression.FeedbackTypeExpression{expression.MustParseFeedbackTypeExpression(\"star\")}\n\tassert.NotEqual(t, c.Hash(&d), c.Hash(&e))\n}\n\nfunc TestUserToUserConfig(t *testing.T) {\n\ta := UserToUserConfig{}\n\tb := UserToUserConfig{}\n\tassert.Equal(t, a.Hash(nil), b.Hash(nil))\n\n\ta = UserToUserConfig{Name: \"a\"}\n\tb = UserToUserConfig{Name: \"b\"}\n\tassert.NotEqual(t, a.Hash(nil), b.Hash(nil))\n\tassert.Equal(t, \"user-to-user/a\", a.FullName())\n\tassert.Equal(t, \"user-to-user/b\", b.FullName())\n\n\ta = UserToUserConfig{Type: \"a\"}\n\tb = UserToUserConfig{Type: \"b\"}\n\tassert.NotEqual(t, a.Hash(nil), b.Hash(nil))\n\n\ta = UserToUserConfig{Column: \"a\"}\n\tb = UserToUserConfig{Column: \"b\"}\n\tassert.NotEqual(t, a.Hash(nil), b.Hash(nil))\n\n\tc := UserToUserConfig{Type: \"items\"}\n\td := RecommendConfig{}\n\te := RecommendConfig{}\n\tassert.Equal(t, c.Hash(&d), c.Hash(&e))\n\n\td.DataSource.PositiveFeedbackTypes = []expression.FeedbackTypeExpression{expression.MustParseFeedbackTypeExpression(\"like\")}\n\te.DataSource.PositiveFeedbackTypes = []expression.FeedbackTypeExpression{expression.MustParseFeedbackTypeExpression(\"star\")}\n\tassert.NotEqual(t, c.Hash(&d), c.Hash(&e))\n}\n\nfunc TestCollaborativeConfig(t *testing.T) {\n\ta := RecommendConfig{}\n\tb := RecommendConfig{}\n\tc := CollaborativeConfig{}\n\tassert.Equal(t, c.Hash(&a), c.Hash(&b))\n\n\ta.DataSource.PositiveFeedbackTypes = []expression.FeedbackTypeExpression{expression.MustParseFeedbackTypeExpression(\"like\")}\n\tb.DataSource.PositiveFeedbackTypes = []expression.FeedbackTypeExpression{expression.MustParseFeedbackTypeExpression(\"star\")}\n\tassert.NotEqual(t, c.Hash(&a), c.Hash(&b))\n}\n\nfunc TestExternalConfig(t *testing.T) {\n\ta := ExternalConfig{}\n\tb := ExternalConfig{}\n\tassert.Equal(t, a.Hash(), b.Hash())\n\n\ta = ExternalConfig{Name: \"a\"}\n\tb = ExternalConfig{Name: \"b\"}\n\tassert.NotEqual(t, a.Hash(), b.Hash())\n\tassert.Equal(t, \"external/a\", a.FullName())\n\tassert.Equal(t, \"external/b\", b.FullName())\n\n\ta = ExternalConfig{Script: \"a\"}\n\tb = ExternalConfig{Script: \"b\"}\n\tassert.NotEqual(t, a.Hash(), b.Hash())\n}\n\nfunc TestRecommendConfig(t *testing.T) {\n\ta := RecommendConfig{}\n\tb := RecommendConfig{}\n\tassert.Equal(t, a.Hash(), b.Hash())\n\n\ta.NonPersonalized = []NonPersonalizedConfig{{Name: \"a\"}}\n\tb.NonPersonalized = []NonPersonalizedConfig{{Name: \"b\"}}\n\tassert.NotEqual(t, a.Hash(), b.Hash())\n\ta.NonPersonalized = []NonPersonalizedConfig{}\n\tb.NonPersonalized = []NonPersonalizedConfig{}\n\n\ta.ItemToItem = []ItemToItemConfig{{Name: \"a\"}}\n\tb.ItemToItem = []ItemToItemConfig{{Name: \"b\"}}\n\tassert.NotEqual(t, a.Hash(), b.Hash())\n\ta.ItemToItem = []ItemToItemConfig{}\n\tb.ItemToItem = []ItemToItemConfig{}\n\n\ta.UserToUser = []UserToUserConfig{{Name: \"a\"}}\n\tb.UserToUser = []UserToUserConfig{{Name: \"b\"}}\n\tassert.NotEqual(t, a.Hash(), b.Hash())\n\ta.UserToUser = []UserToUserConfig{}\n\tb.UserToUser = []UserToUserConfig{}\n\n\ta.External = []ExternalConfig{{Name: \"a\"}}\n\tb.External = []ExternalConfig{{Name: \"b\"}}\n\tassert.NotEqual(t, a.Hash(), b.Hash())\n\ta.External = []ExternalConfig{}\n\tb.External = []ExternalConfig{}\n\n\ta.DataSource.PositiveFeedbackTypes = []expression.FeedbackTypeExpression{expression.MustParseFeedbackTypeExpression(\"like\")}\n\tb.DataSource.PositiveFeedbackTypes = []expression.FeedbackTypeExpression{expression.MustParseFeedbackTypeExpression(\"star\")}\n\tassert.NotEqual(t, a.Hash(), b.Hash())\n\n\ta.Ranker.Recommenders = []string{\"latest\"}\n\tb.Ranker.Recommenders = []string{\"collaborative\"}\n\tassert.NotEqual(t, a.Hash(), b.Hash())\n\n\ta.UserToUser = []UserToUserConfig{{Name: \"a\"}, {Name: \"b\"}}\n\tb.UserToUser = []UserToUserConfig{{Name: \"b\"}, {Name: \"a\"}}\n\ta.Ranker.Recommenders = []string{\"user-to-user/a\", \"user-to-user/b\"}\n\tb.Ranker.Recommenders = []string{\"user-to-user/b\", \"user-to-user/a\"}\n\tassert.Equal(t, a.Hash(), b.Hash())\n}\n\ntype ValidateTestSuite struct {\n\tsuite.Suite\n\t*Config\n}\n\nfunc (s *ValidateTestSuite) SetupTest() {\n\ts.Config = GetDefaultConfig()\n\ts.Database.CacheStore = \"redis://localhost:6379/0\"\n\ts.Database.DataStore = \"mysql://gorse:gorse_pass@tcp(localhost:3306)/gorse\"\n}\n\nfunc (s *ValidateTestSuite) TestDuplicateNonPersonalized() {\n\ts.Recommend.NonPersonalized = []NonPersonalizedConfig{{\n\t\tName:  \"most_starred_weekly\",\n\t\tScore: \"count(feedback, .FeedbackType == 'star')\",\n\t}, {\n\t\tName:  \"most_starred_weekly\",\n\t\tScore: \"count(feedback, .FeedbackType == 'star')\",\n\t}}\n\ts.Error(s.Validate())\n}\n\nfunc (s *ValidateTestSuite) TestDuplicateItemToItem() {\n\ts.Recommend.ItemToItem = []ItemToItemConfig{{\n\t\tName: \"item_to_item\",\n\t\tType: \"users\",\n\t}, {\n\t\tName: \"item_to_item\",\n\t\tType: \"users\",\n\t}}\n\ts.Error(s.Validate())\n}\n\nfunc (s *ValidateTestSuite) TestRecommendersExistence() {\n\ts.Recommend.Ranker.Recommenders = []string{\"not_exist\"}\n\ts.Error(s.Validate())\n\n\ts.Recommend.Collaborative.Type = \"none\"\n\ts.Recommend.Ranker.Recommenders = []string{\"collaborative\"}\n\ts.Error(s.Validate())\n}\n\nfunc TestValidate(t *testing.T) {\n\tsuite.Run(t, new(ValidateTestSuite))\n}\n"
  },
  {
    "path": "dataset/dataset.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage dataset\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/chewxy/math32\"\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/gorse-io/gorse/common/util\"\n\t\"github.com/gorse-io/gorse/model\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/juju/errors\"\n\t\"github.com/samber/lo\"\n\t\"modernc.org/strutil\"\n)\n\ntype ID int32\n\n// CFSplit is the dataset split for collaborative filtering.\ntype CFSplit interface {\n\t// CountUsers returns the number of users.\n\tCountUsers() int\n\t// CountItems returns the number of items.\n\tCountItems() int\n\t// CountFeedback returns the number of (positive) feedback.\n\tCountFeedback() int\n\t// GetItems returns the items.\n\tGetItems() []data.Item\n\t// GetUserDict returns the frequency dictionary of users.\n\tGetUserDict() *FreqDict\n\t// GetItemDict returns the frequency dictionary of items.\n\tGetItemDict() *FreqDict\n\t// GetUserFeedback returns the (positive) feedback of users.\n\tGetUserFeedback() [][]int32\n\t// GetItemFeedback returns the (positive) feedback of items.\n\tGetItemFeedback() [][]int32\n\t// SampleUserNegatives samples negative (feedback) for users.\n\tSampleUserNegatives(excludeSet CFSplit, numCandidates int) [][]int32\n}\n\n// CTRSplit is the dataset split for click-through rate prediction.\ntype CTRSplit interface {\n\tCount() int\n\tCountUsers() int\n\tCountItems() int\n\tCountUserLabels() int\n\tCountItemLabels() int\n\tCountContextLabels() int\n\tCountPositive() int\n\tCountNegative() int\n\tGetIndex() UnifiedIndex\n\tGetTarget(i int) float32\n\tGet(i int) ([]int32, []float32, [][]float32, float32)\n\tGetItemEmbeddingDim() []int\n\tGetItemEmbeddingIndex() *Index\n}\n\ntype Dataset struct {\n\ttimestamp    time.Time\n\tusers        []data.User\n\titems        []data.Item\n\tuserLabels   *Labels\n\titemLabels   *Labels\n\tuserFeedback [][]int32\n\titemFeedback [][]int32\n\ttimestamps   [][]time.Time\n\tnegatives    [][]int32\n\tuserDict     *FreqDict\n\titemDict     *FreqDict\n\tnumFeedback  int\n\tcategories   map[string]int\n}\n\nfunc NewDataset(timestamp time.Time, userCount, itemCount int) *Dataset {\n\treturn &Dataset{\n\t\ttimestamp:    timestamp,\n\t\tusers:        make([]data.User, 0, userCount),\n\t\titems:        make([]data.Item, 0, itemCount),\n\t\tuserLabels:   NewLabels(),\n\t\titemLabels:   NewLabels(),\n\t\tuserFeedback: make([][]int32, userCount),\n\t\titemFeedback: make([][]int32, itemCount),\n\t\ttimestamps:   make([][]time.Time, userCount),\n\t\tuserDict:     NewFreqDict(),\n\t\titemDict:     NewFreqDict(),\n\t\tcategories:   make(map[string]int),\n\t}\n}\n\nfunc (d *Dataset) GetTimestamp() time.Time {\n\treturn d.timestamp\n}\n\nfunc (d *Dataset) CountFeedback() int {\n\treturn d.numFeedback\n}\n\nfunc (d *Dataset) GetUsers() []data.User {\n\treturn d.users\n}\n\nfunc (d *Dataset) GetUserDict() *FreqDict {\n\treturn d.userDict\n}\n\nfunc (d *Dataset) CountUsers() int {\n\treturn len(d.users)\n}\n\nfunc (d *Dataset) GetItems() []data.Item {\n\treturn d.items\n}\n\nfunc (d *Dataset) GetItemDict() *FreqDict {\n\treturn d.itemDict\n}\n\nfunc (d *Dataset) CountItems() int {\n\treturn len(d.items)\n}\n\nfunc (d *Dataset) GetUserFeedback() [][]int32 {\n\treturn d.userFeedback\n}\n\nfunc (d *Dataset) GetItemFeedback() [][]int32 {\n\treturn d.itemFeedback\n}\n\nfunc (d *Dataset) GetCategories() map[string]int {\n\treturn d.categories\n}\n\n// GetUserIDF returns the IDF of users.\n//\n//\tIDF(u) = log(I/freq(u))\n//\n// I is the number of items.\n// freq(u) is the frequency of user u in all feedback.\nfunc (d *Dataset) GetUserIDF() []float32 {\n\tidf := make([]float32, d.userDict.Count())\n\tfor i := int32(0); i < d.userDict.Count(); i++ {\n\t\t// Since zero IDF will cause NaN in the future, we set the minimum value to 1e-3.\n\t\tidf[i] = max(math32.Log(float32(len(d.items))/float32(d.userDict.Freq(i))), 1e-3)\n\t}\n\treturn idf\n}\n\n// GetItemIDF returns the IDF of items.\n//\n//\tIDF(i) = log(U/freq(i))\n//\n// U is the number of users.\n// freq(i) is the frequency of item i in all feedback.\nfunc (d *Dataset) GetItemIDF() []float32 {\n\tidf := make([]float32, d.itemDict.Count())\n\tfor i := int32(0); i < d.itemDict.Count(); i++ {\n\t\t// Since zero IDF will cause NaN in the future, we set the minimum value to 1e-3.\n\t\tidf[i] = max(math32.Log(float32(len(d.users))/float32(d.itemDict.Freq(i))), 1e-3)\n\t}\n\treturn idf\n}\n\nfunc (d *Dataset) GetUserColumnValuesIDF() []float32 {\n\tidf := make([]float32, d.userLabels.values.Count())\n\tfor i := int32(0); i < d.userLabels.values.Count(); i++ {\n\t\t// Since zero IDF will cause NaN in the future, we set the minimum value to 1e-3.\n\t\tidf[i] = max(math32.Log(float32(len(d.users))/float32(d.userLabels.values.Freq(i))), 1e-3)\n\t}\n\treturn idf\n}\n\nfunc (d *Dataset) GetItemColumnValuesIDF() []float32 {\n\tidf := make([]float32, d.itemLabels.values.Count())\n\tfor i := int32(0); i < d.itemLabels.values.Count(); i++ {\n\t\t// Since zero IDF will cause NaN in the future, we set the minimum value to 1e-3.\n\t\tidf[i] = max(math32.Log(float32(len(d.items))/float32(d.itemLabels.values.Freq(i))), 1e-3)\n\t}\n\treturn idf\n}\n\nfunc (d *Dataset) AddUser(user data.User) {\n\td.users = append(d.users, data.User{\n\t\tUserId:  user.UserId,\n\t\tLabels:  d.userLabels.processLabels(user.Labels, \"\"),\n\t\tComment: user.Comment,\n\t})\n\td.userDict.AddNoCount(user.UserId)\n\tif len(d.userFeedback) < len(d.users) {\n\t\td.userFeedback = append(d.userFeedback, nil)\n\t}\n\tif len(d.timestamps) < len(d.users) {\n\t\td.timestamps = append(d.timestamps, nil)\n\t}\n}\n\nfunc (d *Dataset) AddItem(item data.Item) {\n\td.items = append(d.items, data.Item{\n\t\tItemId:     item.ItemId,\n\t\tIsHidden:   item.IsHidden,\n\t\tCategories: item.Categories,\n\t\tTimestamp:  item.Timestamp,\n\t\tLabels:     d.itemLabels.processLabels(item.Labels, \"\"),\n\t\tComment:    item.Comment,\n\t})\n\td.itemDict.AddNoCount(item.ItemId)\n\tif len(d.itemFeedback) < len(d.items) {\n\t\td.itemFeedback = append(d.itemFeedback, nil)\n\t}\n\tfor _, category := range item.Categories {\n\t\td.categories[category]++\n\t}\n}\n\nfunc (d *Dataset) AddFeedback(userId, itemId string, timestamp time.Time) {\n\tuserIndex := d.userDict.Add(userId)\n\titemIndex := d.itemDict.Add(itemId)\n\td.userFeedback[userIndex] = append(d.userFeedback[userIndex], itemIndex)\n\td.itemFeedback[itemIndex] = append(d.itemFeedback[itemIndex], userIndex)\n\td.timestamps[userIndex] = append(d.timestamps[userIndex], timestamp)\n\td.numFeedback++\n}\n\nfunc (d *Dataset) SampleUserNegatives(excludeSet CFSplit, numCandidates int) [][]int32 {\n\tif len(d.negatives) == 0 {\n\t\trng := util.NewRandomGenerator(0)\n\t\td.negatives = make([][]int32, d.CountUsers())\n\t\tfor userIndex := 0; userIndex < d.CountUsers(); userIndex++ {\n\t\t\ts1 := mapset.NewSet(d.GetUserFeedback()[userIndex]...)\n\t\t\ts2 := mapset.NewSet(excludeSet.GetUserFeedback()[userIndex]...)\n\t\t\td.negatives[userIndex] = rng.SampleInt32(0, int32(d.CountItems()), numCandidates, s1, s2)\n\t\t}\n\t}\n\treturn d.negatives\n}\n\n// SplitCF splits dataset by user-leave-one-out method. The argument `numTestUsers` determines the number of users in the test\n// set. If numTestUsers is equal or greater than the number of total users or numTestUsers <= 0, all users are presented\n// in the test set.\nfunc (d *Dataset) SplitCF(numTestUsers int, seed int64) (CFSplit, CFSplit) {\n\ttrainSet, testSet := new(Dataset), new(Dataset)\n\ttrainSet.users, testSet.users = d.users, d.users\n\ttrainSet.items, testSet.items = d.items, d.items\n\ttrainSet.userFeedback, testSet.userFeedback = make([][]int32, d.CountUsers()), make([][]int32, d.CountUsers())\n\ttrainSet.itemFeedback, testSet.itemFeedback = make([][]int32, d.CountItems()), make([][]int32, d.CountItems())\n\ttrainSet.timestamps, testSet.timestamps = make([][]time.Time, d.CountUsers()), make([][]time.Time, d.CountUsers())\n\ttrainSet.userDict, testSet.userDict = d.userDict, d.userDict\n\ttrainSet.itemDict, testSet.itemDict = d.itemDict, d.itemDict\n\trng := util.NewRandomGenerator(seed)\n\tif numTestUsers >= d.CountUsers() || numTestUsers <= 0 {\n\t\tfor userIndex := int32(0); userIndex < int32(d.CountUsers()); userIndex++ {\n\t\t\tif len(d.userFeedback[userIndex]) > 0 {\n\t\t\t\tk := rng.Intn(len(d.userFeedback[userIndex]))\n\t\t\t\ttestSet.userFeedback[userIndex] = append(testSet.userFeedback[userIndex], d.userFeedback[userIndex][k])\n\t\t\t\ttestSet.itemFeedback[d.userFeedback[userIndex][k]] = append(testSet.itemFeedback[d.userFeedback[userIndex][k]], userIndex)\n\t\t\t\ttestSet.timestamps[userIndex] = append(testSet.timestamps[userIndex], d.timestamps[userIndex][k])\n\t\t\t\ttestSet.numFeedback++\n\t\t\t\tfor i, itemIndex := range d.userFeedback[userIndex] {\n\t\t\t\t\tif i != k {\n\t\t\t\t\t\ttrainSet.userFeedback[userIndex] = append(trainSet.userFeedback[userIndex], itemIndex)\n\t\t\t\t\t\ttrainSet.itemFeedback[itemIndex] = append(trainSet.itemFeedback[itemIndex], userIndex)\n\t\t\t\t\t\ttrainSet.timestamps[userIndex] = append(trainSet.timestamps[userIndex], d.timestamps[userIndex][i])\n\t\t\t\t\t\ttrainSet.numFeedback++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\ttestUsers := rng.SampleInt32(0, int32(d.CountUsers()), numTestUsers)\n\t\tfor _, userIndex := range testUsers {\n\t\t\tif len(d.userFeedback[userIndex]) > 0 {\n\t\t\t\tk := rng.Intn(len(d.userFeedback[userIndex]))\n\t\t\t\ttestSet.userFeedback[userIndex] = append(testSet.userFeedback[userIndex], d.userFeedback[userIndex][k])\n\t\t\t\ttestSet.itemFeedback[d.userFeedback[userIndex][k]] = append(testSet.itemFeedback[d.userFeedback[userIndex][k]], userIndex)\n\t\t\t\ttestSet.timestamps[userIndex] = append(testSet.timestamps[userIndex], d.timestamps[userIndex][k])\n\t\t\t\ttestSet.numFeedback++\n\t\t\t\tfor i, itemIndex := range d.userFeedback[userIndex] {\n\t\t\t\t\tif i != k {\n\t\t\t\t\t\ttrainSet.userFeedback[userIndex] = append(trainSet.userFeedback[userIndex], itemIndex)\n\t\t\t\t\t\ttrainSet.itemFeedback[itemIndex] = append(trainSet.itemFeedback[itemIndex], userIndex)\n\t\t\t\t\t\ttrainSet.timestamps[userIndex] = append(trainSet.timestamps[userIndex], d.timestamps[userIndex][i])\n\t\t\t\t\t\ttrainSet.numFeedback++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ttestUserSet := mapset.NewSet(testUsers...)\n\t\tfor userIndex := int32(0); userIndex < int32(d.CountUsers()); userIndex++ {\n\t\t\tif !testUserSet.Contains(userIndex) {\n\t\t\t\tfor idx, itemIndex := range d.userFeedback[userIndex] {\n\t\t\t\t\ttrainSet.userFeedback[userIndex] = append(trainSet.userFeedback[userIndex], itemIndex)\n\t\t\t\t\ttrainSet.itemFeedback[itemIndex] = append(trainSet.itemFeedback[itemIndex], userIndex)\n\t\t\t\t\ttrainSet.timestamps[userIndex] = append(trainSet.timestamps[userIndex], d.timestamps[userIndex][idx])\n\t\t\t\t\ttrainSet.numFeedback++\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn trainSet, testSet\n}\n\n// SplitLatest splits dataset by moving the most recent feedback of all users into the test set to avoid leakage.\nfunc (d *Dataset) SplitLatest(shots int) (CFSplit, CFSplit) {\n\ttrainSet, testSet := new(Dataset), new(Dataset)\n\ttrainSet.users, testSet.users = d.users, d.users\n\ttrainSet.items, testSet.items = d.items, d.items\n\ttrainSet.userFeedback, testSet.userFeedback = make([][]int32, d.CountUsers()), make([][]int32, d.CountUsers())\n\ttrainSet.itemFeedback, testSet.itemFeedback = make([][]int32, d.CountItems()), make([][]int32, d.CountItems())\n\ttrainSet.timestamps, testSet.timestamps = make([][]time.Time, d.CountUsers()), make([][]time.Time, d.CountUsers())\n\ttrainSet.userDict, testSet.userDict = d.userDict, d.userDict\n\ttrainSet.itemDict, testSet.itemDict = d.itemDict, d.itemDict\n\tfor userIndex := int32(0); userIndex < int32(d.CountUsers()); userIndex++ {\n\t\tif len(d.userFeedback[userIndex]) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tidxs := lo.Range(len(d.userFeedback[userIndex]))\n\t\tsort.Slice(idxs, func(i, j int) bool {\n\t\t\treturn d.timestamps[userIndex][idxs[i]].After(d.timestamps[userIndex][idxs[j]])\n\t\t})\n\t\ttestSet.timestamps[userIndex] = append(testSet.timestamps[userIndex], d.timestamps[userIndex][idxs[0]])\n\t\ttestSet.itemFeedback[d.userFeedback[userIndex][idxs[0]]] = append(testSet.itemFeedback[d.userFeedback[userIndex][idxs[0]]], userIndex)\n\t\ttestSet.userFeedback[userIndex] = append(testSet.userFeedback[userIndex], d.userFeedback[userIndex][idxs[0]])\n\t\ttestSet.numFeedback++\n\t\tfor i := 1; i < len(d.userFeedback[userIndex]) && i <= shots; i++ {\n\t\t\titemIndex := d.userFeedback[userIndex][idxs[i]]\n\t\t\ttrainSet.userFeedback[userIndex] = append(trainSet.userFeedback[userIndex], itemIndex)\n\t\t\ttrainSet.itemFeedback[itemIndex] = append(trainSet.itemFeedback[itemIndex], userIndex)\n\t\t\ttrainSet.timestamps[userIndex] = append(trainSet.timestamps[userIndex], d.timestamps[userIndex][idxs[i]])\n\t\t\ttrainSet.numFeedback++\n\t\t}\n\t}\n\treturn trainSet, testSet\n}\n\ntype Labels struct {\n\tfields *strutil.Pool\n\tvalues *FreqDict\n}\n\nfunc NewLabels() *Labels {\n\treturn &Labels{\n\t\tfields: strutil.NewPool(),\n\t\tvalues: NewFreqDict(),\n\t}\n}\n\nfunc (l *Labels) processLabels(labels any, parent string) any {\n\tswitch typed := labels.(type) {\n\tcase map[string]any:\n\t\to := make(map[string]any)\n\t\tfor k, v := range typed {\n\t\t\to[l.fields.Align(k)] = l.processLabels(v, parent+\".\"+k)\n\t\t}\n\t\treturn o\n\tcase []any:\n\t\tif isSliceOf[float64](typed) {\n\t\t\treturn lo.Map(typed, func(e any, _ int) float32 {\n\t\t\t\treturn float32(e.(float64))\n\t\t\t})\n\t\t} else if isSliceOf[string](typed) {\n\t\t\treturn lo.Map(typed, func(e any, _ int) ID {\n\t\t\t\treturn ID(l.values.Add(parent + \":\" + e.(string)))\n\t\t\t})\n\t\t}\n\t\treturn typed\n\tcase string:\n\t\treturn ID(l.values.Add(parent + \":\" + typed))\n\tdefault:\n\t\treturn labels\n\t}\n}\n\nfunc isSliceOf[T any](v []any) bool {\n\tfor _, e := range v {\n\t\tif _, ok := e.(T); !ok {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc LoadDataFromBuiltIn(dataSetName string) (*Dataset, *Dataset, error) {\n\t// Extract Data set information\n\ttrainFilePath, testFilePath, err := model.LocateBuiltInDataset(dataSetName, model.FormatNCF)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\t// Load dataset\n\ttrain, err := loadTrain(trainFilePath)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\ttest := NewDataset(train.GetTimestamp(), 0, 0)\n\ttest.users, test.items = train.users, train.items\n\ttest.userDict, test.itemDict = train.userDict, train.itemDict\n\ttest.userFeedback = make([][]int32, len(train.userFeedback))\n\ttest.itemFeedback = make([][]int32, len(train.itemFeedback))\n\ttest.timestamps = make([][]time.Time, len(train.userFeedback))\n\ttest.negatives = make([][]int32, len(train.userFeedback))\n\terr = loadTest(test, testFilePath)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\treturn train, test, nil\n}\n\nfunc loadTrain(path string) (*Dataset, error) {\n\tdataset := NewDataset(time.Now(), 0, 0)\n\t// Open\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer file.Close()\n\t// Read lines\n\tscanner := bufio.NewScanner(file)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tfields := strings.Split(line, \"\\t\")\n\t\t// add users\n\t\tuserId, err := util.ParseInt[int32](fields[0])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor i := dataset.userDict.Count(); i <= userId; i++ {\n\t\t\tdataset.AddUser(data.User{UserId: util.FormatInt(i)})\n\t\t}\n\t\t// add items\n\t\titemId, err := util.ParseInt[int32](fields[1])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor i := dataset.itemDict.Count(); i <= itemId; i++ {\n\t\t\tdataset.AddItem(data.Item{ItemId: util.FormatInt(i)})\n\t\t}\n\t\t// add feedback\n\t\tdataset.AddFeedback(fields[0], fields[1], time.Time{})\n\t}\n\treturn dataset, scanner.Err()\n}\n\nfunc loadTest(dataset *Dataset, path string) error {\n\t// Open\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tdefer file.Close()\n\t// Read lines\n\tscanner := bufio.NewScanner(file)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\t// parse line\n\t\tfields := strings.Split(line, \"\\t\")\n\t\tpositive, negatives := fields[0], fields[1:]\n\t\tif positive[0] != '(' || positive[len(positive)-1] != ')' {\n\t\t\treturn fmt.Errorf(\"wrong foramt: %v\", line)\n\t\t}\n\t\tpositive = positive[1 : len(positive)-1]\n\t\tfields = strings.Split(positive, \",\")\n\t\t// add feedback\n\t\tdataset.AddFeedback(fields[0], fields[1], time.Time{})\n\t\t// add negatives\n\t\tuserId, err := strconv.Atoi(fields[0])\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdataset.negatives[userId] = make([]int32, len(negatives))\n\t\tfor i, negative := range negatives {\n\t\t\tdataset.negatives[userId][i] = dataset.itemDict.Add(negative)\n\t\t}\n\t}\n\treturn scanner.Err()\n}\n"
  },
  {
    "path": "dataset/dataset_test.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage dataset\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/chewxy/math32\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestDataset_AddItem(t *testing.T) {\n\tdataSet := NewDataset(time.Now(), 0, 1)\n\tdataSet.AddItem(data.Item{\n\t\tItemId:     \"1\",\n\t\tIsHidden:   false,\n\t\tCategories: []string{\"a\", \"b\"},\n\t\tTimestamp:  time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\tLabels: map[string]any{\n\t\t\t\"a\":        1,\n\t\t\t\"embedded\": []any{1.1, 2.2, 3.3},\n\t\t\t\"tags\":     []any{\"a\", \"b\", \"c\"},\n\t\t},\n\t\tComment: \"comment\",\n\t})\n\tdataSet.AddItem(data.Item{\n\t\tItemId:     \"2\",\n\t\tIsHidden:   true,\n\t\tCategories: []string{\"a\", \"b\"},\n\t\tTimestamp:  time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\tLabels: map[string]any{\n\t\t\t\"a\":        1,\n\t\t\t\"embedded\": []any{1.1, 2.2, 3.3},\n\t\t\t\"tags\":     []any{\"b\", \"c\", \"a\"},\n\t\t\t\"topics\":   []any{\"a\", \"b\", \"c\"},\n\t\t},\n\t\tComment: \"comment\",\n\t})\n\tassert.Len(t, dataSet.GetItems(), 2)\n\tassert.Equal(t, data.Item{\n\t\tItemId:     \"1\",\n\t\tIsHidden:   false,\n\t\tCategories: []string{\"a\", \"b\"},\n\t\tTimestamp:  time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\tLabels: map[string]any{\n\t\t\t\"a\":        1,\n\t\t\t\"embedded\": []float32{1.1, 2.2, 3.3},\n\t\t\t\"tags\":     []ID{0, 1, 2},\n\t\t},\n\t\tComment: \"comment\",\n\t}, dataSet.GetItems()[0])\n\tassert.Equal(t, data.Item{\n\t\tItemId:     \"2\",\n\t\tIsHidden:   true,\n\t\tCategories: []string{\"a\", \"b\"},\n\t\tTimestamp:  time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\tLabels: map[string]any{\n\t\t\t\"a\":        1,\n\t\t\t\"embedded\": []float32{1.1, 2.2, 3.3},\n\t\t\t\"tags\":     []ID{1, 2, 0},\n\t\t\t\"topics\":   []ID{3, 4, 5},\n\t\t},\n\t\tComment: \"comment\",\n\t}, dataSet.GetItems()[1])\n}\n\nfunc TestDataset_GetItemColumnValuesIDF(t *testing.T) {\n\tdataSet := NewDataset(time.Now(), 0, 1)\n\tdataSet.AddItem(data.Item{\n\t\tItemId:     \"1\",\n\t\tIsHidden:   false,\n\t\tCategories: []string{\"a\", \"b\"},\n\t\tTimestamp:  time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\tLabels: map[string]any{\n\t\t\t\"tags\": []any{\"a\", \"b\", \"c\"},\n\t\t},\n\t\tComment: \"comment\",\n\t})\n\tdataSet.AddItem(data.Item{\n\t\tItemId:     \"2\",\n\t\tIsHidden:   false,\n\t\tCategories: []string{\"a\", \"b\"},\n\t\tTimestamp:  time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\tLabels: map[string]any{\n\t\t\t\"tags\": []any{\"a\", \"e\"},\n\t\t},\n\t\tComment: \"comment\",\n\t})\n\tidf := dataSet.GetItemColumnValuesIDF()\n\tassert.Len(t, idf, 4)\n\tassert.InDelta(t, 1e-3, idf[0], 1e-6)\n\tassert.InDelta(t, math32.Log(2), idf[1], 1e-6)\n}\n\nfunc TestDataset_AddUser(t *testing.T) {\n\tdataSet := NewDataset(time.Now(), 1, 0)\n\tdataSet.AddUser(data.User{\n\t\tUserId:  \"1\",\n\t\tLabels:  map[string]any{\"a\": 1, \"b\": \"a\"},\n\t\tComment: \"comment\",\n\t})\n\tassert.Len(t, dataSet.users, 1)\n\tassert.Equal(t, data.User{\n\t\tUserId:  \"1\",\n\t\tLabels:  map[string]any{\"a\": 1, \"b\": ID(0)},\n\t\tComment: \"comment\",\n\t}, dataSet.users[0])\n}\n\nfunc TestDataset_GetUserColumnValuesIDF(t *testing.T) {\n\tdataSet := NewDataset(time.Now(), 1, 0)\n\tdataSet.AddUser(data.User{\n\t\tUserId: \"1\",\n\t\tLabels: map[string]any{\n\t\t\t\"tags\": []any{\"a\", \"b\", \"c\"},\n\t\t},\n\t\tComment: \"comment\",\n\t})\n\tdataSet.AddUser(data.User{\n\t\tUserId: \"2\",\n\t\tLabels: map[string]any{\n\t\t\t\"tags\": []any{\"a\", \"e\"},\n\t\t},\n\t\tComment: \"comment\",\n\t})\n\tidf := dataSet.GetUserColumnValuesIDF()\n\tassert.Len(t, idf, 4)\n\tassert.InDelta(t, 1e-3, idf[0], 1e-6)\n\tassert.InDelta(t, math32.Log(2), idf[1], 1e-6)\n}\n\nfunc TestDataset_AddFeedback(t *testing.T) {\n\tdataSet := NewDataset(time.Now(), 10, 10)\n\tfor i := 0; i < 10; i++ {\n\t\tdataSet.AddUser(data.User{\n\t\t\tUserId: strconv.Itoa(i),\n\t\t})\n\t}\n\tfor i := 0; i < 10; i++ {\n\t\tdataSet.AddItem(data.Item{\n\t\t\tItemId: strconv.Itoa(i),\n\t\t})\n\t}\n\tfor i := 0; i < 10; i++ {\n\t\tfor j := i; j < 10; j++ {\n\t\t\tdataSet.AddFeedback(strconv.Itoa(i), strconv.Itoa(j), time.Unix(int64(i*10+j), 0))\n\t\t}\n\t}\n\tuserIDF := dataSet.GetUserIDF()\n\titemIDF := dataSet.GetItemIDF()\n\tfor i := 0; i < 10; i++ {\n\t\tassert.Len(t, dataSet.GetUserFeedback()[i], 10-i)\n\t\tassert.Len(t, dataSet.GetItemFeedback()[i], i+1)\n\t\tassert.Len(t, dataSet.timestamps[i], 10-i)\n\t\tassert.InDelta(t, math32.Log(float32(10)/float32(10-i)), userIDF[i], 1e-2)\n\t\tassert.InDelta(t, math32.Log(float32(10)/float32(i+1)), itemIDF[i], 1e-2)\n\t}\n}\n\nfunc TestDataset_Split(t *testing.T) {\n\tconst numUsers, numItems = 3, 5\n\t// create dataset\n\tdataset := NewDataset(time.Now(), numUsers, numItems)\n\tfor i := 0; i < numUsers; i++ {\n\t\tdataset.AddUser(data.User{UserId: fmt.Sprintf(\"user%v\", i)})\n\t}\n\tfor i := 0; i < numItems; i++ {\n\t\tdataset.AddItem(data.Item{ItemId: fmt.Sprintf(\"item%v\", i)})\n\t}\n\tfor i := 0; i < numUsers; i++ {\n\t\tfor j := i + 1; j < numItems; j++ {\n\t\t\tdataset.AddFeedback(fmt.Sprintf(\"user%v\", i), fmt.Sprintf(\"item%v\", j), time.Time{})\n\t\t}\n\t}\n\tassert.Equal(t, 9, dataset.CountFeedback())\n\t// split\n\ttrain, test := dataset.SplitCF(0, 0)\n\tassert.Equal(t, numUsers, train.CountUsers())\n\tassert.Equal(t, numItems, train.CountItems())\n\tassert.Equal(t, 9-numUsers, train.CountFeedback())\n\tassert.Equal(t, numUsers, test.CountUsers())\n\tassert.Equal(t, numItems, test.CountItems())\n\tassert.Equal(t, numUsers, test.CountFeedback())\n\t// part split\n\ttrain2, test2 := dataset.SplitCF(2, 0)\n\tassert.Equal(t, numUsers, train2.CountUsers())\n\tassert.Equal(t, numItems, train2.CountItems())\n\tassert.Equal(t, 7, train2.CountFeedback())\n\tassert.Equal(t, numUsers, test2.CountUsers())\n\tassert.Equal(t, numItems, test2.CountItems())\n\tassert.Equal(t, 2, test2.CountFeedback())\n}\n\nfunc TestDataset_SplitLatest(t *testing.T) {\n\tconst numUsers, numItems = 3, 5\n\t// create dataset\n\tdataset := NewDataset(time.Now(), numUsers, numItems)\n\tfor i := 0; i < numUsers; i++ {\n\t\tdataset.AddUser(data.User{UserId: fmt.Sprintf(\"user%v\", i)})\n\t}\n\tfor i := 0; i < numItems; i++ {\n\t\tdataset.AddItem(data.Item{ItemId: fmt.Sprintf(\"item%v\", i)})\n\t}\n\tfor i := 0; i < numUsers; i++ {\n\t\tfor j := i + 1; j < numItems; j++ {\n\t\t\tdataset.AddFeedback(fmt.Sprintf(\"user%v\", i), fmt.Sprintf(\"item%v\", j), time.Unix(int64(j), 0))\n\t\t}\n\t}\n\tassert.Equal(t, 9, dataset.CountFeedback())\n\t// split\n\ttrain, test := dataset.SplitLatest(math.MaxInt)\n\tassert.Equal(t, numUsers, train.CountUsers())\n\tassert.Equal(t, numItems, train.CountItems())\n\tassert.Equal(t, numUsers, test.CountUsers())\n\tassert.Equal(t, numItems, test.CountItems())\n\tassert.Equal(t, 6, train.CountFeedback())\n\tassert.Equal(t, 3, test.CountFeedback())\n\tfor i := 0; i < numUsers; i++ {\n\t\tassert.Len(t, train.GetUserFeedback()[i], numItems-i-2)\n\t\tassert.Len(t, test.GetUserFeedback()[i], 1)\n\t\tassert.Equal(t, 4, int(test.GetUserFeedback()[i][0]))\n\t}\n}\n\nfunc TestDataset_LoadMovieLens1M(t *testing.T) {\n\ttrain, test, err := LoadDataFromBuiltIn(\"ml-1m\")\n\tassert.NoError(t, err)\n\tassert.Len(t, train.GetUsers(), 6040)\n\tassert.Len(t, train.GetItems(), 3706)\n\tassert.Equal(t, train.CountFeedback(), 994169)\n\tassert.Len(t, test.GetUsers(), 6040)\n\tassert.Len(t, test.GetItems(), 3706)\n\tassert.Equal(t, test.CountFeedback(), 6040)\n}\n"
  },
  {
    "path": "dataset/dict.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage dataset\n\ntype FreqDict struct {\n\tsi  map[string]int32\n\tis  []string\n\tcnt []int32\n}\n\nfunc NewFreqDict() (d *FreqDict) {\n\td = &FreqDict{map[string]int32{}, []string{}, []int32{}}\n\treturn\n}\n\nfunc (d *FreqDict) Count() int32 {\n\treturn int32(len(d.is))\n}\n\nfunc (d *FreqDict) Add(s string) (y int32) {\n\tif y, ok := d.si[s]; ok {\n\t\td.cnt[y]++\n\t\treturn y\n\t}\n\n\ty = int32(len(d.is))\n\td.si[s] = y\n\td.is = append(d.is, s)\n\td.cnt = append(d.cnt, 1)\n\treturn\n}\n\nfunc (d *FreqDict) AddNoCount(s string) (y int32) {\n\tif y, ok := d.si[s]; ok {\n\t\treturn y\n\t}\n\n\ty = int32(len(d.is))\n\td.si[s] = y\n\td.is = append(d.is, s)\n\td.cnt = append(d.cnt, 0)\n\treturn\n}\n\nfunc (d *FreqDict) Id(s string) int32 {\n\tif y, ok := d.si[s]; ok {\n\t\treturn y\n\t}\n\treturn -1\n}\n\nfunc (d *FreqDict) String(id int32) (s string, ok bool) {\n\tif id >= int32(len(d.is)) {\n\t\treturn \"\", false\n\t}\n\treturn d.is[id], true\n}\n\nfunc (d *FreqDict) Freq(id int32) int32 {\n\tif id >= int32(len(d.cnt)) {\n\t\treturn 0\n\t}\n\treturn d.cnt[id]\n}\n\nfunc (d *FreqDict) ToIndex() *Index {\n\treturn &Index{\n\t\tNumbers: d.si,\n\t\tNames:   d.is,\n\t}\n}\n"
  },
  {
    "path": "dataset/dict_test.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage dataset\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestFreqDict(t *testing.T) {\n\tdict := NewFreqDict()\n\tassert.Equal(t, int32(0), dict.Add(\"a\"))\n\tassert.Equal(t, int32(1), dict.Add(\"b\"))\n\tassert.Equal(t, int32(1), dict.Add(\"b\"))\n\tassert.Equal(t, int32(2), dict.Add(\"c\"))\n\tassert.Equal(t, int32(2), dict.Add(\"c\"))\n\tassert.Equal(t, int32(2), dict.Add(\"c\"))\n\tassert.Equal(t, int32(3), dict.Count())\n\tassert.Equal(t, int32(1), dict.Freq(0))\n\tassert.Equal(t, int32(2), dict.Freq(1))\n\tassert.Equal(t, int32(3), dict.Freq(2))\n\tassert.Equal(t, int32(0), dict.Id(\"a\"))\n\tassert.Equal(t, int32(-1), dict.Id(\"e\"))\n}\n"
  },
  {
    "path": "dataset/index.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage dataset\n\nimport (\n\t\"encoding/binary\"\n\t\"io\"\n\n\t\"github.com/gorse-io/gorse/common/encoding\"\n\t\"github.com/juju/errors\"\n)\n\n// MarshalIndex marshal index into byte stream.\nfunc MarshalIndex(w io.Writer, index *Index) error {\n\treturn index.Marshal(w)\n}\n\n// UnmarshalIndex unmarshal index from byte stream.\nfunc UnmarshalIndex(r io.Reader) (*Index, error) {\n\tindex := &Index{}\n\terr := index.Unmarshal(r)\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\treturn index, nil\n}\n\n// Index manages the map between sparse Names and dense indices. A sparse ID is\n// a user ID or item ID. The dense index is the internal user index or item index\n// optimized for faster parameter access and less memory usage.\ntype Index struct {\n\tNumbers map[string]int32 // sparse ID -> dense index\n\tNames   []string         // dense index -> sparse ID\n}\n\n// NotId represents an ID doesn't exist.\nconst NotId = int32(-1)\n\n// NewMapIndex creates a Index.\nfunc NewMapIndex() *Index {\n\tset := new(Index)\n\tset.Numbers = make(map[string]int32)\n\tset.Names = make([]string, 0)\n\treturn set\n}\n\n// Len returns the number of indexed Names.\nfunc (idx *Index) Len() int32 {\n\tif idx == nil {\n\t\treturn 0\n\t}\n\treturn int32(len(idx.Names))\n}\n\n// Add adds a new ID to the indexer.\nfunc (idx *Index) Add(name string) {\n\tif _, exist := idx.Numbers[name]; !exist {\n\t\tidx.Numbers[name] = int32(len(idx.Names))\n\t\tidx.Names = append(idx.Names, name)\n\t}\n}\n\n// ToNumber converts a sparse ID to a dense index.\nfunc (idx *Index) ToNumber(name string) int32 {\n\tif denseId, exist := idx.Numbers[name]; exist {\n\t\treturn denseId\n\t}\n\treturn NotId\n}\n\n// ToName converts a dense index to a sparse ID.\nfunc (idx *Index) ToName(index int32) string {\n\treturn idx.Names[index]\n}\n\n// GetNames returns all names in current index.\nfunc (idx *Index) GetNames() []string {\n\treturn idx.Names\n}\n\n// Marshal map index into byte stream.\nfunc (idx *Index) Marshal(w io.Writer) error {\n\t// write length\n\terr := binary.Write(w, binary.LittleEndian, int32(len(idx.Names)))\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t// write names\n\tfor _, s := range idx.Names {\n\t\terr = encoding.WriteString(w, s)\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// Unmarshal map index from byte stream.\nfunc (idx *Index) Unmarshal(r io.Reader) error {\n\t// read length\n\tvar n int32\n\terr := binary.Read(r, binary.LittleEndian, &n)\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t// write names\n\tidx.Names = make([]string, 0, n)\n\tidx.Numbers = make(map[string]int32, n)\n\tfor i := 0; i < int(n); i++ {\n\t\tname, err := encoding.ReadString(r)\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\tidx.Add(name)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "dataset/index_test.go",
    "content": "package dataset\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestIndex(t *testing.T) {\n\t// Null indexer\n\tvar index *Index\n\tassert.Zero(t, index.Len())\n\t// Create a indexer\n\tindex = NewMapIndex()\n\tassert.Zero(t, index.Len())\n\t// Add Names\n\tindex.Add(\"1\")\n\tindex.Add(\"2\")\n\tindex.Add(\"4\")\n\tindex.Add(\"8\")\n\tassert.Equal(t, int32(4), index.Len())\n\tassert.Equal(t, int32(0), index.ToNumber(\"1\"))\n\tassert.Equal(t, int32(1), index.ToNumber(\"2\"))\n\tassert.Equal(t, int32(2), index.ToNumber(\"4\"))\n\tassert.Equal(t, int32(3), index.ToNumber(\"8\"))\n\tassert.Equal(t, NotId, index.ToNumber(\"1000\"))\n\tassert.Equal(t, \"1\", index.ToName(0))\n\tassert.Equal(t, \"2\", index.ToName(1))\n\tassert.Equal(t, \"4\", index.ToName(2))\n\tassert.Equal(t, \"8\", index.ToName(3))\n\t// Get names\n\tassert.Equal(t, []string{\"1\", \"2\", \"4\", \"8\"}, index.GetNames())\n\t// Encode and decode\n\tbuf := bytes.NewBuffer(nil)\n\terr := MarshalIndex(buf, index)\n\tassert.NoError(t, err)\n\tindexCopy, err := UnmarshalIndex(buf)\n\tassert.NoError(t, err)\n\tassert.Equal(t, index, indexCopy)\n}\n"
  },
  {
    "path": "dataset/unified_index.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage dataset\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"io\"\n\t\"strconv\"\n\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/juju/errors\"\n)\n\n// UnifiedIndex maps users, items and labels into a unified encoding space.\ntype UnifiedIndex interface {\n\tLen() int32\n\tEncodeUser(userId string) int32\n\tEncodeItem(itemId string) int32\n\tEncodeUserLabel(userLabel string) int32\n\tEncodeItemLabel(itemLabel string) int32\n\tEncodeContextLabel(ctxLabel string) int32\n\tGetUsers() []string\n\tGetItems() []string\n\tGetUserLabels() []string\n\tGetItemLabels() []string\n\tGetContextLabels() []string\n\tCountUsers() int32\n\tCountItems() int32\n\tCountUserLabels() int32\n\tCountItemLabels() int32\n\tCountContextLabels() int32\n\tMarshal(w io.Writer) error\n\tUnmarshal(r io.Reader) error\n}\n\nconst (\n\tmapIndex uint8 = iota\n\tdirectIndex\n\tnilIndex\n)\n\n// MarshalIndex marshal index into byte stream.\nfunc MarshalUnifiedIndex(w io.Writer, index UnifiedIndex) error {\n\t// if index is nil\n\tif index == nil {\n\t\treturn binary.Write(w, binary.LittleEndian, nilIndex)\n\t}\n\t// write index type\n\tvar indexType uint8\n\tswitch index.(type) {\n\tcase *UnifiedMapIndex:\n\t\tindexType = mapIndex\n\tcase *UnifiedDirectIndex:\n\t\tindexType = directIndex\n\tdefault:\n\t\treturn errors.New(\"unknown index type\")\n\t}\n\terr := binary.Write(w, binary.LittleEndian, indexType)\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t// write index\n\treturn index.Marshal(w)\n}\n\n// UnmarshalIndex unmarshal index from byte stream.\nfunc UnmarshalUnifiedIndex(r io.Reader) (UnifiedIndex, error) {\n\t// read type index\n\tvar indexType uint8\n\terr := binary.Read(r, binary.LittleEndian, &indexType)\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\tvar index UnifiedIndex\n\tswitch indexType {\n\tcase mapIndex:\n\t\tindex = &UnifiedMapIndex{}\n\tcase directIndex:\n\t\tindex = &UnifiedDirectIndex{}\n\tcase nilIndex:\n\t\treturn nil, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown index type (%v)\", indexType)\n\t}\n\t// read index\n\terr = index.Unmarshal(r)\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\treturn index, nil\n}\n\n// UnifiedMapIndexBuilder is the builder for UnifiedMapIndex.\ntype UnifiedMapIndexBuilder struct {\n\tUserIndex      *Index\n\tItemIndex      *Index\n\tUserLabelIndex *Index\n\tItemLabelIndex *Index\n\tCtxLabelIndex  *Index\n}\n\n// NewUnifiedMapIndexBuilder creates a UnifiedMapIndexBuilder.\nfunc NewUnifiedMapIndexBuilder() *UnifiedMapIndexBuilder {\n\treturn &UnifiedMapIndexBuilder{\n\t\tUserIndex:      NewMapIndex(),\n\t\tItemIndex:      NewMapIndex(),\n\t\tUserLabelIndex: NewMapIndex(),\n\t\tItemLabelIndex: NewMapIndex(),\n\t\tCtxLabelIndex:  NewMapIndex(),\n\t}\n}\n\n// AddUser adds a user into the unified index.\nfunc (builder *UnifiedMapIndexBuilder) AddUser(userId string) {\n\tbuilder.UserIndex.Add(userId)\n}\n\n// AddItem adds a item into the unified index.\nfunc (builder *UnifiedMapIndexBuilder) AddItem(itemId string) {\n\tbuilder.ItemIndex.Add(itemId)\n}\n\n// AddUserLabel adds a user label into the unified index.\nfunc (builder *UnifiedMapIndexBuilder) AddUserLabel(userLabel string) {\n\tbuilder.UserLabelIndex.Add(userLabel)\n}\n\n// AddItemLabel adds a item label into the unified index.\nfunc (builder *UnifiedMapIndexBuilder) AddItemLabel(itemLabel string) {\n\tbuilder.ItemLabelIndex.Add(itemLabel)\n}\n\n// AddCtxLabel adds a context label the unified index.\nfunc (builder *UnifiedMapIndexBuilder) AddCtxLabel(ctxLabel string) {\n\tbuilder.CtxLabelIndex.Add(ctxLabel)\n}\n\n// Build UnifiedMapIndex from UnifiedMapIndexBuilder.\nfunc (builder *UnifiedMapIndexBuilder) Build() UnifiedIndex {\n\treturn &UnifiedMapIndex{\n\t\tUserIndex:      builder.UserIndex,\n\t\tItemIndex:      builder.ItemIndex,\n\t\tUserLabelIndex: builder.UserLabelIndex,\n\t\tItemLabelIndex: builder.ItemLabelIndex,\n\t\tCtxLabelIndex:  builder.CtxLabelIndex,\n\t}\n}\n\n// UnifiedMapIndex is the id -> index mapper for factorization machines.\n// The division of id is: | user | item | user label | item label | context label |\ntype UnifiedMapIndex struct {\n\tUserIndex      *Index\n\tItemIndex      *Index\n\tUserLabelIndex *Index\n\tItemLabelIndex *Index\n\tCtxLabelIndex  *Index\n}\n\n// GetUserLabels returns all user labels.\nfunc (unified *UnifiedMapIndex) GetUserLabels() []string {\n\treturn unified.UserLabelIndex.GetNames()\n}\n\n// GetItemLabels returns all item labels.\nfunc (unified *UnifiedMapIndex) GetItemLabels() []string {\n\treturn unified.ItemLabelIndex.GetNames()\n}\n\n// GetContextLabels returns all context labels.\nfunc (unified *UnifiedMapIndex) GetContextLabels() []string {\n\treturn unified.CtxLabelIndex.GetNames()\n}\n\n// CountUserLabels returns the number of user labels.\nfunc (unified *UnifiedMapIndex) CountUserLabels() int32 {\n\treturn unified.UserLabelIndex.Len()\n}\n\n// CountItemLabels returns the number of item labels.\nfunc (unified *UnifiedMapIndex) CountItemLabels() int32 {\n\treturn unified.ItemLabelIndex.Len()\n}\n\n// CountContextLabels returns the number of context labels.\nfunc (unified *UnifiedMapIndex) CountContextLabels() int32 {\n\treturn unified.CtxLabelIndex.Len()\n}\n\n// Len returns the size of unified index.\nfunc (unified *UnifiedMapIndex) Len() int32 {\n\treturn unified.UserIndex.Len() + unified.ItemIndex.Len() +\n\t\tunified.UserLabelIndex.Len() + unified.ItemLabelIndex.Len() +\n\t\tunified.CtxLabelIndex.Len()\n}\n\n// EncodeUser converts a user id to a integer in the encoding space.\nfunc (unified *UnifiedMapIndex) EncodeUser(userId string) int32 {\n\treturn unified.UserIndex.ToNumber(userId)\n}\n\n// EncodeItem converts a item id to a integer in the encoding space.\nfunc (unified *UnifiedMapIndex) EncodeItem(itemId string) int32 {\n\titemIndex := unified.ItemIndex.ToNumber(itemId)\n\tif itemIndex != NotId {\n\t\titemIndex += unified.UserIndex.Len()\n\t}\n\treturn itemIndex\n}\n\n// EncodeUserLabel converts a user label to a integer in the encoding space.\nfunc (unified *UnifiedMapIndex) EncodeUserLabel(userLabel string) int32 {\n\tuserLabelIndex := unified.UserLabelIndex.ToNumber(userLabel)\n\tif userLabelIndex != NotId {\n\t\tuserLabelIndex += unified.UserIndex.Len() + unified.ItemIndex.Len()\n\t}\n\treturn userLabelIndex\n}\n\n// EncodeItemLabel converts a item label to a integer in the encoding space.\nfunc (unified *UnifiedMapIndex) EncodeItemLabel(itemLabel string) int32 {\n\titemLabelIndex := unified.ItemLabelIndex.ToNumber(itemLabel)\n\tif itemLabelIndex != NotId {\n\t\titemLabelIndex += unified.UserIndex.Len() + unified.ItemIndex.Len() + unified.UserLabelIndex.Len()\n\t}\n\treturn itemLabelIndex\n}\n\n// EncodeContextLabel converts a context label to a integer in the encoding space.\nfunc (unified *UnifiedMapIndex) EncodeContextLabel(label string) int32 {\n\tctxLabelIndex := unified.CtxLabelIndex.ToNumber(label)\n\tif ctxLabelIndex != NotId {\n\t\tctxLabelIndex += unified.UserIndex.Len() + unified.ItemIndex.Len() +\n\t\t\tunified.UserLabelIndex.Len() + unified.ItemLabelIndex.Len()\n\t}\n\treturn ctxLabelIndex\n}\n\n// GetUsers returns all users.\nfunc (unified *UnifiedMapIndex) GetUsers() []string {\n\treturn unified.UserIndex.GetNames()\n}\n\n// GetItems returns all items.\nfunc (unified *UnifiedMapIndex) GetItems() []string {\n\treturn unified.ItemIndex.GetNames()\n}\n\n// CountUsers returns the number of users.\nfunc (unified *UnifiedMapIndex) CountUsers() int32 {\n\treturn unified.UserIndex.Len()\n}\n\n// CountItems returns the number of items.\nfunc (unified *UnifiedMapIndex) CountItems() int32 {\n\treturn unified.ItemIndex.Len()\n}\n\n// Marshal map index into byte stream.\nfunc (unified *UnifiedMapIndex) Marshal(w io.Writer) error {\n\tindices := []*Index{unified.UserIndex, unified.ItemIndex, unified.UserLabelIndex, unified.ItemLabelIndex, unified.CtxLabelIndex}\n\tfor _, index := range indices {\n\t\terr := MarshalIndex(w, index)\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// Unmarshal map index from byte stream.\nfunc (unified *UnifiedMapIndex) Unmarshal(r io.Reader) error {\n\tindices := []**Index{&unified.UserIndex, &unified.ItemIndex, &unified.UserLabelIndex, &unified.ItemLabelIndex, &unified.CtxLabelIndex}\n\tfor i := range indices {\n\t\tvar err error\n\t\t*indices[i], err = UnmarshalIndex(r)\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// UnifiedDirectIndex maps string to integer in literal.\ntype UnifiedDirectIndex struct {\n\tN int32\n}\n\n// EncodeUserLabel should be used by unit testing only.\nfunc (unified *UnifiedDirectIndex) EncodeUserLabel(userLabel string) int32 {\n\tif val, err := strconv.Atoi(userLabel); err != nil {\n\t\tpanic(err)\n\t} else {\n\t\treturn int32(val)\n\t}\n}\n\n// EncodeItemLabel should be used by unit testing only.\nfunc (unified *UnifiedDirectIndex) EncodeItemLabel(itemLabel string) int32 {\n\tif val, err := strconv.Atoi(itemLabel); err != nil {\n\t\tpanic(err)\n\t} else {\n\t\treturn int32(val)\n\t}\n}\n\n// GetUserLabels should be used by unit testing only.\nfunc (unified *UnifiedDirectIndex) GetUserLabels() []string {\n\tvar names []string\n\tbegin, end := unified.N/5*3, unified.N/5*4\n\tfor i := begin; i < end; i++ {\n\t\tnames = append(names, strconv.Itoa(int(i)))\n\t}\n\treturn names\n}\n\n// GetItemLabels should be used by unit testing only.\nfunc (unified *UnifiedDirectIndex) GetItemLabels() []string {\n\tvar names []string\n\tbegin, end := unified.N/5*2, unified.N/5*3\n\tfor i := begin; i < end; i++ {\n\t\tnames = append(names, strconv.Itoa(int(i)))\n\t}\n\treturn names\n}\n\n// GetContextLabels should be used by unit testing only.\nfunc (unified *UnifiedDirectIndex) GetContextLabels() []string {\n\tvar names []string\n\tbegin, end := unified.N/5*4, unified.N\n\tfor i := begin; i < end; i++ {\n\t\tnames = append(names, strconv.Itoa(int(i)))\n\t}\n\treturn names\n}\n\n// CountUserLabels should be used by unit testing only.\nfunc (unified *UnifiedDirectIndex) CountUserLabels() int32 {\n\treturn unified.N / 5\n}\n\n// CountItemLabels should be used by unit testing only.\nfunc (unified *UnifiedDirectIndex) CountItemLabels() int32 {\n\treturn unified.N / 5\n}\n\n// CountContextLabels should be used by unit testing only.\nfunc (unified *UnifiedDirectIndex) CountContextLabels() int32 {\n\treturn unified.N - unified.N/5*4\n}\n\n// NewUnifiedDirectIndex creates a UnifiedDirectIndex.\nfunc NewUnifiedDirectIndex(n int32) UnifiedIndex {\n\treturn &UnifiedDirectIndex{N: n}\n}\n\n// Len should be used by unit testing only.\nfunc (unified *UnifiedDirectIndex) Len() int32 {\n\treturn unified.N\n}\n\n// EncodeUser should be used by unit testing only.\nfunc (unified *UnifiedDirectIndex) EncodeUser(userId string) int32 {\n\tif val, err := strconv.Atoi(userId); err != nil {\n\t\tpanic(err)\n\t} else {\n\t\treturn int32(val)\n\t}\n}\n\n// EncodeItem should be used by unit testing only.\nfunc (unified *UnifiedDirectIndex) EncodeItem(itemId string) int32 {\n\tif val, err := strconv.Atoi(itemId); err != nil {\n\t\tpanic(err)\n\t} else {\n\t\treturn int32(val)\n\t}\n}\n\n// EncodeContextLabel should be used by unit testing only.\nfunc (unified *UnifiedDirectIndex) EncodeContextLabel(label string) int32 {\n\tif val, err := strconv.Atoi(label); err != nil {\n\t\tpanic(err)\n\t} else {\n\t\treturn int32(val)\n\t}\n}\n\n// GetUsers should be used by unit testing only.\nfunc (unified *UnifiedDirectIndex) GetUsers() []string {\n\tvar names []string\n\tbegin, end := unified.N/5, unified.N/5*2\n\tfor i := begin; i < end; i++ {\n\t\tnames = append(names, strconv.Itoa(int(i)))\n\t}\n\treturn names\n}\n\n// GetItems should be used by unit testing only.\nfunc (unified *UnifiedDirectIndex) GetItems() []string {\n\tlog.Logger().Warn(\"\")\n\tvar names []string\n\tbegin, end := int32(0), unified.N/5\n\tfor i := begin; i < end; i++ {\n\t\tnames = append(names, strconv.Itoa(int(i)))\n\t}\n\treturn names\n}\n\n// CountUsers should be used by unit testing only.\nfunc (unified *UnifiedDirectIndex) CountUsers() int32 {\n\treturn unified.N / 5\n}\n\n// CountItems should be used by unit testing only.\nfunc (unified *UnifiedDirectIndex) CountItems() int32 {\n\treturn unified.N / 5\n}\n\n// Marshal direct index into byte stream.\nfunc (unified *UnifiedDirectIndex) Marshal(w io.Writer) error {\n\treturn binary.Write(w, binary.LittleEndian, unified.N)\n}\n\n// Unmarshal direct index from byte stream.\nfunc (unified *UnifiedDirectIndex) Unmarshal(r io.Reader) error {\n\treturn binary.Read(r, binary.LittleEndian, &unified.N)\n}\n"
  },
  {
    "path": "dataset/unified_index_test.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage dataset\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestUnifiedMapIndex(t *testing.T) {\n\t// create unified map index\n\tbuilder := NewUnifiedMapIndexBuilder()\n\tvar numUsers, numItems, numUserLabels, numItemLabels, numCtxLabels int32 = 3, 4, 5, 6, 7\n\tfor i := int32(0); i < numUsers; i++ {\n\t\tbuilder.AddUser(fmt.Sprintf(\"user%v\", i))\n\t}\n\tfor i := int32(0); i < numItems; i++ {\n\t\tbuilder.AddItem(fmt.Sprintf(\"item%v\", i))\n\t}\n\tfor i := int32(0); i < numUserLabels; i++ {\n\t\tbuilder.AddUserLabel(fmt.Sprintf(\"user_label%v\", i))\n\t}\n\tfor i := int32(0); i < numItemLabels; i++ {\n\t\tbuilder.AddItemLabel(fmt.Sprintf(\"item_label%v\", i))\n\t}\n\tfor i := int32(0); i < numCtxLabels; i++ {\n\t\tbuilder.AddCtxLabel(fmt.Sprintf(\"ctx_label%v\", i))\n\t}\n\tindex := builder.Build()\n\t// check count\n\tassert.Equal(t, numUsers+numItems+numUserLabels+numItemLabels+numCtxLabels, index.Len())\n\tassert.Equal(t, numUsers, index.CountUsers())\n\tassert.Equal(t, numItems, index.CountItems())\n\tassert.Equal(t, numUserLabels, index.CountUserLabels())\n\tassert.Equal(t, numItemLabels, index.CountItemLabels())\n\tassert.Equal(t, numCtxLabels, index.CountContextLabels())\n\t// check encode\n\tusers := index.GetUsers()\n\tfor i := int32(0); i < numUsers; i++ {\n\t\tuserIndex := index.EncodeUser(fmt.Sprintf(\"user%v\", i))\n\t\tassert.Equal(t, i, userIndex)\n\t\tassert.Equal(t, fmt.Sprintf(\"user%v\", i), users[i])\n\t}\n\titems := index.GetItems()\n\tfor i := int32(0); i < numItems; i++ {\n\t\titemIndex := index.EncodeItem(fmt.Sprintf(\"item%v\", i))\n\t\tassert.Equal(t, numUsers+i, itemIndex)\n\t\tassert.Equal(t, fmt.Sprintf(\"item%v\", i), items[i])\n\t}\n\tuserLabels := index.GetUserLabels()\n\tfor i := int32(0); i < numUserLabels; i++ {\n\t\tuserLabelIndex := index.EncodeUserLabel(fmt.Sprintf(\"user_label%v\", i))\n\t\tassert.Equal(t, numUsers+numItems+i, userLabelIndex)\n\t\tassert.Equal(t, fmt.Sprintf(\"user_label%v\", i), userLabels[i])\n\t}\n\titemLabels := index.GetItemLabels()\n\tfor i := int32(0); i < numItemLabels; i++ {\n\t\titemLabelIndex := index.EncodeItemLabel(fmt.Sprintf(\"item_label%v\", i))\n\t\tassert.Equal(t, numUsers+numItems+numUserLabels+i, itemLabelIndex)\n\t\tassert.Equal(t, fmt.Sprintf(\"item_label%v\", i), itemLabels[i])\n\t}\n\tctxLabels := index.GetContextLabels()\n\tfor i := int32(0); i < numCtxLabels; i++ {\n\t\tctxLabelIndex := index.EncodeContextLabel(fmt.Sprintf(\"ctx_label%v\", i))\n\t\tassert.Equal(t, numUsers+numItems+numUserLabels+numItemLabels+i, ctxLabelIndex)\n\t\tassert.Equal(t, fmt.Sprintf(\"ctx_label%v\", i), ctxLabels[i])\n\t}\n\t// Encode and decode\n\tbuf := bytes.NewBuffer(nil)\n\terr := MarshalUnifiedIndex(buf, index)\n\tassert.NoError(t, err)\n\tindexCopy, err := UnmarshalUnifiedIndex(buf)\n\tassert.NoError(t, err)\n\tassert.Equal(t, index, indexCopy)\n}\n\nfunc TestUnifiedDirectIndex(t *testing.T) {\n\tindex := NewUnifiedDirectIndex(10)\n\tassert.Equal(t, int32(10), index.Len())\n\tassert.Equal(t, []string{\"0\", \"1\"}, index.GetItems())\n\tassert.Equal(t, []string{\"2\", \"3\"}, index.GetUsers())\n\tassert.Equal(t, []string{\"4\", \"5\"}, index.GetItemLabels())\n\tassert.Equal(t, []string{\"6\", \"7\"}, index.GetUserLabels())\n\tassert.Equal(t, []string{\"8\", \"9\"}, index.GetContextLabels())\n\tassert.Equal(t, int32(2), index.CountItems())\n\tassert.Equal(t, int32(2), index.CountUsers())\n\tassert.Equal(t, int32(2), index.CountItemLabels())\n\tassert.Equal(t, int32(2), index.CountUserLabels())\n\tassert.Equal(t, int32(2), index.CountContextLabels())\n\tassert.Panics(t, func() { index.EncodeItem(\"abc\") })\n\tassert.Panics(t, func() { index.EncodeUser(\"abc\") })\n\tassert.Panics(t, func() { index.EncodeItemLabel(\"abc\") })\n\tassert.Panics(t, func() { index.EncodeUserLabel(\"abc\") })\n\tassert.Panics(t, func() { index.EncodeContextLabel(\"abc\") })\n\tassert.Equal(t, int32(1), index.EncodeItem(\"1\"))\n\tassert.Equal(t, int32(2), index.EncodeUser(\"2\"))\n\tassert.Equal(t, int32(3), index.EncodeItemLabel(\"3\"))\n\tassert.Equal(t, int32(4), index.EncodeUserLabel(\"4\"))\n\tassert.Equal(t, int32(5), index.EncodeContextLabel(\"5\"))\n\t// Encode and decode\n\tbuf := bytes.NewBuffer(nil)\n\terr := MarshalUnifiedIndex(buf, index)\n\tassert.NoError(t, err)\n\tindexCopy, err := UnmarshalUnifiedIndex(buf)\n\tassert.NoError(t, err)\n\tassert.Equal(t, index, indexCopy)\n}\n"
  },
  {
    "path": "docker-bake.hcl",
    "content": "variable \"VERSIONS\" {\n  default = \"nightly\"\n}\n\nvariable versions {\n  default = split(\",\", VERSIONS)\n}\n\nvariable components {\n  default = [\"gorse-master\", \"gorse-server\", \"gorse-worker\", \"gorse-in-one\"]\n}\n\ngroup \"default\" {\n  targets = [\"gorse-master\", \"gorse-server\", \"gorse-worker\", \"gorse-in-one\"]\n}\n\ntarget \"openblas\" {\n  matrix = {\n    component = components\n  }\n  name       = component\n  context    = \".\"\n  dockerfile = \"cmd/${component}/Dockerfile.openblas\"\n  platforms  = [\"linux/amd64\", \"linux/arm64\", \"linux/riscv64\"]\n  tags       = [for v in versions : \"zhenghaoz/${component}:${v}\"]\n  cache-from = [\"type=gha\"]\n  cache-to   = [\"type=gha,mode=max\"]\n}\n\ntarget \"cuda\" {\n  matrix = {\n    component = components\n  }\n  name       = \"${component}-cuda\"\n  context    = \".\"\n  dockerfile = \"cmd/${component}/Dockerfile.cuda\"\n  platforms  = [\"linux/amd64\"]\n  tags       = [for v in versions : \"zhenghaoz/${component}:${v}-cuda12.8\"]\n  cache-from = [\"type=s3,endpoint_url=https://b172f19b7e057975835d8d311a7b0dbd.r2.cloudflarestorage.com,bucket=github,region=auto\"]\n  cache-to   = [\"type=s3,endpoint_url=https://b172f19b7e057975835d8d311a7b0dbd.r2.cloudflarestorage.com,bucket=github,region=auto,mode=max\"]\n}\n\ntarget \"mkl\" {\n  matrix = {\n    component = components\n  }\n  name       = \"${component}-mkl\"\n  context    = \".\"\n  dockerfile = \"cmd/${component}/Dockerfile.mkl\"\n  platforms  = [\"linux/amd64\"]\n  tags       = [for v in versions : \"zhenghaoz/${component}:${v}-mkl\"]\n  cache-from = [\"type=s3,endpoint_url=https://b172f19b7e057975835d8d311a7b0dbd.r2.cloudflarestorage.com,bucket=github,region=auto\"]\n  cache-to   = [\"type=s3,endpoint_url=https://b172f19b7e057975835d8d311a7b0dbd.r2.cloudflarestorage.com,bucket=github,region=auto,mode=max\"]\n}\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: \"3\"\nservices:\n  mysql:\n    image: mysql/mysql-server\n    restart: unless-stopped\n    ports:\n      - 3306:3306\n    environment:\n      MYSQL_ROOT_PASSWORD: root_pass\n      MYSQL_DATABASE: gorse\n      MYSQL_USER: gorse\n      MYSQL_PASSWORD: gorse_pass\n    volumes:\n      - mysql_data:/var/lib/mysql\n\n  # postgres:\n  #   image: postgres:10.0\n  #   ports:\n  #     - 5432:5432\n  #   environment:\n  #     POSTGRES_DB: gorse\n  #     POSTGRES_USER: gorse\n  #     POSTGRES_PASSWORD: gorse_pass\n  #   volumes:\n  #     - postgres_data:/var/lib/postgresql/data\n\n  # mongo:\n  #   image: mongo:4.0\n  #   ports:\n  #     - 27017:27017\n  #   environment:\n  #     MONGO_INITDB_DATABASE: gorse\n  #     MONGO_INITDB_ROOT_USERNAME: root\n  #     MONGO_INITDB_ROOT_PASSWORD: password\n  #   volumes:\n  #     - mongo_data:/data/db\n\n  # clickhouse:\n  #   image: clickhouse/clickhouse-server:22\n  #   ports:\n  #     - 8123:8123\n  #   environment:\n  #     CLICKHOUSE_DB: gorse\n  #     CLICKHOUSE_USER: gorse\n  #     CLICKHOUSE_PASSWORD: gorse_pass\n  #   volumes:\n  #     - clickhouse_data:/var/lib/clickhouse\n\n  # redis:\n  #   image: redis/redis-stack\n  #   restart: unless-stopped\n  #   ports:\n  #     - 6379:6379\n\n  worker:\n    image: zhenghaoz/gorse-worker\n    restart: unless-stopped\n    ports:\n      - 8089:8089\n    command: >\n      --master-host master --master-port 8086 \n      --http-host 0.0.0.0 --http-port 8089\n      --log-path /var/log/gorse/worker.log \n      --cache-path /var/lib/gorse/worker_cache.data\n    volumes:\n      - gorse_log:/var/log/gorse\n      - worker_data:/var/lib/gorse\n    depends_on:\n      - master\n\n  server:\n    image: zhenghaoz/gorse-server\n    restart: unless-stopped\n    ports:\n      - 8087:8087\n    command: >\n      --master-host master --master-port 8086 \n      --http-host 0.0.0.0 --http-port 8087\n      --log-path /var/log/gorse/server.log \n      --cache-path /var/lib/gorse/server_cache.data\n    volumes:\n      - gorse_log:/var/log/gorse\n      - server_data:/var/lib/gorse\n    depends_on:\n      - master\n\n  master:\n    image: zhenghaoz/gorse-master\n    restart: unless-stopped\n    ports:\n      - 8086:8086\n      - 8088:8088\n    environment:\n      GORSE_CACHE_STORE: mysql://gorse:gorse_pass@tcp(mysql:3306)/gorse\n      # GORSE_CACHE_STORE: postgres://gorse:gorse_pass@postgres/gorse?sslmode=disable\n      # GORSE_CACHE_STORE: mongodb://root:password@mongo:27017/gorse?authSource=admin&connect=direct\n      # GORSE_CACHE_STORE: redis://redis:6379\n      GORSE_DATA_STORE: mysql://gorse:gorse_pass@tcp(mysql:3306)/gorse\n      # GORSE_DATA_STORE: postgres://gorse:gorse_pass@postgres/gorse?sslmode=disable\n      # GORSE_DATA_STORE: mongodb://root:password@mongo:27017/gorse?authSource=admin&connect=direct\n      # GORSE_DATA_STORE: clickhouse://gorse:gorse_pass@clickhouse:8123/gorse\n      GORSE_BLOB_URI: /var/lib/gorse/blob\n      # GORSE_BLOB_URI: s3://my-bucket/path\n      # GORSE_BLOB_URI: gs://my-bucket/path\n      # GORSE_BLOB_URI: az://container/path\n    command: >\n      -c /etc/gorse/config.toml \n      --log-path /var/log/gorse/master.log \n      --cache-path /var/lib/gorse/master\n    volumes:\n      - ./config/config.toml:/etc/gorse/config.toml\n      - gorse_log:/var/log/gorse\n      - master_data:/var/lib/gorse/master\n      - blob_data:/var/lib/gorse/blob\n    depends_on:\n      - mysql\n      # - postgres\n      # - mongo\n      # - clickhouse\n      # - redis\n\nvolumes:\n  worker_data:\n  server_data:\n  master_data:\n  gorse_log:\n  mysql_data:\n  # postgres_data:\n  # mongo_data:\n  # clickhouse_data:\n  blob_data:\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/gorse-io/gorse\n\ngo 1.26\n\nrequire (\n\tcloud.google.com/go/storage v1.61.3\n\tgithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3\n\tgithub.com/XSAM/otelsql v0.41.0\n\tgithub.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de\n\tgithub.com/bits-and-blooms/bitset v1.24.4\n\tgithub.com/c-bata/goptuna v0.8.1\n\tgithub.com/cenkalti/backoff/v5 v5.0.3\n\tgithub.com/chewxy/math32 v1.11.1\n\tgithub.com/coreos/go-oidc/v3 v3.17.0\n\tgithub.com/deckarep/golang-set/v2 v2.8.0\n\tgithub.com/emicklei/go-restful-openapi/v2 v2.12.0\n\tgithub.com/emicklei/go-restful/v3 v3.13.0\n\tgithub.com/expr-lang/expr v1.17.8\n\tgithub.com/fsouza/fake-gcs-server v1.54.0\n\tgithub.com/fxtlabs/primes v0.0.0-20150821004651-dad82d10a449\n\tgithub.com/go-openapi/strfmt v0.26.1\n\tgithub.com/go-playground/locales v0.14.1\n\tgithub.com/go-playground/universal-translator v0.18.1\n\tgithub.com/go-playground/validator/v10 v10.30.1\n\tgithub.com/go-sql-driver/mysql v1.9.3\n\tgithub.com/go-viper/mapstructure/v2 v2.5.0\n\tgithub.com/gomlx/gomlx v0.27.1\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/gorilla/securecookie v1.1.2\n\tgithub.com/gorse-io/dashboard v0.0.0-20260223101641-33715448ded8\n\tgithub.com/gorse-io/gorse-go v0.5.0-alpha.3\n\tgithub.com/invopop/jsonschema v0.13.0\n\tgithub.com/jaswdr/faker v1.19.1\n\tgithub.com/jellydator/ttlcache/v3 v3.4.0\n\tgithub.com/juju/errors v1.0.0\n\tgithub.com/juju/ratelimit v1.0.2\n\tgithub.com/klauspost/cpuid/v2 v2.3.0\n\tgithub.com/lafikl/consistent v0.0.0-20220512074542-bdd3606bfc3e\n\tgithub.com/lib/pq v1.11.2\n\tgithub.com/madflojo/testcerts v1.5.0\n\tgithub.com/mailru/go-clickhouse/v2 v2.5.1\n\tgithub.com/matttproud/golang_protobuf_extensions v1.0.4\n\tgithub.com/milvus-io/milvus-sdk-go/v2 v2.4.2\n\tgithub.com/minio/minio-go/v7 v7.0.99\n\tgithub.com/modern-go/reflect2 v1.0.2\n\tgithub.com/nikolalohinski/gonja/v2 v2.7.0\n\tgithub.com/olekukonko/tablewriter v1.1.4\n\tgithub.com/pkg/errors v0.9.1\n\tgithub.com/prometheus/client_golang v1.23.2\n\tgithub.com/qdrant/go-client v1.17.1\n\tgithub.com/rakyll/statik v0.1.8\n\tgithub.com/redis/go-redis/extra/redisotel/v9 v9.18.0\n\tgithub.com/redis/go-redis/v9 v9.18.0\n\tgithub.com/samber/lo v1.53.0\n\tgithub.com/sashabaranov/go-openai v1.41.2\n\tgithub.com/schollz/progressbar/v3 v3.19.0\n\tgithub.com/sclevine/yj v0.0.0-20210612025309-737bdf40a5d1\n\tgithub.com/spf13/cobra v1.10.2\n\tgithub.com/spf13/pflag v1.0.10\n\tgithub.com/spf13/viper v1.21.0\n\tgithub.com/steinfletcher/apitest v1.6.0\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/swaggest/swgui v1.8.5\n\tgithub.com/tiktoken-go/tokenizer v0.7.0\n\tgithub.com/weaviate/weaviate v1.27.0\n\tgithub.com/weaviate/weaviate-go-client/v4 v4.16.1\n\tgithub.com/yuin/goldmark v1.7.16\n\tgo.mongodb.org/mongo-driver v1.17.9\n\tgo.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful v0.67.0\n\tgo.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo v0.67.0\n\tgo.opentelemetry.io/otel v1.42.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0\n\tgo.opentelemetry.io/otel/exporters/zipkin v1.42.0\n\tgo.opentelemetry.io/otel/sdk v1.42.0\n\tgo.opentelemetry.io/otel/trace v1.42.0\n\tgo.uber.org/atomic v1.11.0\n\tgo.uber.org/zap v1.27.1\n\tgolang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90\n\tgolang.org/x/oauth2 v0.36.0\n\tgolang.org/x/sys v0.42.0\n\tgoogle.golang.org/api v0.272.0\n\tgoogle.golang.org/grpc v1.79.2\n\tgoogle.golang.org/grpc/security/advancedtls v1.0.0\n\tgoogle.golang.org/protobuf v1.36.11\n\tgopkg.in/natefinch/lumberjack.v2 v2.2.1\n\tgorm.io/driver/clickhouse v0.4.2\n\tgorm.io/driver/mysql v1.3.4\n\tgorm.io/driver/postgres v1.3.5\n\tgorm.io/driver/sqlite v1.3.4\n\tgorm.io/gorm v1.23.6\n\tmodernc.org/mathutil v1.7.1\n\tmodernc.org/quickjs v0.17.1\n\tmodernc.org/sortutil v1.2.1\n\tmodernc.org/sqlite v1.47.0\n\tmodernc.org/strutil v1.2.1\n)\n\nrequire (\n\tcel.dev/expr v0.25.1 // indirect\n\tcloud.google.com/go v0.123.0 // indirect\n\tcloud.google.com/go/auth v0.18.2 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect\n\tcloud.google.com/go/compute/metadata v0.9.0 // indirect\n\tcloud.google.com/go/iam v1.5.3 // indirect\n\tcloud.google.com/go/monitoring v1.24.3 // indirect\n\tcloud.google.com/go/pubsub/v2 v2.4.0 // indirect\n\tfilippo.io/edwards25519 v1.1.0 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect\n\tgithub.com/bahlo/generic-list-go v0.2.0 // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/buger/jsonparser v1.1.1 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/clipperhouse/displaywidth v0.11.0 // indirect\n\tgithub.com/clipperhouse/uax29/v2 v2.7.0 // indirect\n\tgithub.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect\n\tgithub.com/cockroachdb/errors v1.9.1 // indirect\n\tgithub.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f // indirect\n\tgithub.com/cockroachdb/redact v1.1.3 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/dlclark/regexp2 v1.11.5 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect\n\tgithub.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect\n\tgithub.com/erraggy/oastools v1.36.1 // indirect\n\tgithub.com/fatih/color v1.18.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.12 // indirect\n\tgithub.com/getsentry/sentry-go v0.30.0 // indirect\n\tgithub.com/go-ini/ini v1.67.0 // indirect\n\tgithub.com/go-jose/go-jose/v4 v4.1.3 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-openapi/analysis v0.24.1 // indirect\n\tgithub.com/go-openapi/errors v0.22.7 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.22.4 // indirect\n\tgithub.com/go-openapi/jsonreference v0.21.4 // indirect\n\tgithub.com/go-openapi/loads v0.23.2 // indirect\n\tgithub.com/go-openapi/spec v0.22.2 // indirect\n\tgithub.com/go-openapi/swag v0.23.0 // indirect\n\tgithub.com/go-openapi/swag/conv v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/fileutils v0.25.1 // indirect\n\tgithub.com/go-openapi/swag/jsonname v0.25.4 // indirect\n\tgithub.com/go-openapi/swag/jsonutils v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/loading v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/mangling v0.25.1 // indirect\n\tgithub.com/go-openapi/swag/stringutils v0.25.4 // indirect\n\tgithub.com/go-openapi/swag/typeutils v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/yamlutils v0.25.5 // indirect\n\tgithub.com/go-openapi/validate v0.25.1 // indirect\n\tgithub.com/gofrs/flock v0.13.0 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/golang/snappy v1.0.0 // indirect\n\tgithub.com/gomlx/go-xla v0.2.0 // indirect\n\tgithub.com/google/renameio/v2 v2.0.0 // indirect\n\tgithub.com/google/s2a-go v0.1.9 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.18.0 // indirect\n\tgithub.com/gorilla/handlers v1.5.2 // indirect\n\tgithub.com/gorilla/mux v1.8.1 // indirect\n\tgithub.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect\n\tgithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect\n\tgithub.com/hashicorp/go-version v1.6.0 // indirect\n\tgithub.com/hashicorp/hcl v1.0.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/jackc/chunkreader/v2 v2.0.1 // indirect\n\tgithub.com/jackc/pgconn v1.12.0 // indirect\n\tgithub.com/jackc/pgio v1.0.0 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgproto3/v2 v2.3.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/pgtype v1.11.0 // indirect\n\tgithub.com/jackc/pgx/v4 v4.16.0 // indirect\n\tgithub.com/jinzhu/inflection v1.0.0 // indirect\n\tgithub.com/jinzhu/now v1.1.5 // indirect\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/compress v1.18.4 // indirect\n\tgithub.com/klauspost/crc32 v1.3.0 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/kr/text v0.2.0 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/mailru/easyjson v0.7.7 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.21 // indirect\n\tgithub.com/milvus-io/milvus-proto/go-api/v2 v2.4.10-0.20240819025435-512e3b98866a // indirect\n\tgithub.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect\n\tgithub.com/minio/crc64nvme v1.1.1 // indirect\n\tgithub.com/minio/md5-simd v1.1.2 // indirect\n\tgithub.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/montanaflynn/stats v0.7.1 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/ncruces/go-strftime v1.0.0 // indirect\n\tgithub.com/oklog/ulid/v2 v2.1.1 // indirect\n\tgithub.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect\n\tgithub.com/olekukonko/errors v1.2.0 // indirect\n\tgithub.com/olekukonko/ll v0.1.6 // indirect\n\tgithub.com/openzipkin/zipkin-go v0.4.3 // indirect\n\tgithub.com/pelletier/go-toml v1.9.5 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pkg/xattr v0.4.12 // indirect\n\tgithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/prometheus/client_model v0.6.2 // indirect\n\tgithub.com/prometheus/common v0.66.1 // indirect\n\tgithub.com/prometheus/procfs v0.16.1 // indirect\n\tgithub.com/redis/go-redis/extra/rediscmd/v9 v9.18.0 // indirect\n\tgithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/rs/xid v1.6.0 // indirect\n\tgithub.com/sagikazarmark/locafero v0.12.0 // indirect\n\tgithub.com/sirupsen/logrus v1.9.4 // indirect\n\tgithub.com/spf13/afero v1.15.0 // indirect\n\tgithub.com/spf13/cast v1.10.0 // indirect\n\tgithub.com/spiffe/go-spiffe/v2 v2.6.0 // indirect\n\tgithub.com/stretchr/objx v0.5.2 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/tidwall/gjson v1.14.4 // indirect\n\tgithub.com/tidwall/match v1.1.1 // indirect\n\tgithub.com/tidwall/pretty v1.2.0 // indirect\n\tgithub.com/tinylib/msgp v1.6.1 // indirect\n\tgithub.com/vearutop/statigz v1.4.0 // indirect\n\tgithub.com/wk8/go-ordered-map/v2 v2.1.8 // indirect\n\tgithub.com/x448/float16 v0.8.4 // indirect\n\tgithub.com/xdg-go/pbkdf2 v1.0.0 // indirect\n\tgithub.com/xdg-go/scram v1.2.0 // indirect\n\tgithub.com/xdg-go/stringprep v1.0.4 // indirect\n\tgithub.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect\n\tgo.opencensus.io v0.24.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/detectors/gcp v1.40.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect\n\tgo.opentelemetry.io/proto/otlp v1.9.0 // indirect\n\tgo.uber.org/multierr v1.10.0 // indirect\n\tgo.yaml.in/yaml/v2 v2.4.2 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgo.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect\n\tgolang.org/x/crypto v0.49.0 // indirect\n\tgolang.org/x/net v0.52.0 // indirect\n\tgolang.org/x/sync v0.20.0 // indirect\n\tgolang.org/x/term v0.41.0 // indirect\n\tgolang.org/x/text v0.35.0 // indirect\n\tgolang.org/x/time v0.15.0 // indirect\n\tgonum.org/v1/gonum v0.16.0 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tk8s.io/klog/v2 v2.140.0 // indirect\n\tmodernc.org/libc v1.70.0 // indirect\n\tmodernc.org/libquickjs v0.12.3 // indirect\n\tmodernc.org/memory v1.11.0 // indirect\n)\n\nreplace (\n\tgorm.io/driver/clickhouse v0.4.2 => github.com/gorse-io/clickhouse v0.3.3-0.20251121080503-d578f146896d\n\tgorm.io/driver/sqlite v1.3.4 => github.com/gorse-io/sqlite v1.3.3-0.20220713123255-c322aec4e59e\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=\ncel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=\ncloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=\ncloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=\ncloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=\ncloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=\ncloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=\ncloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=\ncloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=\ncloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=\ncloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=\ncloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA=\ncloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak=\ncloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=\ncloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=\ncloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=\ncloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub/v2 v2.4.0 h1:oMKNiBQpXImRWnHYla9uSU66ZzByZwBSCJOEs/pTKVg=\ncloud.google.com/go/pubsub/v2 v2.4.0/go.mod h1:2lS/XQKq5qtOMs6kHBK+WX1ytUC36kLl2ig3zqsGUx8=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.61.3 h1:VS//ZfBuPGDvakfD9xyPW1RGF1Vy3BWUoVZXgW1KMOg=\ncloud.google.com/go/storage v1.61.3/go.mod h1:JtqK8BBB7TWv0HVGHubtUdzYYrakOQIsMLffZ2Z/HWk=\ncloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=\ncloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=\ncodeberg.org/go-fonts/liberation v0.5.0 h1:SsKoMO1v1OZmzkG2DY+7ZkCL9U+rrWI09niOLfQ5Bo0=\ncodeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU=\ncodeberg.org/go-latex/latex v0.1.0 h1:hoGO86rIbWVyjtlDLzCqZPjNykpWQ9YuTZqAzPcfL3c=\ncodeberg.org/go-latex/latex v0.1.0/go.mod h1:LA0q/AyWIYrqVd+A9Upkgsb+IqPcmSTKc9Dny04MHMw=\ncodeberg.org/go-pdf/fpdf v0.10.0 h1:u+w669foDDx5Ds43mpiiayp40Ov6sZalgcPMDBcZRd4=\ncodeberg.org/go-pdf/fpdf v0.10.0/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\nfilippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngit.sr.ht/~sbinet/gg v0.6.0 h1:RIzgkizAk+9r7uPzf/VfbJHBMKUr0F5hRFxTUGMnt38=\ngit.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94=\ngithub.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 h1:KpMC6LFL7mqpExyMC9jVOYRiVhLmamjeZfRsUpB7l4s=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA=\ngithub.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 h1:ZJJNFaQ86GVKQ9ehwqyAFE6pIfyicpuJ8IkVaPBc6/4=\ngithub.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3/go.mod h1:URuDvhmATVKqHBH9/0nOiNKk0+YcwfQ3WkK5PqHKxc8=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=\ngithub.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=\ngithub.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=\ngithub.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=\ngithub.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=\ngithub.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=\ngithub.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=\ngithub.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=\ngithub.com/XSAM/otelsql v0.41.0 h1:uZifjQhZhv5EDYJh+IVk1DiYxQZJBlNSen0MBFnfxB8=\ngithub.com/XSAM/otelsql v0.41.0/go.mod h1:NMQT0PiKoFILp9QgjQz+D5mvW+9mT0suR7OejqrtMaM=\ngithub.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=\ngithub.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=\ngithub.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=\ngithub.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=\ngithub.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=\ngithub.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=\ngithub.com/apache/arrow/go/arrow v0.0.0-20201229220542-30ce2eb5d4dc/go.mod h1:c9sxoIT3YgLxH4UhLOCKaBlEojuMhVYpk4Ntv3opUTQ=\ngithub.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=\ngithub.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=\ngithub.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=\ngithub.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=\ngithub.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=\ngithub.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/awalterschulze/gographviz v0.0.0-20190221210632-1e9ccb565bca/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs=\ngithub.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs=\ngithub.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=\ngithub.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=\ngithub.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=\ngithub.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=\ngithub.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=\ngithub.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=\ngithub.com/bool64/dev v0.2.43 h1:yQ7qiZVef6WtCl2vDYU0Y+qSq+0aBrQzY8KXkklk9cQ=\ngithub.com/bool64/dev v0.2.43/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=\ngithub.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=\ngithub.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=\ngithub.com/c-bata/goptuna v0.8.1 h1:25+n1MLv0yvCsD56xv4nqIus3oLHL9GuPAZDLIqmX1U=\ngithub.com/c-bata/goptuna v0.8.1/go.mod h1:knmS8+Iyq5PPy1YUeIEq0pMFR4Y6x7z/CySc9HlZTCY=\ngithub.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY=\ngithub.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=\ngithub.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=\ngithub.com/chewxy/hm v1.0.0/go.mod h1:qg9YI4q6Fkj/whwHR1D+bOGeF7SniIP40VweVepLjg0=\ngithub.com/chewxy/math32 v1.0.0/go.mod h1:Miac6hA1ohdDUTagnvJy/q+aNnEk16qWUdb8ZVhvCN0=\ngithub.com/chewxy/math32 v1.0.6/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs=\ngithub.com/chewxy/math32 v1.11.1 h1:b7PGHlp8KjylDoU8RrcEsRuGZhJuz8haxnKfuMMRqy8=\ngithub.com/chewxy/math32 v1.11.1/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=\ngithub.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=\ngithub.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=\ngithub.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=\ngithub.com/cloudflare/cfssl v0.0.0-20190808011637-b1ec8c586c2a/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=\ngithub.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=\ngithub.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=\ngithub.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=\ngithub.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=\ngithub.com/cockroachdb/errors v1.9.1 h1:yFVvsI0VxmRShfawbt/laCIDy/mtTqqnvoNgiy5bEV8=\ngithub.com/cockroachdb/errors v1.9.1/go.mod h1:2sxOtL2WIc096WSZqZ5h8fa17rdDq9HZOZLBCor4mBk=\ngithub.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f h1:6jduT9Hfc0njg5jJ1DdKCFPdMBrp/mdZfCpa5h+WM74=\ngithub.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=\ngithub.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ=\ngithub.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=\ngithub.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=\ngithub.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=\ngithub.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=\ngithub.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=\ngithub.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=\ngithub.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=\ngithub.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=\ngithub.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=\ngithub.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/cznic/cc v0.0.0-20181122101902-d673e9b70d4d/go.mod h1:m3fD/V+XTB35Kh9zw6dzjMY+We0Q7PMf6LLIC4vuG9k=\ngithub.com/cznic/golex v0.0.0-20181122101858-9c343928389c/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc=\ngithub.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=\ngithub.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=\ngithub.com/cznic/xc v0.0.0-20181122101856-45b06973881e/go.mod h1:3oFoiOvCDBYH+swwf5+k/woVmWy7h1Fcyu8Qig/jjX0=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=\ngithub.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=\ngithub.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=\ngithub.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=\ngithub.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=\ngithub.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=\ngithub.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=\ngithub.com/emicklei/go-restful-openapi/v2 v2.12.0 h1:6wE0/+4tD6CpV5RI7x3ZdH2toUrB74oQmyEXBi47tHc=\ngithub.com/emicklei/go-restful-openapi/v2 v2.12.0/go.mod h1:I/b/Q1A/wpKWJGZJeO4WPaIw0ME4jXp5Yrh5hdB1bBA=\ngithub.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=\ngithub.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=\ngithub.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=\ngithub.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=\ngithub.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=\ngithub.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=\ngithub.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=\ngithub.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=\ngithub.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=\ngithub.com/erraggy/oastools v1.36.1 h1:mNxGZO1w7LCFmZhar9QPOhLFpchE0T9TzNwqa+rENN4=\ngithub.com/erraggy/oastools v1.36.1/go.mod h1:KVEs42aJN9Z+H9YpxqjGrXJRQdrp1W0aJ4w2R+UId+I=\ngithub.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=\ngithub.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM=\ngithub.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=\ngithub.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/fsouza/fake-gcs-server v1.54.0 h1:DGO4EkFVbtP/A5Ha+CAHHx+Xa6O6LeskMB4hQ1wBE48=\ngithub.com/fsouza/fake-gcs-server v1.54.0/go.mod h1:ryXYE4debQs8GjOxwaOAwFRwM4Cvs6S+NKPPgdVJe6g=\ngithub.com/fxtlabs/primes v0.0.0-20150821004651-dad82d10a449 h1:HOYnhuVrhAVGKdg3rZapII640so7QfXQmkLkefUN/uM=\ngithub.com/fxtlabs/primes v0.0.0-20150821004651-dad82d10a449/go.mod h1:i+vbdOOivRRh2j+WwBkjZXloGN/+KAqfKDwNfUJeugc=\ngithub.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=\ngithub.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=\ngithub.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=\ngithub.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c=\ngithub.com/getsentry/sentry-go v0.30.0 h1:lWUwDnY7sKHaVIoZ9wYqRHJ5iEmoc0pqcRqFkosKzBo=\ngithub.com/getsentry/sentry-go v0.30.0/go.mod h1:WU9B9/1/sHDqeV8T+3VwwbjeR5MSXs/6aqG3mqZrezA=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=\ngithub.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=\ngithub.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=\ngithub.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=\ngithub.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=\ngithub.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=\ngithub.com/go-faker/faker/v4 v4.1.0 h1:ffuWmpDrducIUOO0QSKSF5Q2dxAht+dhsT9FvVHhPEI=\ngithub.com/go-faker/faker/v4 v4.1.0/go.mod h1:uuNc0PSRxF8nMgjGrrrU4Nw5cF30Jc6Kd0/FUTTYbhg=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gota/gota v0.10.1/go.mod h1:NZLQccXn0rABmkXjsaugRY6l+UH2dDZSgIgF8E2ipmA=\ngithub.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=\ngithub.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=\ngithub.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=\ngithub.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=\ngithub.com/go-openapi/analysis v0.24.1 h1:Xp+7Yn/KOnVWYG8d+hPksOYnCYImE3TieBa7rBOesYM=\ngithub.com/go-openapi/analysis v0.24.1/go.mod h1:dU+qxX7QGU1rl7IYhBC8bIfmWQdX4Buoea4TGtxXY84=\ngithub.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA=\ngithub.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w=\ngithub.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=\ngithub.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=\ngithub.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=\ngithub.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=\ngithub.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4=\ngithub.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY=\ngithub.com/go-openapi/spec v0.22.2 h1:KEU4Fb+Lp1qg0V4MxrSCPv403ZjBl8Lx1a83gIPU8Qc=\ngithub.com/go-openapi/spec v0.22.2/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=\ngithub.com/go-openapi/strfmt v0.26.1 h1:7zGCHji7zSYDC2tCXIusoxYQz/48jAf2q+sF6wXTG+c=\ngithub.com/go-openapi/strfmt v0.26.1/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y=\ngithub.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=\ngithub.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=\ngithub.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=\ngithub.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=\ngithub.com/go-openapi/swag/fileutils v0.25.1 h1:rSRXapjQequt7kqalKXdcpIegIShhTPXx7yw0kek2uU=\ngithub.com/go-openapi/swag/fileutils v0.25.1/go.mod h1:+NXtt5xNZZqmpIpjqcujqojGFek9/w55b3ecmOdtg8M=\ngithub.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=\ngithub.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=\ngithub.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=\ngithub.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=\ngithub.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=\ngithub.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=\ngithub.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=\ngithub.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=\ngithub.com/go-openapi/swag/mangling v0.25.1 h1:XzILnLzhZPZNtmxKaz/2xIGPQsBsvmCjrJOWGNz/ync=\ngithub.com/go-openapi/swag/mangling v0.25.1/go.mod h1:CdiMQ6pnfAgyQGSOIYnZkXvqhnnwOn997uXZMAd/7mQ=\ngithub.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=\ngithub.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=\ngithub.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=\ngithub.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=\ngithub.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=\ngithub.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=\ngithub.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag=\ngithub.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM=\ngithub.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw=\ngithub.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=\ngithub.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw=\ngithub.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=\ngithub.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=\ngithub.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=\ngithub.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=\ngithub.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=\ngithub.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=\ngithub.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=\ngithub.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=\ngithub.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=\ngithub.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=\ngithub.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=\ngithub.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=\ngithub.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=\ngithub.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=\ngithub.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=\ngithub.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=\ngithub.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=\ngithub.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM=\ngithub.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=\ngithub.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=\ngithub.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=\ngithub.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=\ngithub.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=\ngithub.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=\ngithub.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=\ngithub.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/gomlx/go-xla v0.2.0 h1:vPRgGjKUaN4Pq58ZswWtiKNGUkqcbUj95YmvELZNvTA=\ngithub.com/gomlx/go-xla v0.2.0/go.mod h1:T2CsL/E90te3k4qpuzlXv2uQU2FmLMLfUsRlAGqKSuI=\ngithub.com/gomlx/gomlx v0.27.1 h1:WhWop7VzKWOgl7C0yDfvJ0kXiaNAWjw5XvWME5cP6Zg=\ngithub.com/gomlx/gomlx v0.27.1/go.mod h1:9fOMnTb7YMs/6zYVR6diliq25x9oSjMUPMlHAbcJWd4=\ngithub.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=\ngithub.com/gonum/blas v0.0.0-20181208220705-f22b278b28ac/go.mod h1:P32wAyui1PQ58Oce/KYkOqQv8cVw1zAapXOl+dRFGbc=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/flatbuffers v1.10.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=\ngithub.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=\ngithub.com/google/flatbuffers v1.12.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=\ngithub.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=\ngithub.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=\ngithub.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=\ngithub.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI=\ngithub.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE=\ngithub.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gorgonia/bindgen v0.0.0-20180812032444-09626750019e/go.mod h1:YzKk63P9jQHkwAo2rXHBv02yPxDzoQT2cBV0x5bGV/8=\ngithub.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=\ngithub.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=\ngithub.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=\ngithub.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=\ngithub.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=\ngithub.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=\ngithub.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=\ngithub.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/gorse-io/clickhouse v0.3.3-0.20251121080503-d578f146896d h1:s6U3e1W7NdviEq12zOisra1cAaG9+fkL6jnMK8sok/Y=\ngithub.com/gorse-io/clickhouse v0.3.3-0.20251121080503-d578f146896d/go.mod h1:iILWzbul8U+gsf4kqbheF2QzBmdvVp63mloGGK8emDI=\ngithub.com/gorse-io/dashboard v0.0.0-20260223101641-33715448ded8 h1:IjZK+VF93JvEAZEFsOa8qwVKBfb4dXuCF+m7Qb2Yr5c=\ngithub.com/gorse-io/dashboard v0.0.0-20260223101641-33715448ded8/go.mod h1:Ako5pxQlNyCiJpLpcJAwYWMF5nXdYP0mgpGcEOaw0vQ=\ngithub.com/gorse-io/gorse-go v0.5.0-alpha.3 h1:GR/OWzq016VyFyTTxgQWeayGahRVzB1cGFIW/AaShC4=\ngithub.com/gorse-io/gorse-go v0.5.0-alpha.3/go.mod h1:ZxmVHzZPKm5pmEIlqaRDwK0rkfTRHlfziO033XZ+RW0=\ngithub.com/gorse-io/sqlite v1.3.3-0.20220713123255-c322aec4e59e h1:uPQtYQzG1QcC3Qbv+tuEe8Q2l++V4KEcqYSSwB9qobg=\ngithub.com/gorse-io/sqlite v1.3.3-0.20220713123255-c322aec4e59e/go.mod h1:PmIOwYnI+F1lRKd6F/PdLXGgI8GZ5H8x8z1yx0+0bmQ=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=\ngithub.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=\ngithub.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=\ngithub.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=\ngithub.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=\ngithub.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=\ngithub.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=\ngithub.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=\ngithub.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=\ngithub.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE=\ngithub.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=\ngithub.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=\ngithub.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=\ngithub.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=\ngithub.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk=\ngithub.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g=\ngithub.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=\ngithub.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=\ngithub.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=\ngithub.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=\ngithub.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=\ngithub.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=\ngithub.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=\ngithub.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=\ngithub.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=\ngithub.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=\ngithub.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=\ngithub.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=\ngithub.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=\ngithub.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=\ngithub.com/jackc/pgconn v1.12.0 h1:/RvQ24k3TnNdfBSW0ou9EOi5jx2cX7zfE8n2nLKuiP0=\ngithub.com/jackc/pgconn v1.12.0/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono=\ngithub.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=\ngithub.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=\ngithub.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=\ngithub.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=\ngithub.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=\ngithub.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=\ngithub.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=\ngithub.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=\ngithub.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=\ngithub.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=\ngithub.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=\ngithub.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=\ngithub.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=\ngithub.com/jackc/pgproto3/v2 v2.3.0 h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL8Y=\ngithub.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=\ngithub.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=\ngithub.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=\ngithub.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=\ngithub.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=\ngithub.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=\ngithub.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=\ngithub.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=\ngithub.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=\ngithub.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=\ngithub.com/jackc/pgtype v1.11.0 h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs=\ngithub.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=\ngithub.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=\ngithub.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=\ngithub.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=\ngithub.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=\ngithub.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=\ngithub.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=\ngithub.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA=\ngithub.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=\ngithub.com/jackc/pgx/v4 v4.16.0 h1:4k1tROTJctHotannFYzu77dY3bgtMRymQP7tXQjqpPk=\ngithub.com/jackc/pgx/v4 v4.16.0/go.mod h1:N0A9sFdWzkw/Jy1lwoiB64F2+ugFZi987zRxcPez/wI=\ngithub.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/janpfeifer/go-benchmarks v0.1.1 h1:gLLy07/JrOKSnMWeUxSnjTdhkglgmrNR2IBDnR4kRqw=\ngithub.com/janpfeifer/go-benchmarks v0.1.1/go.mod h1:5AagXCOUzevvmYFQalcgoa4oWPyH1IkZNckolGWfiSM=\ngithub.com/jaswdr/faker v1.19.1 h1:xBoz8/O6r0QAR8eEvKJZMdofxiRH+F0M/7MU9eNKhsM=\ngithub.com/jaswdr/faker v1.19.1/go.mod h1:x7ZlyB1AZqwqKZgyQlnqEG8FDptmHlncA5u2zY/yi6w=\ngithub.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY=\ngithub.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4=\ngithub.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=\ngithub.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=\ngithub.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=\ngithub.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=\ngithub.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=\ngithub.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM=\ngithub.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8=\ngithub.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=\ngithub.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=\ngithub.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=\ngithub.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8=\ngithub.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE=\ngithub.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=\ngithub.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=\ngithub.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=\ngithub.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=\ngithub.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=\ngithub.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=\ngithub.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=\ngithub.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=\ngithub.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=\ngithub.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=\ngithub.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=\ngithub.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=\ngithub.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y=\ngithub.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=\ngithub.com/lafikl/consistent v0.0.0-20220512074542-bdd3606bfc3e h1:DuhzIzxOx3aJ0j4enY7SQ9bvulrT/XjkGAqiychfavc=\ngithub.com/lafikl/consistent v0.0.0-20220512074542-bdd3606bfc3e/go.mod h1:JmowInJuqa6EpSut8NSMAZtlvK9uL+8Q1P7tyew5rQY=\ngithub.com/leesper/go_rng v0.0.0-20171009123644-5344a9259b21/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U=\ngithub.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U=\ngithub.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=\ngithub.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=\ngithub.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=\ngithub.com/madflojo/testcerts v1.5.0 h1:GhQllyAiGzXVZU+i8O/cQkPTHzN59RxMGtm3uETgXnU=\ngithub.com/madflojo/testcerts v1.5.0/go.mod h1:MW8sh39gLnkKh4K0Nc55AyHEDl9l/FBLDUsQhpmkuo0=\ngithub.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/mailru/go-clickhouse/v2 v2.5.1 h1:k+YfKvUrTOHngWNBEmsTs0KAaS1L4paEe6c8IYOVqa8=\ngithub.com/mailru/go-clickhouse/v2 v2.5.1/go.mod h1:mJ/E4F05qQolb98/uFHWwFwgiO9NWss2DzZkhjV+jgo=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=\ngithub.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=\ngithub.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=\ngithub.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=\ngithub.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=\ngithub.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=\ngithub.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=\ngithub.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=\ngithub.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=\ngithub.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=\ngithub.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=\ngithub.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=\ngithub.com/milvus-io/milvus-proto/go-api/v2 v2.4.10-0.20240819025435-512e3b98866a h1:0B/8Fo66D8Aa23Il0yrQvg1KKz92tE/BJ5BvkUxxAAk=\ngithub.com/milvus-io/milvus-proto/go-api/v2 v2.4.10-0.20240819025435-512e3b98866a/go.mod h1:1OIl0v5PQeNxIJhCvY+K55CBUOYDZevw9g9380u1Wek=\ngithub.com/milvus-io/milvus-sdk-go/v2 v2.4.2 h1:Xqf+S7iicElwYoS2Zly8Nf/zKHuZsNy1xQajfdtygVY=\ngithub.com/milvus-io/milvus-sdk-go/v2 v2.4.2/go.mod h1:ulO1YUXKH0PGg50q27grw048GDY9ayB4FPmh7D+FFTA=\ngithub.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g=\ngithub.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ=\ngithub.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=\ngithub.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=\ngithub.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=\ngithub.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=\ngithub.com/minio/minio-go/v7 v7.0.99 h1:2vH/byrwUkIpFQFOilvTfaUpvAX3fEFhEzO+DR3DlCE=\ngithub.com/minio/minio-go/v7 v7.0.99/go.mod h1:EtGNKtlX20iL2yaYnxEigaIvj0G0GwSDnifnG8ClIdw=\ngithub.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=\ngithub.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=\ngithub.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=\ngithub.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=\ngithub.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=\ngithub.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=\ngithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=\ngithub.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=\ngithub.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=\ngithub.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=\ngithub.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=\ngithub.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=\ngithub.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=\ngithub.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=\ngithub.com/nikolalohinski/gonja/v2 v2.7.0 h1:XuwnulQVPwzGaM0J/9AaQv0AFPBAxKI1GILifQ1r9pk=\ngithub.com/nikolalohinski/gonja/v2 v2.7.0/go.mod h1:UIzXPVuOsr5h7dZ5DUbqk3/Z7oFA/NLGQGMjqT4L2aU=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=\ngithub.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=\ngithub.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=\ngithub.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=\ngithub.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=\ngithub.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=\ngithub.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=\ngithub.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=\ngithub.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=\ngithub.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=\ngithub.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=\ngithub.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=\ngithub.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=\ngithub.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=\ngithub.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=\ngithub.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=\ngithub.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=\ngithub.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=\ngithub.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=\ngithub.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=\ngithub.com/pelletier/go-toml v1.9.2/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=\ngithub.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=\ngithub.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=\ngithub.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=\ngithub.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=\ngithub.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/xattr v0.4.12 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM=\ngithub.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=\ngithub.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=\ngithub.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=\ngithub.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=\ngithub.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=\ngithub.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=\ngithub.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=\ngithub.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=\ngithub.com/qdrant/go-client v1.17.1 h1:7QmPwDddrHL3hC4NfycwtQlraVKRLcRi++BX6TTm+3g=\ngithub.com/qdrant/go-client v1.17.1/go.mod h1:n1h6GhkdAzcohoXt/5Z19I2yxbCkMA6Jejob3S6NZT8=\ngithub.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc=\ngithub.com/rakyll/statik v0.1.8 h1:Fe7egWVZbW/2vlPUY8P/aL9o6qbtrBn71uIztkdafMU=\ngithub.com/rakyll/statik v0.1.8/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc=\ngithub.com/redis/go-redis/extra/rediscmd/v9 v9.18.0 h1:QY4nmPHLFAJjtT5O4OMUEOxP8WVaRNOFpcbmxT2NLZU=\ngithub.com/redis/go-redis/extra/rediscmd/v9 v9.18.0/go.mod h1:WH8cY/0fT41Bsf341qzo8v4nx0GCE8FykAA23IVbVmo=\ngithub.com/redis/go-redis/extra/redisotel/v9 v9.18.0 h1:2dKdoEYBJ0CZCLPiCdvvc7luz3DPwY6hKdzjL6m1eHE=\ngithub.com/redis/go-redis/extra/redisotel/v9 v9.18.0/go.mod h1:WzkrVG9ro9BwCQD0eJOWn6AGL4Z1CleGflM45w1hu10=\ngithub.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=\ngithub.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=\ngithub.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=\ngithub.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=\ngithub.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=\ngithub.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=\ngithub.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=\ngithub.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=\ngithub.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=\ngithub.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=\ngithub.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=\ngithub.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=\ngithub.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM=\ngithub.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=\ngithub.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=\ngithub.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=\ngithub.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=\ngithub.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=\ngithub.com/sclevine/yj v0.0.0-20210612025309-737bdf40a5d1 h1:2cp8mZ+gRxIx/5GoLcQmmXzJuKdM1cSE2dmvFn3udJE=\ngithub.com/sclevine/yj v0.0.0-20210612025309-737bdf40a5d1/go.mod h1:PkEqqiiBYB87KgvpQj2r0wtRjDKEhhLRarGCubegp7E=\ngithub.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=\ngithub.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=\ngithub.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=\ngithub.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=\ngithub.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=\ngithub.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=\ngithub.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=\ngithub.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=\ngithub.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=\ngithub.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=\ngithub.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=\ngithub.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=\ngithub.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=\ngithub.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=\ngithub.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=\ngithub.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=\ngithub.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=\ngithub.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=\ngithub.com/steinfletcher/apitest v1.6.0 h1:BvZpQh0DECrDq7nFzDjwQqwXAEc+cykuVD4aUdXvQfA=\ngithub.com/steinfletcher/apitest v1.6.0/go.mod h1:mF+KnYaIkuHM0C4JgGzkIIOJAEjo+EA5tTjJ+bHXnQc=\ngithub.com/streadway/quantile v0.0.0-20220407130108-4246515d968d h1:X4+kt6zM/OVO6gbJdAfJR60MGPsqCzbtXNnjoGqdfAs=\ngithub.com/streadway/quantile v0.0.0-20220407130108-4246515d968d/go.mod h1:lbP8tGiBjZ5YWIc2fzuRpTaz0b/53vT6PEs3QuAWzuU=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/swaggest/swgui v1.8.5 h1:nceK5OJcpXpkfjmPNH6wtubbd8ZYwxy043xmx0SK18g=\ngithub.com/swaggest/swgui v1.8.5/go.mod h1:kvSzLC7+wK4l9n/YcQlb2AMeQtkno9i3C6imADv/fLQ=\ngithub.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=\ngithub.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=\ngithub.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=\ngithub.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tiktoken-go/tokenizer v0.7.0 h1:VMu6MPT0bXFDHr7UPh9uii7CNItVt3X9K90omxL54vw=\ngithub.com/tiktoken-go/tokenizer v0.7.0/go.mod h1:6UCYI/DtOallbmL7sSy30p6YQv60qNyU/4aVigPOx6w=\ngithub.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=\ngithub.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=\ngithub.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=\ngithub.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=\ngithub.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=\ngithub.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=\ngithub.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=\ngithub.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=\ngithub.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=\ngithub.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=\ngithub.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=\ngithub.com/vearutop/statigz v1.4.0 h1:RQL0KG3j/uyA/PFpHeZ/L6l2ta920/MxlOAIGEOuwmU=\ngithub.com/vearutop/statigz v1.4.0/go.mod h1:LYTolBLiz9oJISwiVKnOQoIwhO1LWX1A7OECawGS8XE=\ngithub.com/weaviate/weaviate v1.27.0 h1:ovFnKER+HRpT5PPuR1ysbKgit0NSpHbBLcsjWR1UyWI=\ngithub.com/weaviate/weaviate v1.27.0/go.mod h1:ppTWDzt/atYk1KhyYzxVD8XckmaCaOYnnmelD5M4LK4=\ngithub.com/weaviate/weaviate-go-client/v4 v4.16.1 h1:jkDYuRCYly6zG2ngqTpv6z8azzbqiMUXcmaJHJmAV0Q=\ngithub.com/weaviate/weaviate-go-client/v4 v4.16.1/go.mod h1:XmoRpzNpWrTW5/TE07dUtxy5kMZbG3uAG/3b69nuwFk=\ngithub.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=\ngithub.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=\ngithub.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=\ngithub.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=\ngithub.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=\ngithub.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=\ngithub.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=\ngithub.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=\ngithub.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=\ngithub.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=\ngithub.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=\ngithub.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=\ngithub.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=\ngithub.com/xtgo/set v1.0.0/go.mod h1:d3NHzGzSa0NmB2NhFyECA+QdRp29oEn2xbT+TpeFoM8=\ngithub.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=\ngithub.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=\ngithub.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=\ngithub.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=\ngithub.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=\ngithub.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=\ngithub.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=\ngithub.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=\ngithub.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=\ngithub.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=\ngo.einride.tech/aip v0.79.0 h1:19zdPlZzlUvxOA8syAFw4LkdJdXepzyTl6gt9XEeqdU=\ngo.einride.tech/aip v0.79.0/go.mod h1:E8+wdTApA70odnpFzJgsGogHozC2JCIhFJBKPr8bVig=\ngo.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=\ngo.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=\ngo.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/detectors/gcp v1.40.0 h1:Awaf8gmW99tZTOWqkLCOl6aw1/rxAWVlHsHIZ3fT2sA=\ngo.opentelemetry.io/contrib/detectors/gcp v1.40.0/go.mod h1:99OY9ZCqyLkzJLTh5XhECpLRSxcZl+ZDKBEO+jMBFR4=\ngo.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful v0.67.0 h1:0ONi/nCM00kjql6HSU2kVs6nYFx3l0efqDrR1qZ03/A=\ngo.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful v0.67.0/go.mod h1:2vCOOy1Om03w7th7aDKpgjJ3z0/K80RgIJwkORo5ab4=\ngo.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo v0.67.0 h1:X0H+vhyjOczVijlJqIz2kqq0H3O/y3iwKgAKYTII7yU=\ngo.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo v0.67.0/go.mod h1:hg41UE3tzwcEqMZD7xT4EjISCFFS5fgPbBsW37twGNs=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=\ngo.opentelemetry.io/contrib/propagators/b3 v1.42.0 h1:B2Pew5ufEtgkjLF+tSkXjgYZXQr9m7aCm1wLKB0URbU=\ngo.opentelemetry.io/contrib/propagators/b3 v1.42.0/go.mod h1:iPgUcSEF5DORW6+yNbdw/YevUy+QqJ508ncjhrRSCjc=\ngo.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=\ngo.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=\ngo.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw=\ngo.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs=\ngo.opentelemetry.io/otel/exporters/zipkin v1.42.0 h1:Z7ARHF7193vyVltPYcmuhSKPLf8dP5rtJZLtTQnbMH4=\ngo.opentelemetry.io/otel/exporters/zipkin v1.42.0/go.mod h1:DW09+gaEg5kydlb9g8kp4Nos3yqo9YSA1uHXkeJihXc=\ngo.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=\ngo.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=\ngo.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=\ngo.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=\ngo.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=\ngo.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=\ngo.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=\ngo.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=\ngo.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=\ngo.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=\ngo.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=\ngo.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngo.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=\ngo.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=\ngo.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=\ngo.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=\ngo.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=\ngo.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=\ngo.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=\ngo.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=\ngo.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=\ngo.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=\ngo.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=\ngo.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngo.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=\ngo.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=\ngo4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=\ngolang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=\ngolang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=\ngolang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=\ngolang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=\ngolang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=\ngolang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=\ngolang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=\ngolang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=\ngolang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=\ngolang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190226215855-775f8194d0f9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=\ngolang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=\ngolang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=\ngolang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=\ngolang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=\ngolang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=\ngonum.org/v1/gonum v0.0.0-20190226202314-149afe6ec0b6/go.mod h1:jevfED4GnIEnJrWW55YmY9DMhajHcnkqVnEXmEtMyNI=\ngonum.org/v1/gonum v0.0.0-20190902003836-43865b531bee/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=\ngonum.org/v1/gonum v0.8.1-0.20200930085651-eea0b5cb5cc9/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=\ngonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngonum.org/v1/netlib v0.0.0-20190221094214-0632e2ebbd2d/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=\ngonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=\ngonum.org/v1/netlib v0.0.0-20201012070519-2390d26c3658/go.mod h1:zQa7n16lh3Z6FbSTYgjG+KNhz1bA/b9t3plFEaGMp+A=\ngonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=\ngonum.org/v1/plot v0.15.2 h1:Tlfh/jBk2tqjLZ4/P8ZIwGrLEWQSPDLRm/SNWKNXiGI=\ngonum.org/v1/plot v0.15.2/go.mod h1:DX+x+DWso3LTha+AdkJEv5Txvi+Tql3KAGkehP0/Ubg=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA=\ngoogle.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200911024640-645f7a48b24f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=\ngoogle.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d h1:vsOm753cOAMkt76efriTCDKjpCbK18XGHMJHo0JUKhc=\ngoogle.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:0oz9d7g9QLSdv9/lgbIjowW1JoxMbxmBVNe8i6tORJI=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=\ngoogle.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=\ngoogle.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v0.0.0-20200910201057-6591123024b3/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=\ngoogle.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6 h1:ExN12ndbJ608cboPYflpTny6mXSzPrDLh0iTaVrRrds=\ngoogle.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6/go.mod h1:6ytKWczdvnpnO+m+JiG9NjEDzR1FJfsnmJdG7B8QVZ8=\ngoogle.golang.org/grpc/security/advancedtls v1.0.0 h1:/KQ7VP/1bs53/aopk9QhuPyFAp9Dm9Ejix3lzYkCrDA=\ngoogle.golang.org/grpc/security/advancedtls v1.0.0/go.mod h1:o+s4go+e1PJ2AjuQMY5hU82W7lDlefjJA6FqEHRVHWk=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=\ngopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=\ngopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=\ngopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=\ngopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=\ngopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=\ngopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngorgonia.org/cu v0.9.0-beta/go.mod h1:RPEPIfaxxqUmeRe7T1T8a0NER+KxBI2McoLEXhP1Vd8=\ngorgonia.org/cu v0.9.3/go.mod h1:LgyAYDkN7HWhh8orGnCY2R8pP9PYbO44ivEbLMatkVU=\ngorgonia.org/dawson v1.1.0/go.mod h1:Px1mcziba8YUBIDsbzGwbKJ11uIblv/zkln4jNrZ9Ws=\ngorgonia.org/dawson v1.2.0/go.mod h1:Px1mcziba8YUBIDsbzGwbKJ11uIblv/zkln4jNrZ9Ws=\ngorgonia.org/gorgonia v0.9.2/go.mod h1:ZtOb9f/wM2OMta1ISGspQ4roGDgz9d9dKOaPNvGR+ec=\ngorgonia.org/gorgonia v0.9.16/go.mod h1:EnZtUbxgbqMx8eCTGPq8C0RfBlr/WllVtMyAFUYG+b4=\ngorgonia.org/tensor v0.9.0-beta/go.mod h1:05Y4laKuVlj4qFoZIZW1q/9n1jZkgDBOLmKXZdBLG1w=\ngorgonia.org/tensor v0.9.16/go.mod h1:75SMdLLhZ+2oB0/EE8lFEIt1Caoykdd4bz1mAe59deg=\ngorgonia.org/tensor v0.9.19/go.mod h1:75SMdLLhZ+2oB0/EE8lFEIt1Caoykdd4bz1mAe59deg=\ngorgonia.org/vecf32 v0.7.0/go.mod h1:iHG+kvTMqGYA0SgahfO2k62WRnxmHsqAREGbayRDzy8=\ngorgonia.org/vecf32 v0.9.0/go.mod h1:NCc+5D2oxddRL11hd+pCB1PEyXWOyiQxfZ/1wwhOXCA=\ngorgonia.org/vecf64 v0.7.0/go.mod h1:1y4pmcSd+wh3phG+InwWQjYrqwyrtN9h27WLFVQfV1Q=\ngorgonia.org/vecf64 v0.9.0/go.mod h1:hp7IOWCnRiVQKON73kkC/AUMtEXyf9kGlVrtPQ9ccVA=\ngorm.io/driver/mysql v1.0.3/go.mod h1:twGxftLBlFgNVNakL7F+P/x9oYqoymG3YYT8cAfI9oI=\ngorm.io/driver/mysql v1.3.4 h1:/KoBMgsUHC3bExsekDcmNYaBnfH2WNeFuXqqrqMc98Q=\ngorm.io/driver/mysql v1.3.4/go.mod h1:s4Tq0KmD0yhPGHbZEwg1VPlH0vT/GBHJZorPzhcxBUE=\ngorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg=\ngorm.io/driver/postgres v1.3.5 h1:oVLmefGqBTlgeEVG6LKnH6krOlo4TZ3Q/jIK21KUMlw=\ngorm.io/driver/postgres v1.3.5/go.mod h1:EGCWefLFQSVFrHGy4J8EtiHCWX5Q8t0yz2Jt9aKkGzU=\ngorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=\ngorm.io/gorm v1.20.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=\ngorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=\ngorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=\ngorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=\ngorm.io/gorm v1.23.6 h1:KFLdNgri4ExFFGTRGGFWON2P1ZN28+9SJRN8voOoYe0=\ngorm.io/gorm v1.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nk8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=\nk8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=\nlukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=\nmodernc.org/cc v1.0.0 h1:nPibNuDEx6tvYrUAtvDTTw98rx5juGsa5zuDnKwEEQQ=\nmodernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw=\nmodernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=\nmodernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=\nmodernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=\nmodernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc=\nmodernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=\nmodernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=\nmodernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=\nmodernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=\nmodernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=\nmodernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=\nmodernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=\nmodernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=\nmodernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=\nmodernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=\nmodernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=\nmodernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=\nmodernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=\nmodernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=\nmodernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=\nmodernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=\nmodernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=\nmodernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A=\nmodernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU=\nmodernc.org/libc v1.16.7/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=\nmodernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=\nmodernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=\nmodernc.org/libquickjs v0.12.3 h1:2IU9B6njBmce2PuYttJDkXeoLRV9WnvgP+eU5HAC8YI=\nmodernc.org/libquickjs v0.12.3/go.mod h1:iCsgVxnHTX3i0YPxxHBmJk0GLA5sVUHXWI/090UXgeE=\nmodernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=\nmodernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=\nmodernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=\nmodernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=\nmodernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=\nmodernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=\nmodernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=\nmodernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=\nmodernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=\nmodernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=\nmodernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=\nmodernc.org/quickjs v0.17.1 h1:CbYnbTf7ksZk9YZ1rRM2Ab1Zfi+X6s50kXiOhpd2NIg=\nmodernc.org/quickjs v0.17.1/go.mod h1:hATT7DIJc33I5Q/Fjffhm0tpUHNSqdKHma/ossibTA0=\nmodernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=\nmodernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=\nmodernc.org/sqlite v1.17.3/go.mod h1:10hPVYar9C0kfXuTWGz8s0XtB8uAGymUy51ZzStYe3k=\nmodernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk=\nmodernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=\nmodernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=\nmodernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=\nmodernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=\nmodernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=\nmodernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=\nmodernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=\nmodernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=\nmodernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=\nmodernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I=\nmodernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=\n"
  },
  {
    "path": "logics/cf.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage logics\n\nimport (\n\t\"io\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/common/ann\"\n\t\"github.com/gorse-io/gorse/common/encoding\"\n\t\"github.com/gorse-io/gorse/common/floats\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/samber/lo\"\n\t\"go.uber.org/zap\"\n)\n\nfunc distance(a, b []float32) float32 {\n\treturn -floats.Dot(a, b)\n}\n\ntype MatrixFactorizationItems struct {\n\ttimestamp time.Time\n\titems     []string\n\titemsLock sync.Mutex\n\tindex     *ann.HNSW[[]float32]\n\tdimension int\n}\n\nfunc NewMatrixFactorizationItems(timestamp time.Time) *MatrixFactorizationItems {\n\treturn &MatrixFactorizationItems{\n\t\ttimestamp: timestamp,\n\t\titems:     make([]string, 0),\n\t\tindex:     ann.NewHNSW(distance),\n\t}\n}\n\nfunc (items *MatrixFactorizationItems) Add(itemId string, v []float32) {\n\t// Check dimension\n\titems.itemsLock.Lock()\n\tif items.dimension == 0 {\n\t\titems.dimension = len(v)\n\t} else if items.dimension != len(v) {\n\t\tlog.Logger().Error(\"dimension mismatch\", zap.Int(\"dimension\", len(v)))\n\t\treturn\n\t}\n\t// Push item\n\titems.items = append(items.items, \"\")\n\titems.itemsLock.Unlock()\n\tj := items.index.Add(v)\n\titems.itemsLock.Lock()\n\titems.items[j] = itemId\n\titems.itemsLock.Unlock()\n}\n\nfunc (items *MatrixFactorizationItems) Search(v []float32, n int) []cache.Score {\n\tscores := items.index.SearchVector(v, n, false)\n\treturn lo.Map(scores, func(v lo.Tuple2[int, float32], _ int) cache.Score {\n\t\treturn cache.Score{\n\t\t\tId:        items.items[v.A],\n\t\t\tScore:     -float64(v.B),\n\t\t\tTimestamp: items.timestamp,\n\t\t}\n\t})\n}\n\nfunc (items *MatrixFactorizationItems) Marshal(w io.Writer) error {\n\tif err := encoding.WriteGob(w, items.timestamp); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tif err := encoding.WriteGob(w, items.dimension); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tif err := items.index.Marshal(w); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tnumItems := int64(len(items.items))\n\tif err := encoding.WriteGob(w, numItems); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tfor _, item := range items.items {\n\t\tif err := encoding.WriteGob(w, item); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (items *MatrixFactorizationItems) Unmarshal(r io.Reader) error {\n\tif err := encoding.ReadGob(r, &items.timestamp); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tif err := encoding.ReadGob(r, &items.dimension); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tif err := items.index.Unmarshal(r); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tvar numItems int64\n\tif err := encoding.ReadGob(r, &numItems); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\titems.items = make([]string, numItems)\n\tfor i := int64(0); i < numItems; i++ {\n\t\tif err := encoding.ReadGob(r, &items.items[i]); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t}\n\treturn nil\n}\n\ntype MatrixFactorizationUsers struct {\n\tembeddings map[string][]float32\n}\n\nfunc NewMatrixFactorizationUsers() *MatrixFactorizationUsers {\n\treturn &MatrixFactorizationUsers{\n\t\tembeddings: make(map[string][]float32),\n\t}\n}\n\nfunc (users *MatrixFactorizationUsers) Add(userId string, v []float32) {\n\tusers.embeddings[userId] = v\n}\n\nfunc (users *MatrixFactorizationUsers) Get(userId string) ([]float32, bool) {\n\tv, ok := users.embeddings[userId]\n\treturn v, ok\n}\n\nfunc (users *MatrixFactorizationUsers) Marshal(w io.Writer) error {\n\tnumUsers := int64(len(users.embeddings))\n\tif err := encoding.WriteGob(w, numUsers); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tfor userId, embedding := range users.embeddings {\n\t\tif err := encoding.WriteString(w, userId); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t\tif err := encoding.WriteSlice(w, embedding); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (users *MatrixFactorizationUsers) Unmarshal(r io.Reader) error {\n\tvar numUsers int64\n\tif err := encoding.ReadGob(r, &numUsers); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tusers.embeddings = make(map[string][]float32, numUsers)\n\tfor i := int64(0); i < numUsers; i++ {\n\t\tuserId, err := encoding.ReadString(r)\n\t\tif err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t\tvar embedding []float32\n\t\tif err = encoding.ReadSlice(r, &embedding); err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t\tusers.embeddings[userId] = embedding\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "logics/cf_test.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage logics\n\nimport (\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestMatrixFactorizationItems(t *testing.T) {\n\tts := time.Now()\n\titems := NewMatrixFactorizationItems(ts)\n\titems.Add(\"1\", []float32{1, 1, 1})\n\titems.Add(\"2\", []float32{2, 2, 2})\n\titems.Add(\"3\", []float32{3, 3, 3})\n\titems.Add(\"4\", []float32{4, 4, 4})\n\titems.Add(\"5\", []float32{5, 5, 5})\n\n\tpath := filepath.Join(t.TempDir(), \"items\")\n\tf, err := os.Create(path)\n\tassert.NoError(t, err)\n\tdefer f.Close()\n\terr = items.Marshal(f)\n\tassert.NoError(t, err)\n\n\tf, err = os.Open(path)\n\tassert.NoError(t, err)\n\tdefer f.Close()\n\titems2 := NewMatrixFactorizationItems(time.Time{})\n\terr = items2.Unmarshal(f)\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, items.timestamp.UnixNano(), items2.timestamp.UnixNano())\n\tassert.Equal(t, items.dimension, items2.dimension)\n\tassert.Equal(t, items.items, items2.items)\n\tscores := items2.Search([]float32{1, 1, 1}, 3)\n\tassert.Equal(t, []cache.Score{\n\t\t{Id: \"5\", Score: 15, Timestamp: items2.timestamp},\n\t\t{Id: \"4\", Score: 12, Timestamp: items2.timestamp},\n\t\t{Id: \"3\", Score: 9, Timestamp: items2.timestamp},\n\t}, scores)\n}\n\nfunc TestNewMatrixFactorizationUsers(t *testing.T) {\n\tusers := NewMatrixFactorizationUsers()\n\tusers.Add(\"1\", []float32{1, 1, 1})\n\tusers.Add(\"2\", []float32{2, 2, 2})\n\tusers.Add(\"3\", []float32{3, 3, 3})\n\n\tpath := filepath.Join(t.TempDir(), \"users\")\n\tf, err := os.Create(path)\n\tassert.NoError(t, err)\n\tdefer f.Close()\n\terr = users.Marshal(f)\n\tassert.NoError(t, err)\n\n\tf, err = os.Open(path)\n\tassert.NoError(t, err)\n\tdefer f.Close()\n\tusers2 := NewMatrixFactorizationUsers()\n\terr = users2.Unmarshal(f)\n\tassert.NoError(t, err)\n\tassert.Equal(t, users.embeddings, users2.embeddings)\n}\n"
  },
  {
    "path": "logics/chat.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage logics\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"strings\"\n\n\t\"github.com/gorse-io/gorse/common/reranker\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/nikolalohinski/gonja/v2\"\n\t\"github.com/nikolalohinski/gonja/v2/exec\"\n\t\"github.com/sashabaranov/go-openai\"\n\t\"github.com/yuin/goldmark\"\n\t\"github.com/yuin/goldmark/ast\"\n\t\"github.com/yuin/goldmark/text\"\n)\n\ntype FeedbackItem struct {\n\tFeedbackType string\n\tdata.Item\n}\n\ntype ChatReranker struct {\n\tqueryTemplate *exec.Template\n\tdocTemplate   *exec.Template\n\tclient        *reranker.Client\n\tmodel         string\n}\n\nfunc NewChatReranker(cfg config.RerankerAPIConfig, queryTemplate, docTemplate string) (*ChatReranker, error) {\n\t// create reranker client\n\tclient := reranker.NewClient(cfg.AuthToken, cfg.URL)\n\t// create templates\n\tqTpl, err := gonja.FromString(queryTemplate)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdTpl, err := gonja.FromString(docTemplate)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &ChatReranker{\n\t\tqueryTemplate: qTpl,\n\t\tdocTemplate:   dTpl,\n\t\tclient:        client,\n\t\tmodel:         cfg.Model,\n\t}, nil\n}\n\nfunc (r *ChatReranker) Rank(ctx context.Context, user *data.User, feedback []*FeedbackItem, items []*data.Item) ([]cache.Score, error) {\n\t// render query\n\tvar queryBuf strings.Builder\n\tqueryCtx := exec.NewContext(map[string]any{\n\t\t\"user\":     user,\n\t\t\"feedback\": feedback,\n\t})\n\tif err := r.queryTemplate.Execute(&queryBuf, queryCtx); err != nil {\n\t\treturn nil, err\n\t}\n\t// render documents\n\tdocuments := make([]string, len(items))\n\tfor i, item := range items {\n\t\tvar docBuf strings.Builder\n\t\tdocCtx := exec.NewContext(map[string]any{\n\t\t\t\"item\": item,\n\t\t})\n\t\tif err := r.docTemplate.Execute(&docBuf, docCtx); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdocuments[i] = docBuf.String()\n\t}\n\t// rerank\n\tresp, err := r.client.Rerank(ctx, reranker.RerankRequest{\n\t\tModel:     r.model,\n\t\tQuery:     queryBuf.String(),\n\t\tDocuments: documents,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// sort items\n\tresult := make([]cache.Score, len(resp.Results))\n\tfor i, rerankResult := range resp.Results {\n\t\tresult[i].Id = items[rerankResult.Index].ItemId\n\t\tresult[i].Score = rerankResult.RelevanceScore\n\t}\n\treturn result, nil\n}\n\n// parseArrayFromCompletion parse JSON array from completion.\n// If the completion contains a JSON array, it will return each element in the array.\n// If the completion contains a JSON object, it will return the object as a string.\n// Otherwise, it will return the completion as a string.\nfunc parseArrayFromCompletion(completion string) []string {\n\tsource := []byte(stripThinkInCompletion(completion))\n\troot := goldmark.DefaultParser().Parse(text.NewReader(source))\n\tfor n := root.FirstChild(); n != nil; n = n.NextSibling() {\n\t\tif n.Kind() != ast.KindFencedCodeBlock {\n\t\t\tcontinue\n\t\t}\n\t\tif codeBlock, ok := n.(*ast.FencedCodeBlock); ok {\n\t\t\tif string(codeBlock.Language(source)) == \"json\" {\n\t\t\t\tbytes := codeBlock.Text(source)\n\t\t\t\tif bytes[0] == '[' {\n\t\t\t\t\tvar temp []any\n\t\t\t\t\terr := json.Unmarshal(bytes, &temp)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn []string{string(bytes)}\n\t\t\t\t\t}\n\t\t\t\t\tvar result []string\n\t\t\t\t\tfor _, v := range temp {\n\t\t\t\t\t\tvar bytes []byte\n\t\t\t\t\t\tswitch typed := v.(type) {\n\t\t\t\t\t\tcase string:\n\t\t\t\t\t\t\tbytes = []byte(typed)\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tbytes, err = json.Marshal(v)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn []string{string(bytes)}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresult = append(result, string(bytes))\n\t\t\t\t\t}\n\t\t\t\t\treturn result\n\t\t\t\t}\n\t\t\t\treturn []string{string(bytes)}\n\t\t\t} else if string(codeBlock.Language(source)) == \"csv\" {\n\t\t\t\t// If the code block is CSV, retrieve 1st column as IDs.\n\t\t\t\tbytes := codeBlock.Text(source)\n\t\t\t\tlines := strings.Split(string(bytes), \"\\n\")\n\t\t\t\tvar result []string\n\t\t\t\tfor _, line := range lines {\n\t\t\t\t\tfields := strings.Split(line, \",\")\n\t\t\t\t\tif len(fields) > 0 && strings.TrimSpace(fields[0]) != \"\" {\n\t\t\t\t\t\tresult = append(result, strings.TrimSpace(fields[0]))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn result\n\t\t\t}\n\t\t}\n\t}\n\tvar result []string\n\tfor _, line := range strings.Split(string(source), \"\\n\") {\n\t\tline = strings.TrimSpace(line)\n\t\tif line != \"\" {\n\t\t\tresult = append(result, line)\n\t\t}\n\t}\n\treturn result\n}\n\nfunc isThrottled(err error) bool {\n\tswitch e := err.(type) {\n\tcase *openai.APIError:\n\t\tif e.HTTPStatusCode == 429 {\n\t\t\treturn true\n\t\t}\n\tcase *openai.RequestError:\n\t\treturn e.HTTPStatusCode == 504 || e.HTTPStatusCode == 520\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "logics/chat_test.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage logics\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/gorse-io/gorse/common/reranker\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestChatReranker(t *testing.T) {\n\ts := reranker.NewMockServer()\n\tgo func() {\n\t\terr := s.Start()\n\t\tif err != nil && err != http.ErrServerClosed {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\ts.Ready()\n\tdefer s.Close()\n\n\treranker, err := NewChatReranker(config.RerankerAPIConfig{\n\t\tAuthToken: s.AuthToken(),\n\t\tURL:       s.URL(),\n\t\tModel:     \"gte-rerank\",\n\t},\n\t\t\"{{ user.UserId }} is a {{ user.Comment }} watched the following movies recently: {% for item in feedback %}{{ item.Comment }}, {% endfor %}\",\n\t\t\"{{ item.Comment }}\")\n\tassert.NoError(t, err)\n\n\titems, err := reranker.Rank(t.Context(), &data.User{\n\t\tUserId:  \"Tom\",\n\t\tComment: \"horror movie enthusiast\",\n\t}, []*FeedbackItem{\n\t\t{Item: data.Item{ItemId: \"tt0387564\", Comment: \"Saw\"}},\n\t\t{Item: data.Item{ItemId: \"tt0432348\", Comment: \"Saw II\"}},\n\t\t{Item: data.Item{ItemId: \"tt0435761\", Comment: \"Saw III\"}},\n\t}, []*data.Item{\n\t\t{ItemId: \"tt1233227\", Comment: \"Harry Potter and the Half-Blood Prince\"},\n\t\t{ItemId: \"tt0926084\", Comment: \"Harry Potter and the Deathly Hallows: Part 1\"},\n\t\t{ItemId: \"tt0890870\", Comment: \"Saw IV\"},\n\t\t{ItemId: \"tt1132626\", Comment: \"Saw VI\"},\n\t\t{ItemId: \"tt0435761\", Comment: \"Saw V\"},\n\t})\n\tassert.NoError(t, err)\n\tassert.Equal(t, []cache.Score{\n\t\t{Id: \"tt1233227\", Score: 1},\n\t\t{Id: \"tt0926084\", Score: 0.5},\n\t\t{Id: \"tt0890870\", Score: 0.3333333333333333},\n\t\t{Id: \"tt1132626\", Score: 0.25},\n\t\t{Id: \"tt0435761\", Score: 0.2},\n\t}, items)\n}\n\nfunc TestParseArrayFromCompletion(t *testing.T) {\n\t// parse JSON object\n\tcompletion := \"```json\\n{\\\"a\\\": 1, \\\"b\\\": 2}\\n```\"\n\tparsed := parseArrayFromCompletion(completion)\n\tassert.Equal(t, []string{\"{\\\"a\\\": 1, \\\"b\\\": 2}\\n\"}, parsed)\n\n\t// parse JSON array\n\tcompletion = \"```json\\n[1, 2]\\n```\"\n\tparsed = parseArrayFromCompletion(completion)\n\tassert.Equal(t, []string{\"1\", \"2\"}, parsed)\n\n\t// parse CSV\n\tcompletion = \"```csv\\n1\\n2\\n3\\n```\"\n\tparsed = parseArrayFromCompletion(completion)\n\tassert.Equal(t, []string{\"1\", \"2\", \"3\"}, parsed)\n\n\t// parse text\n\tcompletion = \"Hello, world!\\nThis is a test.\"\n\tparsed = parseArrayFromCompletion(completion)\n\tassert.Equal(t, []string{\"Hello, world!\", \"This is a test.\"}, parsed)\n\n\t// strip think\n\tcompletion = \"<think>hello</think>World!\"\n\tassert.Equal(t, \"World!\", stripThinkInCompletion(completion))\n}\n"
  },
  {
    "path": "logics/external.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage logics\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/samber/lo\"\n\t\"modernc.org/quickjs\"\n)\n\ntype External struct {\n\tvm     *quickjs.VM\n\tclient *http.Client\n\tscript string\n\tname   string\n}\n\nfunc NewExternal(cfg config.ExternalConfig) (*External, error) {\n\tvm, err := quickjs.NewVM()\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\n\t// Add environment variables\n\tenv, err := vm.NewObjectValue()\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\tfor _, e := range os.Environ() {\n\t\tparts := strings.SplitN(e, \"=\", 2)\n\t\tif len(parts) != 2 {\n\t\t\tcontinue\n\t\t}\n\t\tkey, err := vm.NewAtom(parts[0])\n\t\tif err != nil {\n\t\t\treturn nil, errors.WithStack(err)\n\t\t}\n\t\tvalue := parts[1]\n\t\tif err := env.SetProperty(key, value); err != nil {\n\t\t\treturn nil, errors.WithStack(err)\n\t\t}\n\t}\n\tenvKey, err := vm.NewAtom(\"env\")\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\tif err := vm.GlobalObject().SetProperty(envKey, env); err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\n\t// Register fetch function\n\texternal := &External{\n\t\tvm:     vm,\n\t\tclient: &http.Client{},\n\t\tscript: cfg.Script,\n\t\tname:   cfg.Name,\n\t}\n\tif err = vm.RegisterFunc(\"fetch\", external.fetch, false); err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\n\treturn external, nil\n}\n\nfunc (e *External) Close() error {\n\treturn e.vm.Close()\n}\n\nfunc (e *External) Pull(userId string) (res []string, err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tvar ok bool\n\t\t\terr, ok = r.(error)\n\t\t\tif !ok {\n\t\t\t\terr = errors.Errorf(\"%v\", r)\n\t\t\t}\n\t\t}\n\t}()\n\n\tuserIdKey, err := e.vm.NewAtom(\"user_id\")\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\terr = e.vm.GlobalObject().SetProperty(userIdKey, userId)\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\tresult, err := e.vm.Eval(e.script, quickjs.EvalGlobal)\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\tswitch v := result.(type) {\n\tcase string:\n\t\tvar items []string\n\t\tif err := json.Unmarshal([]byte(v), &items); err != nil {\n\t\t\treturn nil, errors.WithStack(err)\n\t\t}\n\t\treturn items, nil\n\tcase *quickjs.Object:\n\t\tvar items []string\n\t\tif err := json.Unmarshal([]byte(v.String()), &items); err != nil {\n\t\t\treturn nil, errors.WithStack(err)\n\t\t}\n\t\treturn items, nil\n\tdefault:\n\t\treturn nil, errors.New(\"script must return string or object\")\n\t}\n}\n\nfunc (e *External) fetch(args ...quickjs.Value) quickjs.Value {\n\tvar (\n\t\turl string\n\t\treq = quickjs.UndefinedValue\n\t)\n\tif len(args) == 1 {\n\t\tswitch v := lo.Must(args[0].Any()).(type) {\n\t\tcase string:\n\t\t\turl = v\n\t\tcase *quickjs.Object:\n\t\t\treq = args[0]\n\t\tdefault:\n\t\t\tpanic(\"fetch requires first argument to be string or object\")\n\t\t}\n\t} else if len(args) == 2 {\n\t\tif _, ok := lo.Must(args[0].Any()).(string); !ok {\n\t\t\tpanic(\"fetch requires first argument to be string\")\n\t\t}\n\t\tif _, ok := lo.Must(args[1].Any()).(*quickjs.Object); !ok {\n\t\t\tpanic(\"fetch requires second argument to be object\")\n\t\t}\n\t\turl = lo.Must(args[0].Any()).(string)\n\t\treq = args[1]\n\t} else {\n\t\tpanic(\"fetch requires 1 or 2 arguments\")\n\t}\n\tr := e.parseRequest(url, req)\n\tresp, err := e.client.Do(r)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn e.newResponse(resp)\n}\n\n// parseRequest parse Fetch API Request.\nfunc (e *External) parseRequest(url string, req quickjs.Value) *http.Request {\n\tmethod := \"GET\"\n\theaders := make(map[string]string)\n\tbody := \"\"\n\tif !req.IsUndefined() {\n\t\t// Request.method\n\t\tmethodKey := lo.Must(e.vm.NewAtom(\"method\"))\n\t\tmethodValue := lo.Must(req.GetPropertyValue(methodKey))\n\t\tif !methodValue.IsUndefined() {\n\t\t\tmethod = lo.Must(methodValue.Any()).(string)\n\t\t}\n\t\t// Request.headers\n\t\theadersKey := lo.Must(e.vm.NewAtom(\"headers\"))\n\t\theadersValue := lo.Must(req.GetPropertyValue(headersKey))\n\t\tif !headersValue.IsUndefined() {\n\t\t\tif headersObj, ok := lo.Must(headersValue.Any()).(*quickjs.Object); ok {\n\t\t\t\tif err := json.Unmarshal([]byte(headersObj.String()), &headers); err != nil {\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// Request.url\n\t\turlKey := lo.Must(e.vm.NewAtom(\"url\"))\n\t\turlValue := lo.Must(req.GetPropertyValue(urlKey))\n\t\tif !urlValue.IsUndefined() {\n\t\t\turl = lo.Must(urlValue.Any()).(string)\n\t\t}\n\t\t// Request.body\n\t\tbodyKey := lo.Must(e.vm.NewAtom(\"body\"))\n\t\tbodyValue := lo.Must(req.GetPropertyValue(bodyKey))\n\t\tif !bodyValue.IsUndefined() {\n\t\t\tbody = lo.Must(bodyValue.Any()).(string)\n\t\t}\n\t}\n\n\tr := lo.Must(http.NewRequest(method, url, strings.NewReader(body)))\n\tfor key, value := range headers {\n\t\tr.Header.Add(key, value)\n\t}\n\treturn r\n}\n\n// newResponse convert http.Response to Fetch API Response.\nfunc (e *External) newResponse(resp *http.Response) quickjs.Value {\n\tif resp == nil {\n\t\treturn quickjs.UndefinedValue\n\t}\n\tresponse := lo.Must(e.vm.NewObjectValue())\n\t// Response.ok\n\tokKey := lo.Must(e.vm.NewAtom(\"ok\"))\n\tlo.Must0(response.SetProperty(okKey, resp.StatusCode >= 200 && resp.StatusCode < 300))\n\t// Response.status\n\tstatusKey := lo.Must(e.vm.NewAtom(\"status\"))\n\tlo.Must0(response.SetProperty(statusKey, resp.StatusCode))\n\t// Response.statusText\n\tstatusTextKey := lo.Must(e.vm.NewAtom(\"statusText\"))\n\tlo.Must0(response.SetProperty(statusTextKey, resp.Status))\n\t// Response.body\n\tbodyKey := lo.Must(e.vm.NewAtom(\"body\"))\n\tbody := lo.Must(io.ReadAll(resp.Body))\n\tlo.Must0(response.SetProperty(bodyKey, string(body)))\n\t// Response.headers\n\theadersKey := lo.Must(e.vm.NewAtom(\"headers\"))\n\theaders := lo.Must(e.vm.NewObjectValue())\n\tfor key, values := range resp.Header {\n\t\theaderKey := lo.Must(e.vm.NewAtom(key))\n\t\tlo.Must0(headers.SetProperty(headerKey, strings.Join(values, \", \")))\n\t}\n\tlo.Must0(response.SetProperty(headersKey, headers))\n\treturn response\n}\n"
  },
  {
    "path": "logics/external_test.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage logics\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"modernc.org/quickjs\"\n)\n\nfunc TestEnv(t *testing.T) {\n\tt.Setenv(\"TEST_ENV\", \"test_value\")\n\n\texternal, err := NewExternal(config.ExternalConfig{})\n\tassert.NoError(t, err)\n\tdefer external.Close()\n\n\tvalue, err := external.vm.Eval(`env.TEST_ENV`, quickjs.EvalGlobal)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"test_value\", value)\n}\n\nfunc TestFetch(t *testing.T) {\n\tvar (\n\t\treq  *http.Request\n\t\tbody string\n\t)\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\treq = r\n\t\tb, err := io.ReadAll(r.Body)\n\t\tassert.NoError(t, err)\n\t\tbody = string(b)\n\t\tfmt.Fprintln(w, \"Hello, client\")\n\t}))\n\tdefer ts.Close()\n\n\texternal, err := NewExternal(config.ExternalConfig{})\n\tassert.NoError(t, err)\n\tdefer external.Close()\n\n\tresponse, err := external.vm.Eval(`fetch(\"`+ts.URL+`\")`, quickjs.EvalGlobal)\n\tassert.NoError(t, err)\n\tif assert.NotNil(t, req) {\n\t\tassert.Equal(t, \"GET\", req.Method)\n\t}\n\tif assert.IsType(t, &quickjs.Object{}, response) {\n\t\tvar resp map[string]any\n\t\terr = json.Unmarshal([]byte(response.(*quickjs.Object).String()), &resp)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, true, resp[\"ok\"])\n\t\tassert.Equal(t, float64(200), resp[\"status\"])\n\t\tassert.Equal(t, \"200 OK\", resp[\"statusText\"])\n\t\tassert.Equal(t, \"Hello, client\\n\", resp[\"body\"])\n\t\theaders := resp[\"headers\"].(map[string]any)\n\t\tassert.Contains(t, headers, \"Content-Length\")\n\t\tassert.Contains(t, headers, \"Date\")\n\t}\n\n\t_, err = external.vm.Eval(`fetch({method: \"POST\", url: \"`+ts.URL+`\"})`, quickjs.EvalGlobal)\n\tassert.NoError(t, err)\n\tif assert.NotNil(t, req) {\n\t\tassert.Equal(t, \"POST\", req.Method)\n\t}\n\n\t_, err = external.vm.Eval(`fetch(\"`+ts.URL+`\", {\n\tmethod: \"PUT\",\n\theaders: {\n\t\t\"Content-Type\": \"application/json\"\n\t},\n\tbody: JSON.stringify({message: \"Hello, server\"})\n\t})`, quickjs.EvalGlobal)\n\tassert.NoError(t, err)\n\tif assert.NotNil(t, req) {\n\t\tassert.Equal(t, \"PUT\", req.Method)\n\t\tassert.Equal(t, \"application/json\", req.Header.Get(\"Content-Type\"))\n\t\tassert.Equal(t, `{\"message\":\"Hello, server\"}`, body)\n\t}\n}\n\nfunc TestExternal(t *testing.T) {\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tuserId := r.URL.Query().Get(\"user_id\")\n\t\tif userId == \"1\" {\n\t\t\tfmt.Fprintln(w, `[\"item_1\", \"item_2\", \"item_3\"]`)\n\t\t} else {\n\t\t\thttp.NotFound(w, r)\n\t\t}\n\t}))\n\tdefer ts.Close()\n\n\texternal, err := NewExternal(config.ExternalConfig{\n\t\tScript: fmt.Sprintf(`fetch(\"%s?user_id=1\").body`, ts.URL),\n\t\tName:   \"test\",\n\t})\n\tassert.NoError(t, err)\n\tdefer external.Close()\n\n\titems, err := external.Pull(\"1\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, []string{\"item_1\", \"item_2\", \"item_3\"}, items)\n}\n\nfunc TestException(t *testing.T) {\n\texternal, err := NewExternal(config.ExternalConfig{\n\t\tScript: `throw new Error(\"test error\")`,\n\t\tName:   \"test\",\n\t})\n\tassert.NoError(t, err)\n\tdefer external.Close()\n\n\t_, err = external.Pull(\"1\")\n\tassert.Error(t, err)\n}\n\nfunc TestPanic(t *testing.T) {\n\texternal, err := NewExternal(config.ExternalConfig{\n\t\tScript: `fetch({}, {})`,\n\t\tName:   \"test\",\n\t})\n\tassert.NoError(t, err)\n\tdefer external.Close()\n\n\t_, err = external.Pull(\"1\")\n\tassert.Error(t, err)\n}\n"
  },
  {
    "path": "logics/item_to_item.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage logics\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/chewxy/math32\"\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/expr-lang/expr\"\n\t\"github.com/expr-lang/expr/vm\"\n\t\"github.com/gorse-io/gorse/common/ann\"\n\t\"github.com/gorse-io/gorse/common/floats\"\n\t\"github.com/gorse-io/gorse/common/heap\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/common/parallel\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/nikolalohinski/gonja/v2\"\n\t\"github.com/nikolalohinski/gonja/v2/exec\"\n\t\"github.com/samber/lo\"\n\t\"github.com/sashabaranov/go-openai\"\n\t\"github.com/tiktoken-go/tokenizer\"\n\t\"go.uber.org/zap\"\n)\n\nvar cl100kBaseTokenizer tokenizer.Codec\n\nfunc init() {\n\tvar err error\n\tcl100kBaseTokenizer, err = tokenizer.Get(tokenizer.Cl100kBase)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\ntype ItemToItemOptions struct {\n\tTagsIDF      []float32\n\tUsersIDF     []float32\n\tOpenAIConfig config.OpenAIConfig\n}\n\ntype ItemToItem interface {\n\tTimestamp() time.Time\n\tCount() int\n\tGet(i int) *data.Item\n\tPush(item *data.Item, feedback []int32)\n\tPopAll(i int) []cache.Score\n}\n\nfunc NewItemToItem(cfg config.ItemToItemConfig, n int, timestamp time.Time, opts *ItemToItemOptions) (ItemToItem, error) {\n\tswitch cfg.Type {\n\tcase \"embedding\":\n\t\treturn newEmbeddingItemToItem(cfg, n, timestamp)\n\tcase \"tags\":\n\t\tif opts == nil || opts.TagsIDF == nil {\n\t\t\treturn nil, errors.New(\"tags IDF is required for tags item-to-item\")\n\t\t}\n\t\treturn newTagsItemToItem(cfg, n, timestamp, opts.TagsIDF)\n\tcase \"users\":\n\t\tif opts == nil || opts.UsersIDF == nil {\n\t\t\treturn nil, errors.New(\"users IDF is required for users item-to-item\")\n\t\t}\n\t\treturn newUsersItemToItem(cfg, n, timestamp, opts.UsersIDF)\n\tcase \"auto\":\n\t\tif opts == nil || opts.TagsIDF == nil || opts.UsersIDF == nil {\n\t\t\treturn nil, errors.New(\"tags and users IDF are required for auto item-to-item\")\n\t\t}\n\t\treturn newAutoItemToItem(cfg, n, timestamp, opts.TagsIDF, opts.UsersIDF)\n\tcase \"chat\":\n\t\tif opts == nil || opts.OpenAIConfig.BaseURL == \"\" || opts.OpenAIConfig.AuthToken == \"\" {\n\t\t\treturn nil, errors.New(\"OpenAI config is required for chat item-to-item\")\n\t\t}\n\t\treturn newChatItemToItem(cfg, n, timestamp, opts.OpenAIConfig)\n\tdefault:\n\t\treturn nil, errors.New(\"invalid item-to-item type\")\n\t}\n}\n\ntype baseItemToItem[T any] struct {\n\tname       string\n\tn          int\n\ttimestamp  time.Time\n\tcolumnFunc *vm.Program\n\tindex      *ann.HNSW[T]\n\titems      []*data.Item\n\titemsLock  sync.Mutex\n\t// Hidden items are stored separately without adding to the index,\n\t// and they have neighbors but are not neighbors of other items.\n\thiddenItems   []*data.Item\n\thiddenVectors []T\n}\n\nfunc (b *baseItemToItem[T]) Timestamp() time.Time {\n\treturn b.timestamp\n}\n\nfunc (b *baseItemToItem[T]) Count() int {\n\treturn len(b.items) + len(b.hiddenItems)\n}\n\nfunc (b *baseItemToItem[T]) Get(i int) *data.Item {\n\tif i < len(b.items) {\n\t\treturn b.items[i]\n\t}\n\treturn b.hiddenItems[i-len(b.items)]\n}\n\nfunc (b *baseItemToItem[T]) pushItem(item *data.Item, v T) {\n\tif item.IsHidden {\n\t\tb.itemsLock.Lock()\n\t\tb.hiddenItems = append(b.hiddenItems, item)\n\t\tb.hiddenVectors = append(b.hiddenVectors, v)\n\t\tb.itemsLock.Unlock()\n\t} else {\n\t\tb.itemsLock.Lock()\n\t\tb.items = append(b.items, nil)\n\t\tb.itemsLock.Unlock()\n\t\tj := b.index.Add(v)\n\t\tb.itemsLock.Lock()\n\t\tb.items[j] = item\n\t\tb.itemsLock.Unlock()\n\t}\n}\n\nfunc (b *baseItemToItem[T]) PopAll(i int) []cache.Score {\n\tvar results []lo.Tuple2[int, float32]\n\tif i < len(b.items) {\n\t\t// Non-hidden item: search by index\n\t\tvar err error\n\t\tresults, err = b.index.SearchIndex(i, b.n+1, true)\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to search index\", zap.Error(err))\n\t\t\treturn nil\n\t\t}\n\t} else {\n\t\t// Hidden item: search by vector\n\t\tresults = b.index.SearchVector(b.hiddenVectors[i-len(b.items)], b.n, true)\n\t}\n\treturn lo.Map(results, func(v lo.Tuple2[int, float32], _ int) cache.Score {\n\t\treturn cache.Score{\n\t\t\tId:         b.items[v.A].ItemId,\n\t\t\tCategories: b.items[v.A].Categories,\n\t\t\tScore:      1.0 / (1.0 + float64(v.B)),\n\t\t\tTimestamp:  b.timestamp,\n\t\t}\n\t})\n}\n\ntype embeddingItemToItem struct {\n\tbaseItemToItem[[]float32]\n\tdimension int\n}\n\nfunc newEmbeddingItemToItem(cfg config.ItemToItemConfig, n int, timestamp time.Time) (*embeddingItemToItem, error) {\n\t// Compile column expression\n\tcolumnFunc, err := expr.Compile(cfg.Column, expr.Env(map[string]any{\n\t\t\"item\": data.Item{},\n\t}))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &embeddingItemToItem{baseItemToItem: baseItemToItem[[]float32]{\n\t\tname:       cfg.Name,\n\t\tn:          n,\n\t\ttimestamp:  timestamp,\n\t\tcolumnFunc: columnFunc,\n\t\tindex:      ann.NewHNSW(floats.Euclidean),\n\t}}, nil\n}\n\nfunc (e *embeddingItemToItem) Push(item *data.Item, _ []int32) {\n\t// Evaluate filter function\n\tresult, err := expr.Run(e.columnFunc, map[string]any{\n\t\t\"item\": item,\n\t})\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to evaluate column expression\",\n\t\t\tzap.Any(\"item\", item), zap.Error(err))\n\t\treturn\n\t}\n\t// Check column type\n\tv, ok := result.([]float32)\n\tif !ok {\n\t\tlog.Logger().Error(\"invalid column type\", zap.Any(\"column\", result))\n\t\treturn\n\t}\n\t// Check dimension\n\te.itemsLock.Lock()\n\tif e.dimension == 0 && len(v) > 0 {\n\t\te.dimension = len(v)\n\t} else if e.dimension != len(v) {\n\t\tlog.Logger().Error(\"invalid column dimension\", zap.Int(\"dimension\", len(v)))\n\t\te.itemsLock.Unlock()\n\t\treturn\n\t}\n\te.itemsLock.Unlock()\n\te.pushItem(item, v)\n}\n\ntype tagsItemToItem struct {\n\tbaseItemToItem[[]dataset.ID]\n\tIDF[dataset.ID]\n}\n\nfunc newTagsItemToItem(cfg config.ItemToItemConfig, n int, timestamp time.Time, idf []float32) (ItemToItem, error) {\n\t// Compile column expression\n\tcolumnFunc, err := expr.Compile(cfg.Column, expr.Env(map[string]any{\n\t\t\"item\": data.Item{},\n\t}))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tt := &tagsItemToItem{IDF: idf}\n\tt.baseItemToItem = baseItemToItem[[]dataset.ID]{\n\t\tname:       cfg.Name,\n\t\tn:          n,\n\t\ttimestamp:  timestamp,\n\t\tcolumnFunc: columnFunc,\n\t\tindex:      ann.NewHNSW(t.distance),\n\t}\n\treturn t, nil\n}\n\nfunc (t *tagsItemToItem) Push(item *data.Item, _ []int32) {\n\t// Evaluate filter function\n\tresult, err := expr.Run(t.columnFunc, map[string]any{\n\t\t\"item\": item,\n\t})\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to evaluate column expression\",\n\t\t\tzap.Any(\"item\", item), zap.Error(err))\n\t\treturn\n\t}\n\t// Extract tags\n\ttSet := mapset.NewSet[dataset.ID]()\n\tflatten(result, tSet)\n\tv := tSet.ToSlice()\n\tsort.Slice(v, func(i, j int) bool {\n\t\treturn v[i] < v[j]\n\t})\n\tt.pushItem(item, v)\n}\n\ntype usersItemToItem struct {\n\tbaseItemToItem[[]int32]\n\tIDF[int32]\n}\n\nfunc newUsersItemToItem(cfg config.ItemToItemConfig, n int, timestamp time.Time, idf []float32) (ItemToItem, error) {\n\tif cfg.Column != \"\" {\n\t\treturn nil, errors.New(\"column is not supported in users item-to-item\")\n\t}\n\tu := &usersItemToItem{IDF: idf}\n\tu.baseItemToItem = baseItemToItem[[]int32]{\n\t\tname:      cfg.Name,\n\t\tn:         n,\n\t\ttimestamp: timestamp,\n\t\tindex:     ann.NewHNSW(u.distance),\n\t}\n\treturn u, nil\n}\n\nfunc (u *usersItemToItem) Push(item *data.Item, feedback []int32) {\n\t// Sort feedback\n\tsort.Slice(feedback, func(i, j int) bool {\n\t\treturn feedback[i] < feedback[j]\n\t})\n\tu.pushItem(item, feedback)\n}\n\ntype autoItemToItem struct {\n\tbaseItemToItem[lo.Tuple2[[]dataset.ID, []int32]]\n\ttIDF IDF[dataset.ID]\n\tuIDF IDF[int32]\n}\n\nfunc newAutoItemToItem(cfg config.ItemToItemConfig, n int, timestamp time.Time, tIDF, uIDF []float32) (ItemToItem, error) {\n\ta := &autoItemToItem{\n\t\ttIDF: tIDF,\n\t\tuIDF: uIDF,\n\t}\n\ta.baseItemToItem = baseItemToItem[lo.Tuple2[[]dataset.ID, []int32]]{\n\t\tname:      cfg.Name,\n\t\tn:         n,\n\t\ttimestamp: timestamp,\n\t\tindex:     ann.NewHNSW[lo.Tuple2[[]dataset.ID, []int32]](a.distance),\n\t}\n\treturn a, nil\n}\n\nfunc (a *autoItemToItem) Push(item *data.Item, feedback []int32) {\n\t// Extract tags\n\ttSet := mapset.NewSet[dataset.ID]()\n\tflatten(item.Labels, tSet)\n\tv := tSet.ToSlice()\n\tsort.Slice(v, func(i, j int) bool {\n\t\treturn v[i] < v[j]\n\t})\n\t// Sort feedback\n\tsort.Slice(feedback, func(i, j int) bool {\n\t\treturn feedback[i] < feedback[j]\n\t})\n\ta.pushItem(item, lo.Tuple2[[]dataset.ID, []int32]{A: v, B: feedback})\n}\n\nfunc (a *autoItemToItem) distance(u, v lo.Tuple2[[]dataset.ID, []int32]) float32 {\n\treturn (a.tIDF.distance(u.A, v.A) + a.uIDF.distance(u.B, v.B)) / 2\n}\n\ntype IDF[T dataset.ID | int32] []float32\n\nfunc (idf IDF[T]) distance(a, b []T) float32 {\n\tcommonSum, commonCount := idf.weightedSumCommonElements(a, b)\n\tif len(a) == len(b) && commonCount == float32(len(a)) {\n\t\t// If two items have the same tags, its distance is zero.\n\t\treturn 0\n\t} else if commonCount > 0 && len(a) > 0 && len(b) > 0 {\n\t\t// Add shrinkage to avoid division by zero\n\t\treturn 1 - commonSum*commonCount/\n\t\t\tmath32.Sqrt(idf.weightedSum(a))/\n\t\t\tmath32.Sqrt(idf.weightedSum(b))/\n\t\t\t(commonCount+100)\n\t} else {\n\t\t// If two items have no common tags, its distance is one.\n\t\treturn 1\n\t}\n}\n\nfunc (idf IDF[T]) weightedSumCommonElements(a, b []T) (float32, float32) {\n\ti, j, sum, count := 0, 0, float32(0), float32(0)\n\tfor i < len(a) && j < len(b) {\n\t\tif a[i] == b[j] {\n\t\t\tsum += idf[a[i]]\n\t\t\tcount++\n\t\t\ti++\n\t\t\tj++\n\t\t} else if a[i] < b[j] {\n\t\t\ti++\n\t\t} else if a[i] > b[j] {\n\t\t\tj++\n\t\t}\n\t}\n\treturn sum, count\n}\n\nfunc (idf IDF[T]) weightedSum(a []T) float32 {\n\tvar sum float32\n\tfor _, i := range a {\n\t\tsum += idf[i]\n\t}\n\treturn sum\n}\n\nfunc flatten(o any, tSet mapset.Set[dataset.ID]) {\n\tswitch typed := o.(type) {\n\tcase dataset.ID:\n\t\ttSet.Add(typed)\n\t\treturn\n\tcase []dataset.ID:\n\t\ttSet.Append(typed...)\n\t\treturn\n\tcase map[string]any:\n\t\tfor _, v := range typed {\n\t\t\tflatten(v, tSet)\n\t\t}\n\t}\n}\n\ntype chatItemToItem struct {\n\t*embeddingItemToItem\n\ttemplate            *exec.Template\n\tclient              *openai.Client\n\tchatCompletionModel string\n\tembeddingModel      string\n\tembeddingDimensions int\n\tpoolSize            int\n}\n\nfunc newChatItemToItem(cfg config.ItemToItemConfig, n int, timestamp time.Time, openaiConfig config.OpenAIConfig) (*chatItemToItem, error) {\n\t// create embedding item-to-item recommender\n\tembedding, err := newEmbeddingItemToItem(cfg, n, timestamp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// parse template\n\ttemplate, err := gonja.FromString(cfg.Prompt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// create openai client\n\tclientConfig := openai.DefaultConfig(openaiConfig.AuthToken)\n\tclientConfig.BaseURL = openaiConfig.BaseURL\n\treturn &chatItemToItem{\n\t\tembeddingItemToItem: embedding,\n\t\ttemplate:            template,\n\t\tclient:              openai.NewClientWithConfig(clientConfig),\n\t\tchatCompletionModel: openaiConfig.ChatCompletionModel,\n\t\tembeddingModel:      openaiConfig.EmbeddingModel,\n\t\tembeddingDimensions: openaiConfig.EmbeddingDimensions,\n\t\tpoolSize:            min(openaiConfig.ChatCompletionRPM, openaiConfig.EmbeddingRPM),\n\t}, nil\n}\n\nfunc (g *chatItemToItem) PopAll(i int) []cache.Score {\n\titem := g.Get(i)\n\t// evaluate column expression and get embedding vector\n\tresult, err := expr.Run(g.columnFunc, map[string]any{\n\t\t\"item\": item,\n\t})\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to evaluate column expression\",\n\t\t\tzap.Any(\"item\", item), zap.Error(err))\n\t\treturn nil\n\t}\n\tembedding0, ok := result.([]float32)\n\tif !ok {\n\t\tlog.Logger().Error(\"invalid column type\", zap.Any(\"column\", result))\n\t\treturn nil\n\t}\n\t// render template\n\tvar buf strings.Builder\n\tctx := exec.NewContext(map[string]any{\n\t\t\"item\": item,\n\t})\n\tif err := g.template.Execute(&buf, ctx); err != nil {\n\t\tlog.Logger().Error(\"failed to execute template\", zap.Error(err))\n\t\treturn nil\n\t}\n\t// chat completion\n\tstart := time.Now()\n\tids, _, _ := cl100kBaseTokenizer.Encode(buf.String())\n\tresp, err := backoff.Retry(context.Background(), func() (openai.ChatCompletionResponse, error) {\n\t\ttime.Sleep(parallel.ChatCompletionRequestsLimiter.Take(1))\n\t\ttime.Sleep(parallel.ChatCompletionTokensLimiter.Take(int64(len(ids))))\n\t\tresp, err := g.client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{\n\t\t\tModel: g.chatCompletionModel,\n\t\t\tMessages: []openai.ChatCompletionMessage{{\n\t\t\t\tRole:    openai.ChatMessageRoleUser,\n\t\t\t\tContent: buf.String(),\n\t\t\t}},\n\t\t})\n\t\tif err == nil {\n\t\t\treturn resp, nil\n\t\t}\n\t\tif isThrottled(err) {\n\t\t\treturn openai.ChatCompletionResponse{}, err\n\t\t}\n\t\treturn openai.ChatCompletionResponse{}, backoff.Permanent(err)\n\t}, backoff.WithBackOff(backoff.NewExponentialBackOff()))\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to chat completion\", zap.String(\"item_id\", item.ItemId), zap.Error(err))\n\t\treturn nil\n\t}\n\tduration := time.Since(start)\n\tparsed := parseArrayFromCompletion(resp.Choices[0].Message.Content)\n\tlog.OpenAILogger().Info(\"chat completion\",\n\t\tzap.String(\"prompt\", buf.String()),\n\t\tzap.String(\"completion\", resp.Choices[0].Message.Content),\n\t\tzap.Strings(\"parsed\", parsed),\n\t\tzap.Int(\"prompt_tokens\", resp.Usage.PromptTokens),\n\t\tzap.Int(\"completion_tokens\", resp.Usage.CompletionTokens),\n\t\tzap.Int(\"total_tokens\", resp.Usage.TotalTokens),\n\t\tzap.Duration(\"duration\", duration))\n\t// message embedding\n\tembeddings := make([][]float32, len(parsed))\n\tfor i, message := range parsed {\n\t\tids, _, _ := cl100kBaseTokenizer.Encode(message)\n\t\tresp, err := backoff.Retry(context.Background(), func() (openai.EmbeddingResponse, error) {\n\t\t\ttime.Sleep(parallel.EmbeddingRequestsLimiter.Take(1))\n\t\t\ttime.Sleep(parallel.EmbeddingTokensLimiter.Take(int64(len(ids))))\n\t\t\tresp, err := g.client.CreateEmbeddings(context.Background(), openai.EmbeddingRequest{\n\t\t\t\tInput:      message,\n\t\t\t\tModel:      openai.EmbeddingModel(g.embeddingModel),\n\t\t\t\tDimensions: g.embeddingDimensions,\n\t\t\t})\n\t\t\tif err == nil {\n\t\t\t\treturn resp, nil\n\t\t\t}\n\t\t\tif isThrottled(err) {\n\t\t\t\treturn openai.EmbeddingResponse{}, err\n\t\t\t}\n\t\t\treturn openai.EmbeddingResponse{}, backoff.Permanent(err)\n\t\t}, backoff.WithBackOff(backoff.NewExponentialBackOff()))\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to create embeddings\", zap.String(\"item_id\", g.items[i].ItemId), zap.Error(err))\n\t\t\treturn nil\n\t\t}\n\t\tembeddings[i] = resp.Data[0].Embedding\n\t}\n\t// search index\n\tpq := heap.NewPriorityQueue(true)\n\tfor _, embedding := range embeddings {\n\t\tscore0 := floats.Euclidean(embedding, embedding0)\n\t\tscores := g.index.SearchVector(embedding, g.n+1, true)\n\t\tfor _, score := range scores {\n\t\t\tif score.A != i {\n\t\t\t\tpq.Push(int32(score.A), score.B*score0)\n\t\t\t\tif pq.Len() > g.n {\n\t\t\t\t\tpq.Pop()\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tscores := make([]cache.Score, pq.Len())\n\tfor i := pq.Len() - 1; i >= 0; i-- {\n\t\tid, score := pq.Pop()\n\t\tscores[i] = cache.Score{\n\t\t\tId:         g.items[id].ItemId,\n\t\t\tCategories: g.items[id].Categories,\n\t\t\tScore:      1.0 / (1.0 + float64(score)),\n\t\t\tTimestamp:  g.timestamp,\n\t\t}\n\t}\n\treturn scores\n}\n\nfunc stripThinkInCompletion(s string) string {\n\tif len(s) < 7 || s[:7] != \"<think>\" {\n\t\treturn s\n\t}\n\tend := strings.Index(s, \"</think>\")\n\tif end == -1 {\n\t\treturn s\n\t}\n\treturn s[end+8:]\n}\n"
  },
  {
    "path": "logics/item_to_item_test.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage logics\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/common/floats\"\n\t\"github.com/gorse-io/gorse/common/mock\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\ntype ItemToItemTestSuite struct {\n\tsuite.Suite\n}\n\nfunc (suite *ItemToItemTestSuite) TestColumnFunc() {\n\titem2item, err := newEmbeddingItemToItem(config.ItemToItemConfig{\n\t\tColumn: \"item.Labels.description\",\n\t}, 10, time.Now())\n\tsuite.NoError(err)\n\n\t// Push success\n\titem2item.Push(&data.Item{\n\t\tItemId: \"1\",\n\t\tLabels: map[string]any{\n\t\t\t\"description\": []float32{0.1, 0.2, 0.3},\n\t\t},\n\t}, nil)\n\tsuite.Equal(1, item2item.Count())\n\n\t// Hidden\n\titem2item.Push(&data.Item{\n\t\tItemId:   \"2\",\n\t\tIsHidden: true,\n\t\tLabels: map[string]any{\n\t\t\t\"description\": []float32{0.1, 0.2, 0.3},\n\t\t},\n\t}, nil)\n\tsuite.Equal(2, item2item.Count())\n\n\t// Dimension does not match\n\titem2item.Push(&data.Item{\n\t\tItemId: \"1\",\n\t\tLabels: map[string]any{\n\t\t\t\"description\": []float32{0.1, 0.2},\n\t\t},\n\t}, nil)\n\tsuite.Equal(2, item2item.Count())\n\n\t// Type does not match\n\titem2item.Push(&data.Item{\n\t\tItemId: \"1\",\n\t\tLabels: map[string]any{\n\t\t\t\"description\": \"hello\",\n\t\t},\n\t}, nil)\n\tsuite.Equal(2, item2item.Count())\n\n\t// Column does not exist\n\titem2item.Push(&data.Item{\n\t\tItemId: \"2\",\n\t\tLabels: []float32{0.1, 0.2, 0.3},\n\t}, nil)\n\tsuite.Equal(2, item2item.Count())\n}\n\nfunc (suite *ItemToItemTestSuite) TestEmbedding() {\n\ttimestamp := time.Now()\n\titem2item, err := newEmbeddingItemToItem(config.ItemToItemConfig{\n\t\tColumn: \"item.Labels.description\",\n\t}, 10, timestamp)\n\tsuite.NoError(err)\n\n\tfor i := 0; i < 100; i++ {\n\t\titem2item.Push(&data.Item{\n\t\t\tItemId: strconv.Itoa(i),\n\t\t\tLabels: map[string]any{\n\t\t\t\t\"description\": []float32{0.1 * float32(i), 0.2 * float32(i), 0.3 * float32(i)},\n\t\t\t},\n\t\t}, nil)\n\t}\n\n\tscores := item2item.PopAll(0)\n\tsuite.Len(scores, 10)\n\tfor i := 1; i <= 10; i++ {\n\t\tsuite.Equal(strconv.Itoa(i), scores[i-1].Id)\n\t}\n}\n\nfunc (suite *ItemToItemTestSuite) TestHidden() {\n\ttimestamp := time.Now()\n\titem2item, err := newEmbeddingItemToItem(config.ItemToItemConfig{\n\t\tColumn: \"item.Labels.description\",\n\t}, 2, timestamp)\n\tsuite.NoError(err)\n\n\titem2item.Push(&data.Item{\n\t\tItemId: \"visible_1\",\n\t\tLabels: map[string]any{\n\t\t\t\"description\": []float32{0.0, 0.0, 0.0},\n\t\t},\n\t}, nil)\n\titem2item.Push(&data.Item{\n\t\tItemId: \"visible_2\",\n\t\tLabels: map[string]any{\n\t\t\t\"description\": []float32{0.1, 0.0, 0.0},\n\t\t},\n\t}, nil)\n\titem2item.Push(&data.Item{\n\t\tItemId:   \"hidden_1\",\n\t\tIsHidden: true,\n\t\tLabels: map[string]any{\n\t\t\t\"description\": []float32{0.05, 0.0, 0.0},\n\t\t},\n\t}, nil)\n\n\tsuite.Equal(3, item2item.Count())\n\n\t// hidden item should have similar items generated from non-hidden index\n\thiddenScores := item2item.PopAll(2)\n\tsuite.Len(hiddenScores, 2)\n\tfor _, score := range hiddenScores {\n\t\tsuite.NotEqual(\"hidden_1\", score.Id)\n\t}\n\n\t// non-hidden item should never get hidden item in similarity results\n\tvisibleScores := item2item.PopAll(0)\n\tsuite.Len(visibleScores, 1)\n\tfor _, score := range visibleScores {\n\t\tsuite.NotEqual(\"hidden_1\", score.Id)\n\t}\n}\n\nfunc (suite *ItemToItemTestSuite) TestTags() {\n\ttimestamp := time.Now()\n\tidf := make([]float32, 101)\n\tfor i := range idf {\n\t\tidf[i] = 1\n\t}\n\titem2item, err := newTagsItemToItem(config.ItemToItemConfig{\n\t\tColumn: \"item.Labels\",\n\t}, 10, timestamp, idf)\n\tsuite.NoError(err)\n\n\tfor i := 0; i < 100; i++ {\n\t\tlabels := make(map[string]any)\n\t\tfor j := 1; j <= 100-i; j++ {\n\t\t\tlabels[strconv.Itoa(j)] = []dataset.ID{dataset.ID(j)}\n\t\t}\n\t\titem2item.Push(&data.Item{\n\t\t\tItemId: strconv.Itoa(i),\n\t\t\tLabels: labels,\n\t\t}, nil)\n\t}\n\n\tscores := item2item.PopAll(0)\n\tsuite.Len(scores, 10)\n\tfor i := 1; i <= 10; i++ {\n\t\tsuite.Equal(strconv.Itoa(i), scores[i-1].Id)\n\t}\n}\n\nfunc (suite *ItemToItemTestSuite) TestUsers() {\n\ttimestamp := time.Now()\n\tidf := make([]float32, 101)\n\tfor i := range idf {\n\t\tidf[i] = 1\n\t}\n\titem2item, err := newUsersItemToItem(config.ItemToItemConfig{}, 10, timestamp, idf)\n\tsuite.NoError(err)\n\n\tfor i := 0; i < 100; i++ {\n\t\tfeedback := make([]int32, 0, 100-i)\n\t\tfor j := 1; j <= 100-i; j++ {\n\t\t\tfeedback = append(feedback, int32(j))\n\t\t}\n\t\titem2item.Push(&data.Item{ItemId: strconv.Itoa(i)}, feedback)\n\t}\n\n\tscores := item2item.PopAll(0)\n\tsuite.Len(scores, 10)\n\tfor i := 1; i <= 10; i++ {\n\t\tsuite.Equal(strconv.Itoa(i), scores[i-1].Id)\n\t}\n}\n\nfunc (suite *ItemToItemTestSuite) TestAuto() {\n\ttimestamp := time.Now()\n\tidf := make([]float32, 101)\n\tfor i := range idf {\n\t\tidf[i] = 1\n\t}\n\titem2item, err := newAutoItemToItem(config.ItemToItemConfig{}, 10, timestamp, idf, idf)\n\tsuite.NoError(err)\n\n\tfor i := 0; i < 100; i++ {\n\t\titem := &data.Item{ItemId: strconv.Itoa(i)}\n\t\tfeedback := make([]int32, 0, 100-i)\n\t\tif i%2 == 0 {\n\t\t\tlabels := make(map[string]any)\n\t\t\tfor j := 1; j <= 100-i; j++ {\n\t\t\t\tlabels[strconv.Itoa(j)] = []dataset.ID{dataset.ID(j)}\n\t\t\t}\n\t\t\titem.Labels = labels\n\t\t} else {\n\t\t\tfor j := 1; j <= 100-i; j++ {\n\t\t\t\tfeedback = append(feedback, int32(j))\n\t\t\t}\n\t\t}\n\t\titem2item.Push(item, feedback)\n\t}\n\n\tscores0 := item2item.PopAll(0)\n\tsuite.Len(scores0, 10)\n\tfor i := 1; i <= 10; i++ {\n\t\tsuite.Equal(strconv.Itoa(i*2), scores0[i-1].Id)\n\t}\n\tscores1 := item2item.PopAll(1)\n\tsuite.Len(scores1, 10)\n\tfor i := 1; i <= 10; i++ {\n\t\tsuite.Equal(strconv.Itoa(i*2+1), scores1[i-1].Id)\n\t}\n}\n\nfunc (suite *ItemToItemTestSuite) TestChat() {\n\tmockAI := mock.NewOpenAIServer()\n\tgo func() {\n\t\t_ = mockAI.Start()\n\t}()\n\tmockAI.Ready()\n\tdefer mockAI.Close()\n\n\ttimestamp := time.Now()\n\titem2item, err := newChatItemToItem(config.ItemToItemConfig{\n\t\tColumn: \"item.Labels.embeddings\",\n\t\tPrompt: \"Please generate similar items for {{ item.Labels.title }}.\",\n\t}, 10, timestamp, config.OpenAIConfig{\n\t\tBaseURL:             mockAI.BaseURL(),\n\t\tAuthToken:           mockAI.AuthToken(),\n\t\tChatCompletionModel: \"deepseek-r1\",\n\t\tEmbeddingModel:      \"text-similarity-ada-001\",\n\t})\n\tsuite.NoError(err)\n\n\tfor i := 0; i < 100; i++ {\n\t\tembedding := mock.Hash(\"Please generate similar items for item_0.\")\n\t\tfloats.AddConst(embedding, float32(i+1))\n\t\titem2item.Push(&data.Item{\n\t\t\tItemId: strconv.Itoa(i),\n\t\t\tLabels: map[string]any{\n\t\t\t\t\"title\":      \"item_\" + strconv.Itoa(i),\n\t\t\t\t\"embeddings\": embedding,\n\t\t\t},\n\t\t}, nil)\n\t}\n\n\tscores := item2item.PopAll(0)\n\tsuite.Len(scores, 10)\n\tfor i := 1; i <= 10; i++ {\n\t\tsuite.Equal(strconv.Itoa(i), scores[i-1].Id)\n\t}\n}\n\nfunc TestItemToItem(t *testing.T) {\n\tsuite.Run(t, new(ItemToItemTestSuite))\n}\n"
  },
  {
    "path": "logics/non_personalized.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage logics\n\nimport (\n\t\"reflect\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/expr-lang/expr\"\n\t\"github.com/expr-lang/expr/vm\"\n\t\"github.com/gorse-io/gorse/common/heap\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/juju/errors\"\n\t\"github.com/samber/lo\"\n\t\"go.uber.org/zap\"\n)\n\ntype NonPersonalized struct {\n\tsync.Mutex\n\tname       string\n\ttimestamp  time.Time\n\tscoreFunc  *vm.Program\n\tfilterFunc *vm.Program\n\theapSize   int\n\theaps      map[string]*heap.TopKFilter[string, float64]\n}\n\nfunc NewNonPersonalized(cfg config.NonPersonalizedConfig, n int, timestamp time.Time) (*NonPersonalized, error) {\n\t// Compile score expression\n\tscoreFunc, err := expr.Compile(cfg.Score, expr.Env(map[string]any{\n\t\t\"item\":     data.Item{},\n\t\t\"feedback\": []data.Feedback{},\n\t}))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tswitch scoreFunc.Node().Type().Kind() {\n\tcase reflect.Float64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:\n\tdefault:\n\t\treturn nil, errors.New(\"score function must return float64\")\n\t}\n\t// Compile filter expression\n\tvar filterFunc *vm.Program\n\tif cfg.Filter != \"\" {\n\t\tfilterFunc, err = expr.Compile(cfg.Filter, expr.Env(map[string]any{\n\t\t\t\"item\":     data.Item{},\n\t\t\t\"feedback\": []data.Feedback{},\n\t\t}))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif filterFunc.Node().Type().Kind() != reflect.Bool {\n\t\t\treturn nil, errors.New(\"filter function must return bool\")\n\t\t}\n\t}\n\t// Initialize heap\n\theaps := make(map[string]*heap.TopKFilter[string, float64])\n\theaps[\"\"] = heap.NewTopKFilter[string, float64](n)\n\treturn &NonPersonalized{\n\t\tname:       cfg.Name,\n\t\ttimestamp:  timestamp,\n\t\tscoreFunc:  scoreFunc,\n\t\tfilterFunc: filterFunc,\n\t\theapSize:   n,\n\t\theaps:      heaps,\n\t}, nil\n}\n\nfunc (l *NonPersonalized) Push(item data.Item, feedback []data.Feedback) {\n\t// Skip hidden items\n\tif item.IsHidden {\n\t\treturn\n\t}\n\t// Evaluate filter function\n\tif l.filterFunc != nil {\n\t\tresult, err := expr.Run(l.filterFunc, map[string]any{\n\t\t\t\"item\":     item,\n\t\t\t\"feedback\": feedback,\n\t\t})\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"evaluate filter function\", zap.Error(err))\n\t\t\treturn\n\t\t}\n\t\tif !result.(bool) {\n\t\t\treturn\n\t\t}\n\t}\n\t// Evaluate score function\n\tresult, err := expr.Run(l.scoreFunc, map[string]any{\n\t\t\"item\":     item,\n\t\t\"feedback\": feedback,\n\t})\n\tif err != nil {\n\t\tlog.Logger().Error(\"evaluate score function\", zap.Error(err))\n\t\treturn\n\t}\n\tvar score float64\n\tswitch typed := result.(type) {\n\tcase float64:\n\t\tscore = typed\n\tcase int:\n\t\tscore = float64(typed)\n\tcase int8:\n\t\tscore = float64(typed)\n\tcase int16:\n\t\tscore = float64(typed)\n\tcase int32:\n\t\tscore = float64(typed)\n\tcase int64:\n\t\tscore = float64(typed)\n\tdefault:\n\t\tlog.Logger().Error(\"score function must return float64\", zap.Any(\"result\", result))\n\t\treturn\n\t}\n\t// Add to heap\n\tl.Lock()\n\tdefer l.Unlock()\n\tl.heaps[\"\"].Push(item.ItemId, score)\n\tfor _, group := range item.Categories {\n\t\tif _, exist := l.heaps[group]; !exist {\n\t\t\tl.heaps[group] = heap.NewTopKFilter[string, float64](l.heapSize)\n\t\t}\n\t\tl.heaps[group].Push(item.ItemId, score)\n\t}\n}\n\nfunc (l *NonPersonalized) PopAll() []cache.Score {\n\tscores := make(map[string]*cache.Score)\n\tl.Lock()\n\tdefer l.Unlock()\n\tfor category, h := range l.heaps {\n\t\telems := h.PopAll()\n\t\tfor _, elem := range elems {\n\t\t\tif _, exist := scores[elem.Value]; !exist {\n\t\t\t\tscores[elem.Value] = &cache.Score{\n\t\t\t\t\tId:         elem.Value,\n\t\t\t\t\tScore:      elem.Weight,\n\t\t\t\t\tCategories: []string{category},\n\t\t\t\t\tTimestamp:  l.timestamp,\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tscores[elem.Value].Categories = append(scores[elem.Value].Categories, category)\n\t\t\t}\n\t\t}\n\t}\n\tresult := lo.MapToSlice(scores, func(_ string, v *cache.Score) cache.Score {\n\t\treturn *v\n\t})\n\tsort.Slice(result, func(i, j int) bool {\n\t\treturn result[i].Score > result[j].Score\n\t})\n\treturn result\n}\n\nfunc (l *NonPersonalized) Name() string {\n\treturn l.name\n}\n\nfunc (l *NonPersonalized) Timestamp() time.Time {\n\treturn l.timestamp\n}\n"
  },
  {
    "path": "logics/non_personalized_test.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage logics\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestLatest(t *testing.T) {\n\ttimestamp := time.Now()\n\tlatest, err := NewNonPersonalized(config.NonPersonalizedConfig{\n\t\tName:  \"latest\",\n\t\tScore: \"item.Timestamp.Unix()\",\n\t}, 10, timestamp)\n\tassert.NoError(t, err)\n\tfor i := 0; i < 100; i++ {\n\t\titem := data.Item{ItemId: strconv.Itoa(i), Timestamp: timestamp.Add(time.Duration(-i) * time.Second)}\n\t\tlatest.Push(item, nil)\n\t}\n\tscores := latest.PopAll()\n\tassert.Len(t, scores, 10)\n\tfor i := 0; i < 10; i++ {\n\t\tassert.Equal(t, strconv.Itoa(i), scores[i].Id)\n\t\tassert.Equal(t, float64(timestamp.Add(time.Duration(-i)*time.Second).Unix()), scores[i].Score)\n\t\tassert.Equal(t, timestamp, scores[i].Timestamp)\n\t}\n}\n\nfunc TestPopular(t *testing.T) {\n\ttimestamp := time.Now()\n\tpopular, err := NewNonPersonalized(config.NonPersonalizedConfig{\n\t\tName:  \"popular\",\n\t\tScore: \"len(feedback)\",\n\t}, 10, timestamp)\n\tassert.NoError(t, err)\n\tfor i := 0; i < 100; i++ {\n\t\titem := data.Item{ItemId: strconv.Itoa(i)}\n\t\tfeedback := make([]data.Feedback, i)\n\t\tpopular.Push(item, feedback)\n\t}\n\tscores := popular.PopAll()\n\tassert.Len(t, scores, 10)\n\tfor i := 0; i < 10; i++ {\n\t\tassert.Equal(t, strconv.Itoa(99-i), scores[i].Id)\n\t\tassert.Equal(t, float64(99-i), scores[i].Score)\n\t}\n}\n\nfunc TestPopularWindow(t *testing.T) {\n\t// Create popular recommender\n\ttimestamp := time.Now()\n\tpopular, err := NewNonPersonalized(config.NonPersonalizedConfig{\n\t\tName:   \"popular\",\n\t\tScore:  \"len(feedback)\",\n\t\tFilter: fmt.Sprintf(\"(now() - item.Timestamp).Nanoseconds() < %d\", time.Hour.Nanoseconds()),\n\t}, 10, timestamp)\n\tassert.NoError(t, err)\n\t// Add items\n\tfor i := 0; i < 100; i++ {\n\t\titem := data.Item{ItemId: strconv.Itoa(i), Timestamp: timestamp.Add(time.Second - time.Hour)}\n\t\tfeedback := make([]data.Feedback, i)\n\t\tpopular.Push(item, feedback)\n\t}\n\n\t// Add outdated items\n\tfor i := 100; i < 110; i++ {\n\t\titem := data.Item{ItemId: strconv.Itoa(i), Timestamp: timestamp.Add(-time.Hour)}\n\t\tfeedback := make([]data.Feedback, i)\n\t\tpopular.Push(item, feedback)\n\t}\n\n\t// Check result\n\tscores := popular.PopAll()\n\tassert.Len(t, scores, 10)\n\tfor i := 0; i < 10; i++ {\n\t\tassert.Equal(t, strconv.Itoa(99-i), scores[i].Id)\n\t\tassert.Equal(t, float64(99-i), scores[i].Score)\n\t}\n}\n\nfunc TestFilter(t *testing.T) {\n\ttimestamp := time.Now()\n\tlatest, err := NewNonPersonalized(config.NonPersonalizedConfig{\n\t\tName:   \"latest\",\n\t\tScore:  \"item.Timestamp.Unix()\",\n\t\tFilter: \"!item.IsHidden\",\n\t}, 10, timestamp)\n\tassert.NoError(t, err)\n\tfor i := 0; i < 100; i++ {\n\t\titem := data.Item{ItemId: strconv.Itoa(i), Timestamp: timestamp.Add(time.Duration(-i) * time.Second)}\n\t\titem.IsHidden = i < 10\n\t\tlatest.Push(item, nil)\n\t}\n\tscores := latest.PopAll()\n\tassert.Len(t, scores, 10)\n\tfor i := 0; i < 10; i++ {\n\t\tassert.Equal(t, strconv.Itoa(i+10), scores[i].Id)\n\t\tassert.Equal(t, float64(timestamp.Add(time.Duration(-i-10)*time.Second).Unix()), scores[i].Score)\n\t\tassert.Equal(t, timestamp, scores[i].Timestamp)\n\t}\n}\n\nfunc TestHidden(t *testing.T) {\n\ttimestamp := time.Now()\n\tlatest, err := NewNonPersonalized(config.NonPersonalizedConfig{\n\t\tName:  \"latest\",\n\t\tScore: \"item.Timestamp.Unix()\",\n\t}, 10, timestamp)\n\tassert.NoError(t, err)\n\tfor i := 0; i < 100; i++ {\n\t\titem := data.Item{ItemId: strconv.Itoa(i), Timestamp: timestamp.Add(time.Duration(-i) * time.Second)}\n\t\titem.IsHidden = i < 10\n\t\tlatest.Push(item, nil)\n\t}\n\tscores := latest.PopAll()\n\tassert.Len(t, scores, 10)\n\tfor i := 0; i < 10; i++ {\n\t\tassert.Equal(t, strconv.Itoa(i+10), scores[i].Id)\n\t\tassert.Equal(t, float64(timestamp.Add(time.Duration(-i-10)*time.Second).Unix()), scores[i].Score)\n\t\tassert.Equal(t, timestamp, scores[i].Timestamp)\n\t}\n}\n\nfunc TestMostStarredWeekly(t *testing.T) {\n\t// Create non-personalized recommender\n\ttimestamp := time.Now()\n\tmostStarredWeekly, err := NewNonPersonalized(config.NonPersonalizedConfig{\n\t\tName:   \"most_starred_weekly\",\n\t\tScore:  \"count(feedback, .FeedbackType == 'star')\",\n\t\tFilter: \"(now() - item.Timestamp).Hours() < 168\",\n\t}, 10, timestamp)\n\tassert.NoError(t, err)\n\n\t// Add items\n\tfor i := 0; i < 100; i++ {\n\t\titem := data.Item{ItemId: strconv.Itoa(i), Timestamp: timestamp.Add(-167 * time.Hour)}\n\t\tvar feedback []data.Feedback\n\t\tfor j := 0; j < i; j++ {\n\t\t\tfeedback = append(feedback, data.Feedback{\n\t\t\t\tFeedbackKey: data.FeedbackKey{\n\t\t\t\t\tFeedbackType: \"star\",\n\t\t\t\t\tUserId:       strconv.Itoa(j),\n\t\t\t\t\tItemId:       strconv.Itoa(i),\n\t\t\t\t},\n\t\t\t\tTimestamp: timestamp,\n\t\t\t})\n\t\t\tfeedback = append(feedback, data.Feedback{\n\t\t\t\tFeedbackKey: data.FeedbackKey{\n\t\t\t\t\tFeedbackType: \"like\",\n\t\t\t\t\tUserId:       strconv.Itoa(j),\n\t\t\t\t\tItemId:       strconv.Itoa(i),\n\t\t\t\t},\n\t\t\t\tTimestamp: timestamp,\n\t\t\t})\n\t\t}\n\t\tmostStarredWeekly.Push(item, feedback)\n\t}\n\n\t// Add outdated items\n\tfor i := 100; i < 110; i++ {\n\t\titem := data.Item{ItemId: strconv.Itoa(i), Timestamp: timestamp.Add(-168 * time.Hour)}\n\t\tvar feedback []data.Feedback\n\t\tfor j := 0; j < i; j++ {\n\t\t\tfeedback = append(feedback, data.Feedback{\n\t\t\t\tFeedbackKey: data.FeedbackKey{\n\t\t\t\t\tFeedbackType: \"star\",\n\t\t\t\t\tUserId:       strconv.Itoa(j),\n\t\t\t\t\tItemId:       strconv.Itoa(i),\n\t\t\t\t},\n\t\t\t\tTimestamp: timestamp.Add(-time.Hour * 169),\n\t\t\t})\n\t\t}\n\t\tmostStarredWeekly.Push(item, feedback)\n\t}\n\n\t// Check result\n\tscores := mostStarredWeekly.PopAll()\n\tassert.Len(t, scores, 10)\n\tfor i := 0; i < 10; i++ {\n\t\tassert.Equal(t, strconv.Itoa(99-i), scores[i].Id)\n\t\tassert.Equal(t, float64(99-i), scores[i].Score)\n\t}\n}\n"
  },
  {
    "path": "logics/recommend.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage logics\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"time\"\n\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/gorse-io/gorse/common/expression\"\n\t\"github.com/gorse-io/gorse/common/heap\"\n\t\"github.com/gorse-io/gorse/common/util\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/juju/errors\"\n\t\"github.com/samber/lo\"\n)\n\nconst (\n\tLatestRecommender          = \"latest\"\n\tNonPersonalizedRecommender = \"non-personalized/\"\n\tItemToItemRecommender      = \"item-to-item/\"\n\tUserToUserRecommender      = \"user-to-user/\"\n\tExternalRecommender        = \"external/\"\n\tCollaborativeRecommender   = \"collaborative\"\n)\n\ntype Recommender struct {\n\tconfig      config.RecommendConfig\n\tcacheClient cache.Database\n\tdataClient  data.Database\n\n\tonline       bool\n\tcoldstart    bool\n\tuserId       string\n\tuserFeedback []data.Feedback\n\tcategories   []string\n\texcludeSet   mapset.Set[string]\n}\n\ntype RecommenderFunc func(ctx context.Context) ([]cache.Score, string, error)\n\nfunc NewRecommender(config config.RecommendConfig, cacheClient cache.Database, dataClient data.Database, online bool, userId string, categories []string) (*Recommender, error) {\n\t// Load user feedback\n\tuserFeedback, err := dataClient.GetUserFeedback(context.Background(), userId, lo.ToPtr(time.Now()))\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\texcludeSet := mapset.NewSet[string]()\n\tcoldstart := true\n\tfor _, feedback := range userFeedback {\n\t\tif !config.Replacement.EnableReplacement || !online {\n\t\t\texcludeSet.Add(feedback.ItemId)\n\t\t}\n\t\tif expression.MatchFeedbackTypeExpressions(config.DataSource.PositiveFeedbackTypes, feedback.FeedbackType, feedback.Value) {\n\t\t\tcoldstart = false\n\t\t}\n\t}\n\treturn &Recommender{\n\t\tconfig:       config,\n\t\tcacheClient:  cacheClient,\n\t\tdataClient:   dataClient,\n\t\tuserId:       userId,\n\t\tuserFeedback: userFeedback,\n\t\tonline:       online,\n\t\tcoldstart:    coldstart,\n\t\tcategories:   categories,\n\t\texcludeSet:   excludeSet,\n\t}, nil\n}\n\nfunc (r *Recommender) ExcludeSet() mapset.Set[string] {\n\treturn r.excludeSet\n}\n\nfunc (r *Recommender) UserFeedback() []data.Feedback {\n\treturn r.userFeedback\n}\n\nfunc (r *Recommender) IsColdStart() bool {\n\treturn r.coldstart\n}\n\nfunc (r *Recommender) Recommend(ctx context.Context, limit int) (result []cache.Score, err error) {\n\tif !strings.EqualFold(r.config.Ranker.Type, \"none\") {\n\t\tscores, err := r.cacheClient.SearchScores(ctx, cache.Recommend, r.userId, r.categories, 0, r.config.CacheSize)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tresult = make([]cache.Score, 0, len(scores))\n\t\tfor _, score := range scores {\n\t\t\tif !r.excludeSet.Contains(score.Id) {\n\t\t\t\tr.excludeSet.Add(score.Id)\n\t\t\t\tresult = append(result, score)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tresult, _, err = r.RecommendSequential(ctx, result, r.config.CacheSize, r.config.Ranker.Recommenders...)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t}\n\tif len(result) >= limit && limit > 0 {\n\t\treturn result[:limit], nil\n\t}\n\tresult, _, err = r.RecommendSequential(ctx, result, limit, r.config.Fallback.Recommenders...)\n\treturn result, errors.Trace(err)\n}\n\n// RecommendSequential recommend items from multiple recommenders sequentially util reaching the limit.\n// If limit <= 0, all recommendations are returned.\nfunc (r *Recommender) RecommendSequential(ctx context.Context, result []cache.Score, limit int, names ...string) ([]cache.Score, string, error) {\n\tvar digests []string\n\tfor _, name := range names {\n\t\trecommenderFunc, err := r.parse(name)\n\t\tif err != nil {\n\t\t\treturn nil, \"\", errors.Trace(err)\n\t\t}\n\t\tscores, digest, err := recommenderFunc(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, \"\", errors.Trace(err)\n\t\t}\n\t\tfor _, score := range scores {\n\t\t\tr.excludeSet.Add(score.Id)\n\t\t}\n\t\tresult = append(result, scores...)\n\t\tdigests = append(digests, digest)\n\t\tif limit > 0 && len(result) >= limit {\n\t\t\treturn result[:limit], util.MD5(digests...), nil\n\t\t}\n\t}\n\treturn result, util.MD5(digests...), nil\n}\n\nfunc (r *Recommender) parse(fullname string) (RecommenderFunc, error) {\n\tif fullname == CollaborativeRecommender {\n\t\treturn r.recommendCollaborative, nil\n\t} else if fullname == LatestRecommender {\n\t\treturn r.recommendLatest, nil\n\t} else if strings.HasPrefix(fullname, NonPersonalizedRecommender) {\n\t\tname := strings.TrimPrefix(fullname, NonPersonalizedRecommender)\n\t\treturn r.recommendNonPersonalized(name), nil\n\t} else if strings.HasPrefix(fullname, ItemToItemRecommender) {\n\t\tname := strings.TrimPrefix(fullname, ItemToItemRecommender)\n\t\treturn r.recommendItemToItem(name), nil\n\t} else if strings.HasPrefix(fullname, UserToUserRecommender) {\n\t\tname := strings.TrimPrefix(fullname, UserToUserRecommender)\n\t\treturn r.recommendUserToUser(name), nil\n\t} else if strings.HasPrefix(fullname, ExternalRecommender) {\n\t\tname := strings.TrimPrefix(fullname, ExternalRecommender)\n\t\treturn r.recommendExternal(name), nil\n\t} else {\n\t\treturn nil, errors.Errorf(\"unknown recommender: %s\", fullname)\n\t}\n}\n\nfunc (r *Recommender) recommendLatest(ctx context.Context) ([]cache.Score, string, error) {\n\titems, err := r.dataClient.GetLatestItems(ctx, r.config.CacheSize, r.categories)\n\tif err != nil {\n\t\treturn nil, \"\", errors.Trace(err)\n\t}\n\tscores := make([]cache.Score, 0, len(items))\n\tfor _, item := range items {\n\t\tif !r.excludeSet.Contains(item.ItemId) {\n\t\t\tscores = append(scores, cache.Score{\n\t\t\t\tId:         item.ItemId,\n\t\t\t\tScore:      float64(item.Timestamp.Unix()),\n\t\t\t\tCategories: item.Categories,\n\t\t\t})\n\t\t}\n\t}\n\treturn scores, \"latest\", nil\n}\n\nfunc (r *Recommender) recommendNonPersonalized(name string) RecommenderFunc {\n\treturn func(ctx context.Context) ([]cache.Score, string, error) {\n\t\tvar categories []string\n\t\tif len(r.categories) == 0 {\n\t\t\tcategories = []string{\"\"}\n\t\t} else {\n\t\t\tcategories = r.categories\n\t\t}\n\t\t// fetch items from cache\n\t\titems, err := r.cacheClient.SearchScores(ctx, cache.NonPersonalized, name, categories, 0, r.config.CacheSize)\n\t\tif err != nil {\n\t\t\treturn nil, \"\", errors.Trace(err)\n\t\t}\n\t\t// read digest\n\t\tdigest, err := r.cacheClient.Get(ctx, cache.Key(cache.NonPersonalizedDigest, name)).String()\n\t\tif err != nil {\n\t\t\treturn nil, \"\", errors.Trace(err)\n\t\t}\n\t\t// remove excluded items\n\t\treturn lo.Filter(items, func(item cache.Score, index int) bool {\n\t\t\treturn !r.excludeSet.Contains(item.Id)\n\t\t}), digest, nil\n\t}\n}\n\nfunc (r *Recommender) recommendCollaborative(ctx context.Context) ([]cache.Score, string, error) {\n\t// fetch items from cache\n\titems, err := r.cacheClient.SearchScores(ctx, cache.CollaborativeFiltering, r.userId, r.categories, 0, r.config.CacheSize)\n\tif err != nil {\n\t\treturn nil, \"\", errors.Trace(err)\n\t}\n\t// read digest\n\tdigest, err := r.cacheClient.Get(ctx, cache.Key(cache.CollaborativeFilteringDigest, r.userId)).String()\n\tif err != nil {\n\t\treturn nil, \"\", errors.Trace(err)\n\t}\n\t// remove excluded items\n\treturn lo.Filter(items, func(item cache.Score, index int) bool {\n\t\treturn !r.excludeSet.Contains(item.Id)\n\t}), digest, nil\n}\n\nfunc (r *Recommender) recommendItemToItem(name string) RecommenderFunc {\n\treturn func(ctx context.Context) ([]cache.Score, string, error) {\n\t\t// filter positive feedbacks\n\t\tdata.SortFeedbacks(r.userFeedback)\n\t\tuserFeedback := make([]data.Feedback, 0, r.config.CacheSize)\n\t\tfor _, feedback := range r.userFeedback {\n\t\t\tif r.online && r.config.ContextSize <= len(userFeedback) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif expression.MatchFeedbackTypeExpressions(r.config.DataSource.PositiveFeedbackTypes, feedback.FeedbackType, feedback.Value) {\n\t\t\t\tuserFeedback = append(userFeedback, feedback)\n\t\t\t}\n\t\t}\n\t\t// collect scores\n\t\tscores := make(map[string]float64)\n\t\tcategories := make(map[string][]string)\n\t\tdigests := mapset.NewSet[string]()\n\t\tfor _, feedback := range userFeedback {\n\t\t\tsimilarItems, err := r.cacheClient.SearchScores(ctx, cache.ItemToItem, cache.Key(name, feedback.ItemId), r.categories, 0, r.config.CacheSize)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, \"\", errors.Trace(err)\n\t\t\t}\n\t\t\tdigest, err := r.cacheClient.Get(ctx, cache.Key(cache.ItemToItemDigest, name, feedback.ItemId)).String()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, \"\", errors.Trace(err)\n\t\t\t}\n\t\t\tfor _, item := range similarItems {\n\t\t\t\tif !r.excludeSet.Contains(item.Id) {\n\t\t\t\t\tscores[item.Id] += item.Score\n\t\t\t\t\tcategories[item.Id] = item.Categories\n\t\t\t\t\tdigests.Add(digest)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// collect top scores\n\t\tfilter := heap.NewTopKFilter[string, float64](r.config.CacheSize)\n\t\tfor id, score := range scores {\n\t\t\tfilter.Push(id, score)\n\t\t}\n\t\telems := filter.PopAll()\n\t\treturn lo.Map(elems, func(elem heap.Elem[string, float64], _ int) cache.Score {\n\t\t\treturn cache.Score{\n\t\t\t\tId:         elem.Value,\n\t\t\t\tScore:      elem.Weight,\n\t\t\t\tCategories: categories[elem.Value],\n\t\t\t}\n\t\t}), strings.Join(digests.ToSlice(), \"\"), nil\n\t}\n}\n\nfunc (r *Recommender) recommendUserToUser(name string) RecommenderFunc {\n\treturn func(ctx context.Context) ([]cache.Score, string, error) {\n\t\tscores := make(map[string]float64)\n\t\t// load similar users\n\t\tsimilarUsers, err := r.cacheClient.SearchScores(ctx, cache.UserToUser, cache.Key(name, r.userId), nil, 0, r.config.CacheSize)\n\t\tif err != nil {\n\t\t\treturn nil, \"\", errors.Trace(err)\n\t\t}\n\t\t// read digest\n\t\tdigest, err := r.cacheClient.Get(ctx, cache.Key(cache.UserToUserDigest, name, r.userId)).String()\n\t\tif err != nil {\n\t\t\treturn nil, \"\", errors.Trace(err)\n\t\t}\n\t\t// aggregate scores\n\t\tfor _, user := range similarUsers {\n\t\t\t// load historical feedback\n\t\t\tfeedbacks, err := r.dataClient.GetUserFeedback(ctx, user.Id, lo.ToPtr(time.Now()), r.config.DataSource.PositiveFeedbackTypes...)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, \"\", errors.Trace(err)\n\t\t\t}\n\t\t\t// add unseen items\n\t\t\tfor _, feedback := range feedbacks {\n\t\t\t\tif !r.excludeSet.Contains(feedback.ItemId) {\n\t\t\t\t\tscores[feedback.ItemId] += user.Score\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// collect top k\n\t\tfilter := heap.NewTopKFilter[string, float64](r.config.CacheSize)\n\t\tfor id, score := range scores {\n\t\t\tfilter.Push(id, score)\n\t\t}\n\t\telems := filter.PopAll()\n\t\t// filter by categories\n\t\tresults := make([]cache.Score, 0, len(elems))\n\t\tids := lo.Map(elems, func(elem heap.Elem[string, float64], _ int) string {\n\t\t\treturn elem.Value\n\t\t})\n\t\titems, err := r.dataClient.BatchGetItems(ctx, ids)\n\t\tif err != nil {\n\t\t\treturn nil, \"\", errors.Trace(err)\n\t\t}\n\t\titemsMap := make(map[string]data.Item)\n\t\tfor _, item := range items {\n\t\t\titemsMap[item.ItemId] = item\n\t\t}\n\t\tfor _, elem := range elems {\n\t\t\tif item, ok := itemsMap[elem.Value]; ok && lo.Every(item.Categories, r.categories) {\n\t\t\t\tresults = append(results, cache.Score{\n\t\t\t\t\tId:         item.ItemId,\n\t\t\t\t\tScore:      elem.Weight,\n\t\t\t\t\tCategories: item.Categories,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\treturn results, digest, nil\n\t}\n}\n\nfunc (r *Recommender) recommendExternal(name string) RecommenderFunc {\n\treturn func(ctx context.Context) ([]cache.Score, string, error) {\n\t\tvar externalConfig config.ExternalConfig\n\t\tfor _, extConfig := range r.config.External {\n\t\t\tif extConfig.Name == name {\n\t\t\t\texternalConfig = extConfig\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif len(r.categories) > 0 {\n\t\t\t// external recommenders do not support categories\n\t\t\treturn nil, externalConfig.Hash(), nil\n\t\t}\n\n\t\texternal, err := NewExternal(externalConfig)\n\t\tif err != nil {\n\t\t\treturn nil, \"\", errors.Trace(err)\n\t\t}\n\t\tdefer external.Close()\n\t\titems, err := external.Pull(r.userId)\n\t\tif err != nil {\n\t\t\treturn nil, \"\", errors.Trace(err)\n\t\t}\n\t\tscores := make([]cache.Score, 0, len(items))\n\t\tfor _, itemId := range items {\n\t\t\tif !r.excludeSet.Contains(itemId) {\n\t\t\t\tscores = append(scores, cache.Score{\n\t\t\t\t\tId: itemId,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\treturn scores, externalConfig.Hash(), nil\n\t}\n}\n"
  },
  {
    "path": "logics/recommend_test.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage logics\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\ntype RecommenderTestSuite struct {\n\tsuite.Suite\n\tdataClient  data.Database\n\tcacheClient cache.Database\n}\n\nfunc (suite *RecommenderTestSuite) SetupSuite() {\n\tvar err error\n\t// open database\n\tsuite.dataClient, err = data.Open(fmt.Sprintf(\"sqlite://%s/data.db\", suite.T().TempDir()), \"\")\n\tsuite.NoError(err)\n\tsuite.cacheClient, err = cache.Open(fmt.Sprintf(\"sqlite://%s/cache.db\", suite.T().TempDir()), \"\")\n\tsuite.NoError(err)\n\t// init database\n\terr = suite.dataClient.Init()\n\tsuite.NoError(err)\n\terr = suite.cacheClient.Init()\n\tsuite.NoError(err)\n}\n\nfunc (suite *RecommenderTestSuite) TearDownSuite() {\n\terr := suite.dataClient.Close()\n\tsuite.NoError(err)\n\terr = suite.cacheClient.Close()\n\tsuite.NoError(err)\n}\n\nfunc (suite *RecommenderTestSuite) TestLatest() {\n\titems := make([]data.Item, 20)\n\tfor i := 0; i < 20; i++ {\n\t\titems[i] = data.Item{\n\t\t\tItemId:    fmt.Sprintf(\"item_%d\", i),\n\t\t\tTimestamp: time.Unix(int64(i), 0),\n\t\t}\n\t\tif i%2 == 0 {\n\t\t\titems[i].Categories = []string{\"cat_1\"}\n\t\t}\n\t}\n\terr := suite.dataClient.BatchInsertItems(suite.T().Context(), items)\n\tsuite.NoError(err)\n\n\tfeedback := make([]data.Feedback, 10)\n\tfor i := 0; i < 10; i++ {\n\t\tfeedback[i] = data.Feedback{\n\t\t\tFeedbackKey: data.FeedbackKey{\n\t\t\t\tFeedbackType: \"click\",\n\t\t\t\tUserId:       \"user_1\",\n\t\t\t\tItemId:       fmt.Sprintf(\"item_%d\", i),\n\t\t\t},\n\t\t}\n\t}\n\terr = suite.dataClient.BatchInsertFeedback(suite.T().Context(), feedback, true, true, false)\n\tsuite.NoError(err)\n\n\trecommender, err := NewRecommender(config.RecommendConfig{}, suite.cacheClient, suite.dataClient, true, \"user_1\", nil)\n\tsuite.NoError(err)\n\tscores, digest, err := recommender.recommendLatest(suite.T().Context())\n\tsuite.NoError(err)\n\tsuite.Equal(\"latest\", digest)\n\tif suite.Equal(10, len(scores)) {\n\t\tfor i := 0; i < 10; i++ {\n\t\t\tsuite.Equal(fmt.Sprintf(\"item_%d\", 19-i), scores[i].Id)\n\t\t\tsuite.Equal(float64(19-i), scores[i].Score)\n\t\t}\n\t}\n\n\trecommender, err = NewRecommender(config.RecommendConfig{}, suite.cacheClient, suite.dataClient, true, \"user_1\", []string{\"cat_1\"})\n\tsuite.NoError(err)\n\tscores, digest, err = recommender.recommendLatest(suite.T().Context())\n\tsuite.NoError(err)\n\tsuite.Equal(\"latest\", digest)\n\tif suite.Equal(5, len(scores)) {\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tsuite.Equal(fmt.Sprintf(\"item_%d\", 18-2*i), scores[i].Id)\n\t\t\tsuite.Equal(float64(18-2*i), scores[i].Score)\n\t\t}\n\t}\n}\n\nfunc (suite *RecommenderTestSuite) TestCollaborative() {\n\trecommends := make([]cache.Score, 20)\n\tfor i := 0; i < 20; i++ {\n\t\trecommends[i] = cache.Score{\n\t\t\tId:    fmt.Sprintf(\"item_%d\", i),\n\t\t\tScore: float64(i),\n\t\t}\n\t\tif i%2 == 0 {\n\t\t\trecommends[i].Categories = []string{\"cat_1\"}\n\t\t}\n\t}\n\terr := suite.cacheClient.AddScores(suite.T().Context(), cache.CollaborativeFiltering, \"user_1\", recommends)\n\tsuite.NoError(err)\n\terr = suite.cacheClient.Set(suite.T().Context(), cache.String(cache.Key(cache.CollaborativeFilteringDigest, \"user_1\"), \"digest\"))\n\tsuite.NoError(err)\n\n\tfeedback := make([]data.Feedback, 10)\n\tfor i := 0; i < 10; i++ {\n\t\tfeedback[i] = data.Feedback{\n\t\t\tFeedbackKey: data.FeedbackKey{\n\t\t\t\tFeedbackType: \"click\",\n\t\t\t\tUserId:       \"user_1\",\n\t\t\t\tItemId:       fmt.Sprintf(\"item_%d\", i),\n\t\t\t},\n\t\t}\n\t}\n\terr = suite.dataClient.BatchInsertFeedback(suite.T().Context(), feedback, true, true, false)\n\tsuite.NoError(err)\n\n\trecommender, err := NewRecommender(config.RecommendConfig{}, suite.cacheClient, suite.dataClient, true, \"user_1\", nil)\n\tsuite.NoError(err)\n\tscores, digest, err := recommender.recommendCollaborative(suite.T().Context())\n\tsuite.NoError(err)\n\tsuite.Equal(\"digest\", digest)\n\tif suite.Equal(10, len(scores)) {\n\t\tfor i := 0; i < 10; i++ {\n\t\t\tsuite.Equal(fmt.Sprintf(\"item_%d\", 19-i), scores[i].Id)\n\t\t\tsuite.Equal(float64(19-i), scores[i].Score)\n\t\t}\n\t}\n\n\trecommender, err = NewRecommender(config.RecommendConfig{}, suite.cacheClient, suite.dataClient, true, \"user_1\", []string{\"cat_1\"})\n\tsuite.NoError(err)\n\tscores, digest, err = recommender.recommendCollaborative(suite.T().Context())\n\tsuite.NoError(err)\n\tsuite.Equal(\"digest\", digest)\n\tif suite.Equal(5, len(scores)) {\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tsuite.Equal(fmt.Sprintf(\"item_%d\", 18-2*i), scores[i].Id)\n\t\t\tsuite.Equal(float64(18-2*i), scores[i].Score)\n\t\t}\n\t}\n}\n\nfunc (suite *RecommenderTestSuite) TestNonPersonalized() {\n\trecommends := make([]cache.Score, 20)\n\tfor i := 0; i < 20; i++ {\n\t\trecommends[i] = cache.Score{\n\t\t\tId:    fmt.Sprintf(\"item_%d\", i),\n\t\t\tScore: float64(i),\n\t\t}\n\t\tif i%2 == 0 {\n\t\t\trecommends[i].Categories = []string{\"\", \"cat_1\"}\n\t\t} else {\n\t\t\trecommends[i].Categories = []string{\"\"}\n\t\t}\n\t}\n\terr := suite.cacheClient.AddScores(suite.T().Context(), cache.NonPersonalized, \"a\", recommends)\n\tsuite.NoError(err)\n\terr = suite.cacheClient.Set(suite.T().Context(), cache.String(cache.Key(cache.NonPersonalizedDigest, \"a\"), \"digest\"))\n\tsuite.NoError(err)\n\n\tfeedback := make([]data.Feedback, 10)\n\tfor i := 0; i < 10; i++ {\n\t\tfeedback[i] = data.Feedback{\n\t\t\tFeedbackKey: data.FeedbackKey{\n\t\t\t\tFeedbackType: \"click\",\n\t\t\t\tUserId:       \"user_1\",\n\t\t\t\tItemId:       fmt.Sprintf(\"item_%d\", i),\n\t\t\t},\n\t\t}\n\t}\n\terr = suite.dataClient.BatchInsertFeedback(suite.T().Context(), feedback, true, true, false)\n\tsuite.NoError(err)\n\n\trecommender, err := NewRecommender(config.RecommendConfig{}, suite.cacheClient, suite.dataClient, true, \"user_1\", nil)\n\tsuite.NoError(err)\n\trecommendFunc := recommender.recommendNonPersonalized(\"a\")\n\tscores, digest, err := recommendFunc(suite.T().Context())\n\tsuite.NoError(err)\n\tsuite.Equal(\"digest\", digest)\n\tif suite.Equal(10, len(scores)) {\n\t\tfor i := 0; i < 10; i++ {\n\t\t\tsuite.Equal(fmt.Sprintf(\"item_%d\", 19-i), scores[i].Id)\n\t\t\tsuite.Equal(float64(19-i), scores[i].Score)\n\t\t}\n\t}\n\n\trecommender, err = NewRecommender(config.RecommendConfig{}, suite.cacheClient, suite.dataClient, true, \"user_1\", []string{\"cat_1\"})\n\tsuite.NoError(err)\n\trecommendFunc = recommender.recommendNonPersonalized(\"a\")\n\tscores, digest, err = recommendFunc(suite.T().Context())\n\tsuite.NoError(err)\n\tsuite.Equal(\"digest\", digest)\n\tif suite.Equal(5, len(scores)) {\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tsuite.Equal(fmt.Sprintf(\"item_%d\", 18-2*i), scores[i].Id)\n\t\t\tsuite.Equal(float64(18-2*i), scores[i].Score)\n\t\t}\n\t}\n}\n\nfunc (suite *RecommenderTestSuite) TestExternal() {\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tuserId := r.URL.Query().Get(\"user_id\")\n\t\tif userId == \"user_1\" {\n\t\t\tfmt.Fprintln(w, `[\"item_1\", \"item_2\", \"item_3\", \"item_100\", \"item_200\", \"item_300\"]`)\n\t\t} else {\n\t\t\thttp.NotFound(w, r)\n\t\t}\n\t}))\n\tdefer ts.Close()\n\n\tfeedback := make([]data.Feedback, 10)\n\tfor i := 0; i < 10; i++ {\n\t\tfeedback[i] = data.Feedback{\n\t\t\tFeedbackKey: data.FeedbackKey{\n\t\t\t\tFeedbackType: \"click\",\n\t\t\t\tUserId:       \"user_1\",\n\t\t\t\tItemId:       fmt.Sprintf(\"item_%d\", i),\n\t\t\t},\n\t\t}\n\t}\n\terr := suite.dataClient.BatchInsertFeedback(suite.T().Context(), feedback, true, true, false)\n\tsuite.NoError(err)\n\n\tcfg := config.RecommendConfig{\n\t\tExternal: []config.ExternalConfig{{\n\t\t\tScript: fmt.Sprintf(`fetch(\"%s?user_id=user_1\").body`, ts.URL),\n\t\t\tName:   \"test\",\n\t\t}},\n\t}\n\trecommender, err := NewRecommender(cfg, suite.cacheClient, suite.dataClient, true, \"user_1\", nil)\n\tsuite.NoError(err)\n\trecommendFunc := recommender.recommendExternal(\"test\")\n\tscores, digest, err := recommendFunc(suite.T().Context())\n\tsuite.NoError(err)\n\tsuite.Equal(cfg.External[0].Hash(), digest)\n\tsuite.Equal([]cache.Score{\n\t\t{Id: \"item_100\", Score: 0},\n\t\t{Id: \"item_200\", Score: 0},\n\t\t{Id: \"item_300\", Score: 0},\n\t}, scores)\n}\n\nfunc TestRecommenderTestSuite(t *testing.T) {\n\tsuite.Run(t, new(RecommenderTestSuite))\n}\n"
  },
  {
    "path": "logics/user_to_user.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage logics\n\nimport (\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/expr-lang/expr\"\n\t\"github.com/expr-lang/expr/vm\"\n\t\"github.com/gorse-io/gorse/common/ann\"\n\t\"github.com/gorse-io/gorse/common/floats\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/juju/errors\"\n\t\"github.com/samber/lo\"\n\t\"go.uber.org/zap\"\n)\n\ntype UserToUserOptions struct {\n\tTagsIDF  []float32\n\tItemsIDF []float32\n}\n\ntype UserToUser interface {\n\tUsers() []*data.User\n\tPush(user *data.User, feedback []int32)\n\tPopAll(i int) []cache.Score\n\tTimestamp() time.Time\n}\n\nfunc NewUserToUser(cfg config.UserToUserConfig, n int, timestamp time.Time, opts *UserToUserOptions) (UserToUser, error) {\n\tswitch cfg.Type {\n\tcase \"embedding\":\n\t\treturn newEmbeddingUserToUser(cfg, n, timestamp)\n\tcase \"tags\":\n\t\tif opts == nil || opts.TagsIDF == nil {\n\t\t\treturn nil, errors.New(\"tags IDF is required for tags user-to-user\")\n\t\t}\n\t\treturn newTagsUserToUser(cfg, n, timestamp, opts.TagsIDF)\n\tcase \"items\":\n\t\tif opts == nil || opts.ItemsIDF == nil {\n\t\t\treturn nil, errors.New(\"items IDF is required for items user-to-user\")\n\t\t}\n\t\treturn newItemsUserToUser(cfg, n, timestamp, opts.ItemsIDF)\n\tcase \"auto\":\n\t\tif opts == nil || opts.TagsIDF == nil || opts.ItemsIDF == nil {\n\t\t\treturn nil, errors.New(\"tags IDF and items IDF are required for auto user-to-user\")\n\t\t}\n\t\treturn newAutoUserToUser(cfg, n, timestamp, opts.TagsIDF, opts.ItemsIDF)\n\t}\n\treturn nil, errors.New(\"unknown user-to-user method\")\n}\n\ntype baseUserToUser[T any] struct {\n\tname       string\n\tn          int\n\ttimestamp  time.Time\n\tcolumnFunc *vm.Program\n\tindex      *ann.HNSW[T]\n\tusers      []*data.User\n\tusersLock  sync.Mutex\n}\n\nfunc (b *baseUserToUser[T]) Users() []*data.User {\n\treturn b.users\n}\n\nfunc (b *baseUserToUser[T]) Timestamp() time.Time {\n\treturn b.timestamp\n}\n\nfunc (b *baseUserToUser[T]) PopAll(i int) []cache.Score {\n\tscores, err := b.index.SearchIndex(i, b.n+1, true)\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to search index\", zap.Error(err))\n\t\treturn nil\n\t}\n\treturn lo.Map(scores, func(v lo.Tuple2[int, float32], _ int) cache.Score {\n\t\treturn cache.Score{\n\t\t\tId:        b.users[v.A].UserId,\n\t\t\tScore:     1.0 / (1.0 + float64(v.B)),\n\t\t\tTimestamp: b.timestamp,\n\t\t}\n\t})\n}\n\ntype embeddingUserToUser struct {\n\tbaseUserToUser[[]float32]\n\tdimension int\n}\n\nfunc newEmbeddingUserToUser(cfg config.UserToUserConfig, n int, timestamp time.Time) (UserToUser, error) {\n\t// Compile column expression\n\tcolumnFunc, err := expr.Compile(cfg.Column, expr.Env(map[string]any{\n\t\t\"user\": data.User{},\n\t}))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &embeddingUserToUser{baseUserToUser: baseUserToUser[[]float32]{\n\t\tname:       cfg.Name,\n\t\tn:          n,\n\t\ttimestamp:  timestamp,\n\t\tcolumnFunc: columnFunc,\n\t\tindex:      ann.NewHNSW[[]float32](floats.Euclidean),\n\t\tusers:      []*data.User{},\n\t}}, nil\n}\n\nfunc (e *embeddingUserToUser) Push(user *data.User, _ []int32) {\n\t// Evaluate filter function\n\tresult, err := expr.Run(e.columnFunc, map[string]any{\n\t\t\"user\": user,\n\t})\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to evaluate column expression\", zap.Error(err))\n\t\treturn\n\t}\n\t// Check column type\n\tv, ok := result.([]float32)\n\tif !ok {\n\t\tlog.Logger().Error(\"invalid column type\", zap.Any(\"column\", result))\n\t\treturn\n\t}\n\t// Check dimension\n\te.usersLock.Lock()\n\tif e.dimension == 0 && len(v) > 0 {\n\t\te.dimension = len(v)\n\t} else if e.dimension != len(v) {\n\t\tlog.Logger().Error(\"invalid dimension\", zap.Int(\"expected\", e.dimension), zap.Int(\"actual\", len(v)))\n\t\treturn\n\t}\n\t// Push user\n\te.users = append(e.users, nil)\n\te.usersLock.Unlock()\n\tj := e.index.Add(v)\n\te.usersLock.Lock()\n\te.users[j] = user\n\te.usersLock.Unlock()\n}\n\ntype tagsUserToUser struct {\n\tbaseUserToUser[[]dataset.ID]\n\tIDF[dataset.ID]\n}\n\nfunc newTagsUserToUser(cfg config.UserToUserConfig, n int, timestamp time.Time, idf []float32) (UserToUser, error) {\n\t// Compile column expression\n\tcolumnFunc, err := expr.Compile(cfg.Column, expr.Env(map[string]any{\n\t\t\"user\": data.User{},\n\t}))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tt := &tagsUserToUser{IDF: idf}\n\tt.baseUserToUser = baseUserToUser[[]dataset.ID]{\n\t\tname:       cfg.Name,\n\t\tn:          n,\n\t\ttimestamp:  timestamp,\n\t\tcolumnFunc: columnFunc,\n\t\tindex:      ann.NewHNSW[[]dataset.ID](t.distance),\n\t}\n\treturn t, nil\n}\n\nfunc (t *tagsUserToUser) Push(user *data.User, _ []int32) {\n\t// Evaluate filter function\n\tresult, err := expr.Run(t.columnFunc, map[string]any{\n\t\t\"user\": user,\n\t})\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to evaluate column expression\", zap.Error(err))\n\t\treturn\n\t}\n\t// Extract tags\n\ttSet := mapset.NewSet[dataset.ID]()\n\tflatten(result, tSet)\n\tv := tSet.ToSlice()\n\tsort.Slice(v, func(i, j int) bool {\n\t\treturn v[i] < v[j]\n\t})\n\t// Push user\n\tt.usersLock.Lock()\n\tt.users = append(t.users, nil)\n\tt.usersLock.Unlock()\n\tj := t.index.Add(v)\n\tt.usersLock.Lock()\n\tt.users[j] = user\n\tt.usersLock.Unlock()\n}\n\ntype itemsUserToUser struct {\n\tbaseUserToUser[[]int32]\n\tIDF[int32]\n}\n\nfunc newItemsUserToUser(cfg config.UserToUserConfig, n int, timestamp time.Time, idf []float32) (UserToUser, error) {\n\tif cfg.Column != \"\" {\n\t\treturn nil, errors.New(\"column is not supported in items user-to-user\")\n\t}\n\ti := &itemsUserToUser{IDF: idf}\n\ti.baseUserToUser = baseUserToUser[[]int32]{\n\t\tname:      cfg.Name,\n\t\tn:         n,\n\t\ttimestamp: timestamp,\n\t\tindex:     ann.NewHNSW[[]int32](i.distance),\n\t}\n\treturn i, nil\n}\n\nfunc (i *itemsUserToUser) Push(user *data.User, feedback []int32) {\n\t// Sort feedback\n\tsort.Slice(feedback, func(i, j int) bool {\n\t\treturn feedback[i] < feedback[j]\n\t})\n\t// Push user\n\ti.usersLock.Lock()\n\ti.users = append(i.users, nil)\n\ti.usersLock.Unlock()\n\tj := i.index.Add(feedback)\n\ti.usersLock.Lock()\n\ti.users[j] = user\n\ti.usersLock.Unlock()\n}\n\ntype autoUserToUser struct {\n\tbaseUserToUser[lo.Tuple2[[]dataset.ID, []int32]]\n\ttIDF IDF[dataset.ID]\n\tiIDF IDF[int32]\n}\n\nfunc newAutoUserToUser(cfg config.UserToUserConfig, n int, timestamp time.Time, tIDF, iIDF []float32) (UserToUser, error) {\n\ta := &autoUserToUser{\n\t\ttIDF: tIDF,\n\t\tiIDF: iIDF,\n\t}\n\ta.baseUserToUser = baseUserToUser[lo.Tuple2[[]dataset.ID, []int32]]{\n\t\tname:      cfg.Name,\n\t\tn:         n,\n\t\ttimestamp: timestamp,\n\t\tindex:     ann.NewHNSW[lo.Tuple2[[]dataset.ID, []int32]](a.distance),\n\t}\n\treturn a, nil\n}\n\nfunc (a *autoUserToUser) Push(user *data.User, feedback []int32) {\n\t// Extract tags\n\ttSet := mapset.NewSet[dataset.ID]()\n\tflatten(user.Labels, tSet)\n\tt := tSet.ToSlice()\n\tsort.Slice(t, func(i, j int) bool {\n\t\treturn t[i] < t[j]\n\t})\n\t// Sort feedback\n\tsort.Slice(feedback, func(i, j int) bool {\n\t\treturn feedback[i] < feedback[j]\n\t})\n\t// Push user\n\ta.usersLock.Lock()\n\ta.users = append(a.users, nil)\n\ta.usersLock.Unlock()\n\tj := a.index.Add(lo.Tuple2[[]dataset.ID, []int32]{A: t, B: feedback})\n\ta.usersLock.Lock()\n\ta.users[j] = user\n\ta.usersLock.Unlock()\n}\n\nfunc (a *autoUserToUser) distance(u, v lo.Tuple2[[]dataset.ID, []int32]) float32 {\n\treturn (a.tIDF.distance(u.A, v.A) + a.iIDF.distance(u.B, v.B)) / 2\n}\n"
  },
  {
    "path": "logics/user_to_user_test.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage logics\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\ntype UserToUserTestSuite struct {\n\tsuite.Suite\n}\n\nfunc (suite *UserToUserTestSuite) TestEmbedding() {\n\ttimestamp := time.Now()\n\tuser2user, err := newEmbeddingUserToUser(config.UserToUserConfig{\n\t\tColumn: \"user.Labels.description\",\n\t}, 10, timestamp)\n\tsuite.NoError(err)\n\n\tfor i := 0; i < 100; i++ {\n\t\tuser2user.Push(&data.User{\n\t\t\tUserId: strconv.Itoa(i),\n\t\t\tLabels: map[string]any{\n\t\t\t\t\"description\": []float32{0.1 * float32(i), 0.2 * float32(i), 0.3 * float32(i)},\n\t\t\t},\n\t\t}, nil)\n\t}\n\n\tscores := user2user.PopAll(0)\n\tsuite.Len(scores, 10)\n\tfor i := 1; i <= 10; i++ {\n\t\tsuite.Equal(strconv.Itoa(i), scores[i-1].Id)\n\t}\n}\n\nfunc (suite *UserToUserTestSuite) TestTags() {\n\ttimestamp := time.Now()\n\tidf := make([]float32, 101)\n\tfor i := range idf {\n\t\tidf[i] = 1\n\t}\n\tuser2user, err := newTagsUserToUser(config.UserToUserConfig{\n\t\tColumn: \"user.Labels\",\n\t}, 10, timestamp, idf)\n\tsuite.NoError(err)\n\n\tfor i := 0; i < 100; i++ {\n\t\tlabels := make(map[string]any)\n\t\tfor j := 1; j <= 100-i; j++ {\n\t\t\tlabels[strconv.Itoa(j)] = []dataset.ID{dataset.ID(j)}\n\t\t}\n\t\tuser2user.Push(&data.User{\n\t\t\tUserId: strconv.Itoa(i),\n\t\t\tLabels: labels,\n\t\t}, nil)\n\t}\n\n\tscores := user2user.PopAll(0)\n\tsuite.Len(scores, 10)\n\tfor i := 1; i <= 10; i++ {\n\t\tsuite.Equal(strconv.Itoa(i), scores[i-1].Id)\n\t}\n}\n\nfunc (suite *UserToUserTestSuite) TestItems() {\n\ttimestamp := time.Now()\n\tidf := make([]float32, 101)\n\tfor i := range idf {\n\t\tidf[i] = 1\n\t}\n\tuser2user, err := newItemsUserToUser(config.UserToUserConfig{}, 10, timestamp, idf)\n\tsuite.NoError(err)\n\n\tfor i := 0; i < 100; i++ {\n\t\tfeedback := make([]int32, 0, 100-i)\n\t\tfor j := 1; j <= 100-i; j++ {\n\t\t\tfeedback = append(feedback, int32(j))\n\t\t}\n\t\tuser2user.Push(&data.User{UserId: strconv.Itoa(i)}, feedback)\n\t}\n\n\tscores := user2user.PopAll(0)\n\tsuite.Len(scores, 10)\n\tfor i := 1; i <= 10; i++ {\n\t\tsuite.Equal(strconv.Itoa(i), scores[i-1].Id)\n\t}\n}\n\nfunc (suite *UserToUserTestSuite) TestAuto() {\n\ttimestamp := time.Now()\n\tidf := make([]float32, 101)\n\tfor i := range idf {\n\t\tidf[i] = 1\n\t}\n\tuser2user, err := newAutoUserToUser(config.UserToUserConfig{}, 10, timestamp, idf, idf)\n\tsuite.NoError(err)\n\n\tfor i := 0; i < 100; i++ {\n\t\tuser := &data.User{UserId: strconv.Itoa(i)}\n\t\tfeedback := make([]int32, 0, 100-i)\n\t\tif i%2 == 0 {\n\t\t\tlabels := make(map[string]any)\n\t\t\tfor j := 1; j <= 100-i; j++ {\n\t\t\t\tlabels[strconv.Itoa(j)] = []dataset.ID{dataset.ID(j)}\n\t\t\t}\n\t\t\tuser.Labels = labels\n\t\t} else {\n\t\t\tfor j := 1; j <= 100-i; j++ {\n\t\t\t\tfeedback = append(feedback, int32(j))\n\t\t\t}\n\t\t}\n\t\tuser2user.Push(user, feedback)\n\t}\n\n\tscores0 := user2user.PopAll(0)\n\tsuite.Len(scores0, 10)\n\tscores1 := user2user.PopAll(1)\n\tsuite.Len(scores1, 10)\n}\n\nfunc TestUserToUser(t *testing.T) {\n\tsuite.Run(t, new(UserToUserTestSuite))\n}\n"
  },
  {
    "path": "master/master.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage master\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"math\"\n\t\"math/rand\"\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/coreos/go-oidc/v3/oidc\"\n\t\"github.com/emicklei/go-restful/v3\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/common/monitor\"\n\t\"github.com/gorse-io/gorse/common/parallel\"\n\t\"github.com/gorse-io/gorse/common/util\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/model/cf\"\n\t\"github.com/gorse-io/gorse/model/ctr\"\n\t\"github.com/gorse-io/gorse/protocol\"\n\t\"github.com/gorse-io/gorse/server\"\n\t\"github.com/gorse-io/gorse/storage/blob\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/gorse-io/gorse/storage/meta\"\n\t\"github.com/jellydator/ttlcache/v3\"\n\t\"github.com/juju/errors\"\n\t\"github.com/sashabaranov/go-openai\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/propagation\"\n\t\"go.uber.org/zap\"\n\t\"golang.org/x/oauth2\"\n\t\"google.golang.org/grpc\"\n)\n\ntype Datasets struct {\n\trankingDataset  *dataset.Dataset\n\trankingTrainSet dataset.CFSplit\n\trankingTestSet  dataset.CFSplit\n\tclickDataset    *ctr.Dataset\n\tclickTrainSet   *ctr.Dataset\n\tclickTestSet    *ctr.Dataset\n}\n\n// Master is the master node.\ntype Master struct {\n\tprotocol.UnimplementedMasterServer\n\tserver.RestServer\n\tgrpcServer *grpc.Server\n\n\ttracer         *monitor.Monitor\n\tremoteProgress sync.Map\n\tcachePath      string\n\tconfigPath     string\n\tstandalone     bool\n\topenAIClient   *openai.Client\n\n\t// cluster meta cache\n\tmetaStore  meta.Database\n\tblobStore  blob.Store\n\tblobServer *blob.MasterStoreServer\n\n\t// collaborative filtering\n\tcollaborativeFilteringModelMutex   sync.RWMutex\n\tcollaborativeFilteringTrainSetSize int\n\tcollaborativeFilteringMeta         meta.Model[cf.Score]\n\tcollaborativeFilteringTarget       meta.Model[cf.Score]\n\n\t// click model\n\tclickThroughRateModelMutex   sync.RWMutex\n\tclickThroughRateTrainSetSize int\n\tclickThroughRateMeta         meta.Model[ctr.Score]\n\tclickThroughRateTarget       meta.Model[ctr.Score]\n\n\t// oauth2\n\toauth2Config oauth2.Config\n\tverifier     *oidc.IDTokenVerifier\n\ttokenCache   *ttlcache.Cache[string, UserInfo]\n\n\t// events\n\tticker    *time.Ticker\n\tscheduled chan struct{}\n\tcancel    context.CancelFunc\n}\n\n// NewMaster creates a master node.\nfunc NewMaster(cfg *config.Config, cacheFolder string, standalone bool, configPath string) *Master {\n\trand.Seed(time.Now().UnixNano())\n\n\t// setup trace provider\n\ttp, err := cfg.Tracing.NewTracerProvider()\n\tif err != nil {\n\t\tlog.Logger().Fatal(\"failed to create trace provider\", zap.Error(err))\n\t}\n\totel.SetTracerProvider(tp)\n\totel.SetErrorHandler(log.GetErrorHandler())\n\totel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))\n\n\t// setup OpenAI client\n\tclientConfig := openai.DefaultConfig(cfg.OpenAI.AuthToken)\n\tclientConfig.BaseURL = cfg.OpenAI.BaseURL\n\t// setup OpenAI logger\n\tlog.InitOpenAILogger(cfg.OpenAI.LogFile)\n\t// setup OpenAI rate limiter\n\tparallel.InitChatCompletionLimiters(cfg.OpenAI.ChatCompletionRPM, cfg.OpenAI.ChatCompletionTPM)\n\tparallel.InitEmbeddingLimiters(cfg.OpenAI.EmbeddingRPM, cfg.OpenAI.EmbeddingTPM)\n\n\tduration := min(cfg.Recommend.Collaborative.FitPeriod, cfg.Recommend.Ranker.FitPeriod)\n\tm := &Master{\n\t\t// create task monitor\n\t\tcachePath:    cacheFolder,\n\t\tconfigPath:   configPath,\n\t\tstandalone:   standalone,\n\t\ttracer:       monitor.NewTracer(\"master\"),\n\t\topenAIClient: openai.NewClientWithConfig(clientConfig),\n\t\tRestServer: server.RestServer{\n\t\t\tConfig:      cfg,\n\t\t\tCacheClient: cache.NoDatabase{},\n\t\t\tDataClient:  data.NoDatabase{},\n\t\t\tHttpHost:    cfg.Master.HttpHost,\n\t\t\tHttpPort:    cfg.Master.HttpPort,\n\t\t\tWebService:  new(restful.WebService),\n\t\t},\n\t\tticker:    time.NewTicker(duration),\n\t\tscheduled: make(chan struct{}, 1),\n\t\tcancel:    func() {},\n\t}\n\treturn m\n}\n\n// Serve starts the master node.\nfunc (m *Master) Serve() {\n\t// connect blob store\n\tvar err error\n\tm.blobServer = blob.NewMasterStoreServer(m.Config.Blob.URI)\n\tm.blobStore, err = blob.NewStore(m.Config.Blob, nil)\n\tif err != nil {\n\t\tlog.Logger().Fatal(\"failed to create blob store\", zap.Error(err))\n\t}\n\n\t// connect meta database\n\tm.metaStore, err = meta.Open(fmt.Sprintf(\"sqlite://%s/meta.sqlite3\", m.cachePath), m.Config.Master.MetaTimeout)\n\tif err != nil {\n\t\tlog.Logger().Fatal(\"failed to connect meta database\", zap.Error(err))\n\t}\n\tif err = m.metaStore.Init(); err != nil {\n\t\tlog.Logger().Fatal(\"failed to init meta database\", zap.Error(err))\n\t}\n\n\t// connect data database\n\tdataOpts := m.Config.Database.StorageOptions(m.Config.Database.DataStore)\n\tm.DataClient, err = data.Open(m.Config.Database.DataStore, m.Config.Database.DataTablePrefix, dataOpts...)\n\tif err != nil {\n\t\tlog.Logger().Fatal(\"failed to connect data database\", zap.Error(err),\n\t\t\tzap.String(\"database\", log.RedactDBURL(m.Config.Database.DataStore)))\n\t}\n\tif err = m.DataClient.Init(); err != nil {\n\t\tlog.Logger().Fatal(\"failed to init database\", zap.Error(err))\n\t}\n\n\t// connect cache database\n\tcacheOpts := m.Config.Database.StorageOptions(m.Config.Database.CacheStore)\n\tm.CacheClient, err = cache.Open(m.Config.Database.CacheStore, m.Config.Database.CacheTablePrefix, cacheOpts...)\n\tif err != nil {\n\t\tlog.Logger().Fatal(\"failed to connect cache database\", zap.Error(err),\n\t\t\tzap.String(\"database\", log.RedactDBURL(m.Config.Database.CacheStore)))\n\t}\n\tif err = m.CacheClient.Init(); err != nil {\n\t\tlog.Logger().Fatal(\"failed to init database\", zap.Error(err))\n\t}\n\n\t// load recommend config\n\tmetaStr, err := m.metaStore.Get(meta.RECOMMEND_CONFIG)\n\tif err != nil && !errors.Is(err, errors.NotFound) {\n\t\tlog.Logger().Error(\"failed to load recommend config\", zap.Error(err))\n\t} else if metaStr != nil {\n\t\terr = json.Unmarshal([]byte(*metaStr), &m.Config.Recommend)\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to unmarshal recommend config\", zap.Error(err))\n\t\t}\n\t}\n\n\t// load collective filtering model meta\n\tmetaStr, err = m.metaStore.Get(meta.COLLABORATIVE_FILTERING_MODEL)\n\tif err != nil && !errors.Is(err, errors.NotFound) {\n\t\tlog.Logger().Error(\"failed to load collaborative filtering meta\", zap.Error(err))\n\t} else if metaStr != nil {\n\t\tif err = m.collaborativeFilteringMeta.FromJSON(*metaStr); err != nil {\n\t\t\tlog.Logger().Error(\"failed to unmarshal collaborative filtering meta\", zap.Error(err))\n\t\t} else {\n\t\t\tlog.Logger().Info(\"loaded collaborative filtering model\",\n\t\t\t\tzap.String(\"type\", m.collaborativeFilteringMeta.Type),\n\t\t\t\tzap.Any(\"params\", m.collaborativeFilteringMeta.Params),\n\t\t\t\tzap.Any(\"score\", m.collaborativeFilteringMeta.Score))\n\t\t}\n\t}\n\n\t// load click-through rate model\n\tmetaStr, err = m.metaStore.Get(meta.CLICK_THROUGH_RATE_MODEL)\n\tif err != nil && !errors.Is(err, errors.NotFound) {\n\t\tlog.Logger().Error(\"failed to load click-through rate meta\", zap.Error(err))\n\t} else if metaStr != nil {\n\t\tif err = m.clickThroughRateMeta.FromJSON(*metaStr); err != nil {\n\t\t\tlog.Logger().Error(\"failed to unmarshal click-through rate meta\", zap.Error(err))\n\t\t} else {\n\t\t\tlog.Logger().Info(\"loaded click-through rate model\",\n\t\t\t\tzap.String(\"type\", m.clickThroughRateMeta.Type),\n\t\t\t\tzap.Any(\"params\", m.clickThroughRateMeta.Params),\n\t\t\t\tzap.Any(\"score\", m.clickThroughRateMeta.Score))\n\t\t}\n\t}\n\n\tgo m.RunTasksLoop()\n\n\t// start rpc server\n\tgo func() {\n\t\tlog.Logger().Info(\"start rpc server\",\n\t\t\tzap.String(\"host\", m.Config.Master.Host),\n\t\t\tzap.Int(\"port\", m.Config.Master.Port),\n\t\t\tzap.Bool(\"ssl_mode\", m.Config.Master.SSLMode),\n\t\t\tzap.String(\"ssl_ca\", m.Config.Master.SSLCA),\n\t\t\tzap.String(\"ssl_cert\", m.Config.Master.SSLCert),\n\t\t\tzap.String(\"ssl_key\", m.Config.Master.SSLKey))\n\t\topts := []grpc.ServerOption{grpc.MaxSendMsgSize(math.MaxInt)}\n\t\tif m.Config.Master.SSLMode {\n\t\t\tc, err := util.NewServerCreds(&util.TLSConfig{\n\t\t\t\tSSLCA:   m.Config.Master.SSLCA,\n\t\t\t\tSSLCert: m.Config.Master.SSLCert,\n\t\t\t\tSSLKey:  m.Config.Master.SSLKey,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tlog.Logger().Fatal(\"failed to load server TLS\", zap.Error(err))\n\t\t\t}\n\t\t\topts = append(opts, grpc.Creds(c))\n\t\t}\n\t\tlis, err := net.Listen(\"tcp\", fmt.Sprintf(\"%s:%d\", m.Config.Master.Host, m.Config.Master.Port))\n\t\tif err != nil {\n\t\t\tlog.Logger().Fatal(\"failed to listen\", zap.Error(err))\n\t\t}\n\t\tm.grpcServer = grpc.NewServer(opts...)\n\t\tprotocol.RegisterMasterServer(m.grpcServer, m)\n\t\tprotocol.RegisterCacheStoreServer(m.grpcServer, cache.NewProxyServer(m.CacheClient))\n\t\tprotocol.RegisterDataStoreServer(m.grpcServer, data.NewProxyServer(m.DataClient))\n\t\tprotocol.RegisterBlobStoreServer(m.grpcServer, m.blobServer)\n\t\tif err = m.grpcServer.Serve(lis); err != nil {\n\t\t\tlog.Logger().Fatal(\"failed to start rpc server\", zap.Error(err))\n\t\t}\n\t}()\n\n\tif m.Config.OIDC.Enable {\n\t\tprovider, err := oidc.NewProvider(context.Background(), m.Config.OIDC.Issuer)\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to create oidc provider\", zap.Error(err))\n\t\t} else {\n\t\t\tm.verifier = provider.Verifier(&oidc.Config{ClientID: m.Config.OIDC.ClientID})\n\t\t\tm.oauth2Config = oauth2.Config{\n\t\t\t\tClientID:     m.Config.OIDC.ClientID,\n\t\t\t\tClientSecret: m.Config.OIDC.ClientSecret,\n\t\t\t\tRedirectURL:  m.Config.OIDC.RedirectURL,\n\t\t\t\tEndpoint:     provider.Endpoint(),\n\t\t\t\tScopes:       []string{oidc.ScopeOpenID, \"profile\", \"email\"},\n\t\t\t}\n\t\t\tm.tokenCache = ttlcache.New(ttlcache.WithTTL[string, UserInfo](time.Hour))\n\t\t\tgo m.tokenCache.Start()\n\t\t}\n\t}\n\n\t// start http server\n\tm.StartHttpServer()\n}\n\nfunc (m *Master) Shutdown() {\n\t// stop http server\n\terr := m.HttpServer.Shutdown(context.TODO())\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to shutdown http server\", zap.Error(err))\n\t}\n\t// stop grpc server\n\tm.grpcServer.GracefulStop()\n}\n\nfunc (m *Master) RunTasksLoop() {\n\tdefer util.CheckPanic()\n\tselect {\n\tcase m.scheduled <- struct{}{}:\n\tdefault:\n\t}\n\tfor {\n\t\tselect {\n\t\tcase <-m.ticker.C:\n\t\tcase <-m.scheduled:\n\t\t}\n\n\t\t// download dataset\n\t\tvar ctx context.Context\n\t\tctx, m.cancel = context.WithCancel(context.Background())\n\t\terr := m.runLoadDatasetTask(ctx)\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to load ranking dataset\", zap.Error(err))\n\t\t\tcontinue\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "master/master_test.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage master\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/gorse-io/gorse/common/monitor\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\ntype MasterTestSuite struct {\n\tsuite.Suite\n\tMaster\n}\n\nfunc (s *MasterTestSuite) SetupTest() {\n\t// open database\n\tvar err error\n\ts.tracer = monitor.NewTracer(\"test\")\n\ts.Config = config.GetDefaultConfig()\n\ts.DataClient, err = data.Open(fmt.Sprintf(\"sqlite://%s/data.db\", s.T().TempDir()), \"\")\n\ts.NoError(err)\n\ts.CacheClient, err = cache.Open(fmt.Sprintf(\"sqlite://%s/cache.db\", s.T().TempDir()), \"\")\n\ts.NoError(err)\n\t// init database\n\terr = s.DataClient.Init()\n\ts.NoError(err)\n\terr = s.CacheClient.Init()\n\ts.NoError(err)\n}\n\nfunc (s *MasterTestSuite) TearDownTest() {\n\ts.NoError(s.DataClient.Close())\n\ts.NoError(s.CacheClient.Close())\n}\n\nfunc TestMaster(t *testing.T) {\n\tsuite.Run(t, new(MasterTestSuite))\n}\n"
  },
  {
    "path": "master/metrics.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage master\n\nimport (\n\t\"time\"\n\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/gorse-io/gorse/common/expression\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n)\n\nconst (\n\tLabelFeedbackType = \"feedback_type\"\n\tLabelStep         = \"step\"\n\tLabelData         = \"data\"\n)\n\nvar (\n\tLoadDatasetStepSecondsVec = promauto.NewGaugeVec(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"load_dataset_step_seconds\",\n\t}, []string{LabelStep})\n\tLoadDatasetTotalSeconds = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"load_dataset_total_seconds\",\n\t})\n\tFindUserNeighborsSecondsVec = promauto.NewGaugeVec(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"find_user_neighbors_seconds\",\n\t}, []string{LabelStep})\n\tFindUserNeighborsTotalSeconds = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"find_user_neighbors_total_seconds\",\n\t})\n\tFindItemNeighborsSecondsVec = promauto.NewGaugeVec(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"find_item_neighbors_seconds\",\n\t}, []string{\"step\"})\n\tFindItemNeighborsTotalSeconds = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"find_item_neighbors_total_seconds\",\n\t})\n\tUpdateUserNeighborsTotal = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"update_user_neighbors_total\",\n\t})\n\tUpdateItemNeighborsTotal = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"update_item_neighbors_total\",\n\t})\n\tCacheScannedTotal = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"cache_scanned_total\",\n\t})\n\tCacheReclaimedTotal = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"cache_reclaimed_total\",\n\t})\n\tCacheScannedSeconds = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"cache_scanned_seconds\",\n\t})\n\n\tCollaborativeFilteringFitSeconds = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"collaborative_filtering_fit_seconds\",\n\t})\n\tCollaborativeFilteringSearchSeconds = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"collaborative_filtering_search_seconds\",\n\t})\n\tCollaborativeFilteringNDCG10 = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"collaborative_filtering_ndcg_10\",\n\t})\n\tCollaborativeFilteringPrecision10 = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"collaborative_filtering_precision_10\",\n\t})\n\tCollaborativeFilteringRecall10 = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"collaborative_filtering_recall_10\",\n\t})\n\tCollaborativeFilteringSearchPrecision10 = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"collaborative_filtering_search_precision_10\",\n\t})\n\tRankingFitSeconds = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"ranking_fit_seconds\",\n\t})\n\tRankingSearchSeconds = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"ranking_search_seconds\",\n\t})\n\tRankingPrecision = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"ranking_model_precision\",\n\t})\n\tRankingRecall = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"ranking_model_recall\",\n\t})\n\tRankingAUC = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"ranking_model_auc\",\n\t})\n\tRankingSearchPrecision = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"ranking_search_precision\",\n\t})\n\n\tUsersTotal = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"users_total\",\n\t})\n\tActiveUsersTotal = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"active_users_total\",\n\t})\n\tInactiveUsersTotal = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"inactive_users_total\",\n\t})\n\tItemsTotal = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"items_total\",\n\t})\n\tActiveItemsTotal = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"active_items_total\",\n\t})\n\tInactiveItemsTotal = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"inactive_items_total\",\n\t})\n\tUserLabelsTotal = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"user_labels_total\",\n\t})\n\tItemLabelsTotal = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"item_labels_total\",\n\t})\n\tFeedbacksTotal = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"feedbacks_total\",\n\t})\n\tImplicitFeedbacksTotal = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"implicit_feedbacks_total\",\n\t})\n\tPositiveFeedbacksTotal = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"positive_feedbacks_total\",\n\t})\n\tNegativeFeedbackTotal = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"negative_feedbacks_total\",\n\t})\n\tMemoryInUseBytesVec = promauto.NewGaugeVec(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"master\",\n\t\tName:      \"memory_inuse_bytes\",\n\t}, []string{LabelData})\n)\n\ntype OnlineEvaluator struct {\n\tReadTypes        []expression.FeedbackTypeExpression\n\tReadFeedback     []map[int32]mapset.Set[int32]\n\tPositiveTypes    []expression.FeedbackTypeExpression\n\tPositiveFeedback map[string]map[int32]mapset.Set[int32]\n\tWindowSize       int\n\tWindowEnd        time.Time\n}\n\nfunc NewOnlineEvaluator(positiveTypes, readTypes []expression.FeedbackTypeExpression) *OnlineEvaluator {\n\tevaluator := new(OnlineEvaluator)\n\tevaluator.WindowSize = 30\n\tevaluator.WindowEnd = time.Now().Truncate(time.Hour * 24)\n\tevaluator.ReadTypes = readTypes\n\tevaluator.ReadFeedback = make([]map[int32]mapset.Set[int32], evaluator.WindowSize)\n\tfor i := 0; i < evaluator.WindowSize; i++ {\n\t\tevaluator.ReadFeedback[i] = make(map[int32]mapset.Set[int32])\n\t}\n\tevaluator.PositiveTypes = positiveTypes\n\tevaluator.PositiveFeedback = make(map[string]map[int32]mapset.Set[int32])\n\tevaluator.PositiveFeedback[\"\"] = make(map[int32]mapset.Set[int32])\n\treturn evaluator\n}\n\nfunc (evaluator *OnlineEvaluator) Add(feedbackType string, value float64, userIndex int32, itemIndex int32, timestamp time.Time) {\n\tif expression.MatchFeedbackTypeExpressions(evaluator.ReadTypes, feedbackType, value) {\n\t\ttruncated := timestamp.Truncate(time.Hour * 24)\n\t\twindowIndex := int(evaluator.WindowEnd.Sub(truncated) / time.Hour / 24)\n\t\tif windowIndex < 0 || windowIndex >= evaluator.WindowSize {\n\t\t\treturn\n\t\t}\n\t\tif evaluator.ReadFeedback[windowIndex][userIndex] == nil {\n\t\t\tevaluator.ReadFeedback[windowIndex][userIndex] = mapset.NewSet[int32]()\n\t\t}\n\t\tevaluator.ReadFeedback[windowIndex][userIndex].Add(itemIndex)\n\t}\n\tif expression.MatchFeedbackTypeExpressions(evaluator.PositiveTypes, feedbackType, value) {\n\t\tif evaluator.PositiveFeedback[feedbackType] == nil {\n\t\t\tevaluator.PositiveFeedback[feedbackType] = make(map[int32]mapset.Set[int32])\n\t\t}\n\t\tif evaluator.PositiveFeedback[feedbackType][userIndex] == nil {\n\t\t\tevaluator.PositiveFeedback[feedbackType][userIndex] = mapset.NewSet[int32]()\n\t\t}\n\t\tevaluator.PositiveFeedback[feedbackType][userIndex].Add(itemIndex)\n\t\tif evaluator.PositiveFeedback[\"\"][userIndex] == nil {\n\t\t\tevaluator.PositiveFeedback[\"\"][userIndex] = mapset.NewSet[int32]()\n\t\t}\n\t\tevaluator.PositiveFeedback[\"\"][userIndex].Add(itemIndex)\n\t}\n}\n\nfunc (evaluator *OnlineEvaluator) Evaluate() []cache.TimeSeriesPoint {\n\tvar points []cache.TimeSeriesPoint\n\tfor feedbackType := range evaluator.PositiveFeedback {\n\t\tfor i := 0; i < evaluator.WindowSize; i++ {\n\t\t\tdate := evaluator.WindowEnd.AddDate(0, 0, -i)\n\t\t\tvar ratioSum float64\n\t\t\tvar userCount int\n\t\t\tfor userIndex, readItems := range evaluator.ReadFeedback[i] {\n\t\t\t\tpositiveItems, ok := evaluator.PositiveFeedback[feedbackType][userIndex]\n\t\t\t\tif !ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tpositiveCount := float64(readItems.Intersect(positiveItems).Cardinality())\n\t\t\t\tif readItems.Cardinality() > 0 {\n\t\t\t\t\tratioSum += positiveCount / float64(readItems.Cardinality())\n\t\t\t\t\tuserCount++\n\t\t\t\t}\n\t\t\t}\n\t\t\tif userCount > 0 {\n\t\t\t\tname := cache.PositiveFeedbackRatio\n\t\t\t\tif feedbackType != \"\" {\n\t\t\t\t\tname += \"_\" + feedbackType\n\t\t\t\t}\n\t\t\t\tpoints = append(points, cache.TimeSeriesPoint{\n\t\t\t\t\tName:      name,\n\t\t\t\t\tTimestamp: date,\n\t\t\t\t\tValue:     ratioSum / float64(userCount),\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\treturn points\n}\n"
  },
  {
    "path": "master/metrics_test.go",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage master\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/common/expression\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestOnlineEvaluator(t *testing.T) {\n\tpositiveTypes := []expression.FeedbackTypeExpression{expression.MustParseFeedbackTypeExpression(\"read>=100\")}\n\treadTypes := []expression.FeedbackTypeExpression{expression.MustParseFeedbackTypeExpression(\"read\")}\n\n\tevaluator := NewOnlineEvaluator(positiveTypes, readTypes)\n\tresult := evaluator.Evaluate()\n\tassert.Empty(t, result)\n\n\tevaluator = NewOnlineEvaluator(positiveTypes, readTypes)\n\tevaluator.WindowEnd = time.Date(2005, 6, 16, 0, 0, 0, 0, time.UTC)\n\tevaluator.WindowSize = 2\n\tevaluator.Add(\"read\", 0, 1, 1, time.Date(2005, 6, 15, 0, 0, 0, 0, time.UTC))\n\tevaluator.Add(\"read\", 0, 1, 2, time.Date(2005, 6, 15, 0, 0, 0, 0, time.UTC))\n\tevaluator.Add(\"read\", 0, 1, 3, time.Date(2005, 6, 15, 0, 0, 0, 0, time.UTC))\n\tevaluator.Add(\"read\", 100, 1, 4, time.Date(2005, 6, 15, 0, 0, 0, 0, time.UTC))\n\tevaluator.Add(\"read\", 100, 2, 1, time.Date(2005, 6, 15, 0, 0, 0, 0, time.UTC))\n\tevaluator.Add(\"read\", 0, 2, 2, time.Date(2005, 6, 15, 0, 0, 0, 0, time.UTC))\n\tevaluator.Add(\"read\", 0, 2, 3, time.Date(2005, 6, 15, 0, 0, 0, 0, time.UTC))\n\tevaluator.Add(\"read\", 0, 2, 4, time.Date(2005, 6, 15, 0, 0, 0, 0, time.UTC))\n\tevaluator.Add(\"read\", 0, 2, 3, time.Date(2005, 6, 16, 0, 0, 0, 0, time.UTC))\n\tevaluator.Add(\"read\", 100, 3, 3, time.Date(2005, 6, 16, 0, 0, 0, 0, time.UTC))\n\tresult = evaluator.Evaluate()\n\tassert.ElementsMatch(t, []cache.TimeSeriesPoint{\n\t\t{Name: \"positive_feedback_ratio_read\", Timestamp: time.Date(2005, 6, 16, 0, 0, 0, 0, time.UTC), Value: 0.5},\n\t\t{Name: \"positive_feedback_ratio_read\", Timestamp: time.Date(2005, 6, 15, 0, 0, 0, 0, time.UTC), Value: 0.25},\n\t\t{Name: \"positive_feedback_ratio\", Timestamp: time.Date(2005, 6, 16, 0, 0, 0, 0, time.UTC), Value: 0.5},\n\t\t{Name: \"positive_feedback_ratio\", Timestamp: time.Date(2005, 6, 15, 0, 0, 0, 0, time.UTC), Value: 0.25},\n\t}, result)\n}\n"
  },
  {
    "path": "master/rest.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage master\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/araddon/dateparse\"\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\trestfulspec \"github.com/emicklei/go-restful-openapi/v2\"\n\t\"github.com/emicklei/go-restful/v3\"\n\t\"github.com/go-viper/mapstructure/v2\"\n\t\"github.com/gorilla/securecookie\"\n\t_ \"github.com/gorse-io/dashboard\"\n\t\"github.com/gorse-io/gorse/cmd/version\"\n\t\"github.com/gorse-io/gorse/common/expression\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/common/monitor\"\n\t\"github.com/gorse-io/gorse/common/util\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/logics\"\n\t\"github.com/gorse-io/gorse/model/cf\"\n\t\"github.com/gorse-io/gorse/model/ctr\"\n\t\"github.com/gorse-io/gorse/protocol\"\n\t\"github.com/gorse-io/gorse/server\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/gorse-io/gorse/storage/meta\"\n\t\"github.com/invopop/jsonschema\"\n\t\"github.com/juju/errors\"\n\t\"github.com/nikolalohinski/gonja/v2\"\n\t\"github.com/nikolalohinski/gonja/v2/exec\"\n\t\"github.com/rakyll/statik/fs\"\n\t\"github.com/samber/lo\"\n\t\"github.com/sashabaranov/go-openai\"\n\t\"go.uber.org/zap\"\n\t\"google.golang.org/protobuf/proto\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n)\n\ntype UserInfo struct {\n\tName       string `json:\"name\"`\n\tFamilyName string `json:\"family_name\"`\n\tGivenName  string `json:\"given_name\"`\n\tMiddleName string `json:\"middle_name\"`\n\tNickName   string `json:\"nickname\"`\n\tPicture    string `json:\"picture\"`\n\tUpdatedAt  string `json:\"updated_at\"`\n\tEmail      string `json:\"email\"`\n\tVerified   bool   `json:\"email_verified\"`\n\tAuthType   string `json:\"auth_type\"`\n}\n\ntype RerankerPrompt struct {\n\tQuery     string   `json:\"query\"`\n\tDocuments []string `json:\"documents\"`\n}\n\nfunc (m *Master) CreateWebService() {\n\tws := m.WebService\n\tws.Consumes(restful.MIME_JSON).Produces(restful.MIME_JSON)\n\tws.Path(\"/api/\")\n\tws.Filter(m.LoginFilter)\n\n\tws.Route(ws.GET(\"/dashboard/userinfo\").To(m.handleUserInfo).\n\t\tDoc(\"Get login user information.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"dashboard\"}).\n\t\tReturns(http.StatusOK, \"OK\", UserInfo{}).\n\t\tWrites(UserInfo{}))\n\tws.Route(ws.GET(\"/dashboard/cluster\").To(m.getCluster).\n\t\tDoc(\"Get nodes in the cluster.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"dashboard\"}).\n\t\tReturns(http.StatusOK, \"OK\", []meta.Node{}).\n\t\tWrites([]meta.Node{}))\n\tws.Route(ws.GET(\"/dashboard/categories\").To(m.getCategories).\n\t\tDoc(\"Get categories of items.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"dashboard\"}).\n\t\tReturns(http.StatusOK, \"OK\", []string{}).\n\t\tWrites([]string{}))\n\tws.Route(ws.POST(\"/dashboard/config\").To(m.postConfig).\n\t\tDoc(\"Update config.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"dashboard\"}).\n\t\tReturns(http.StatusOK, \"OK\", config.Config{}).\n\t\tWrites(config.Config{}))\n\tws.Route(ws.GET(\"/dashboard/config\").To(m.getConfig).\n\t\tDoc(\"Get config.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"dashboard\"}).\n\t\tReturns(http.StatusOK, \"OK\", config.Config{}).\n\t\tWrites(config.Config{}))\n\tws.Route(ws.DELETE(\"/dashboard/config\").To(m.deleteConfig).\n\t\tDoc(\"Delete config.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"dashboard\"}).\n\t\tReturns(http.StatusOK, \"OK\", struct{}{}))\n\tws.Route(ws.GET(\"/dashboard/config/schema\").To(m.getConfigSchema).\n\t\tDoc(\"Get config schema.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"dashboard\"}).\n\t\tReturns(http.StatusOK, \"OK\", config.Config{}).\n\t\tWrites(jsonschema.Schema{}))\n\tws.Route(ws.GET(\"/dashboard/stats\").To(m.getStats).\n\t\tDoc(\"Get global status.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"dashboard\"}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"secret key for RESTful API\")).\n\t\tReturns(http.StatusOK, \"OK\", Status{}).\n\t\tWrites(Status{}))\n\tws.Route(ws.GET(\"/dashboard/tasks\").To(m.getTasks).\n\t\tDoc(\"Get tasks.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"dashboard\"}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"secret key for RESTful API\")).\n\t\tReturns(http.StatusOK, \"OK\", []monitor.Progress{}).\n\t\tWrites([]monitor.Progress{}))\n\tws.Route(ws.GET(\"/dashboard/timeseries/{name}\").To(m.getTimeseries).\n\t\tDoc(\"Get time series data.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"dashboard\"}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"secret key for RESTful API\")).\n\t\tParam(ws.PathParameter(\"name\", \"name of the time series\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", map[string][]cache.TimeSeriesPoint{}).\n\t\tWrites(map[string][]cache.TimeSeriesPoint{}))\n\t// Get a user\n\tws.Route(ws.GET(\"/dashboard/user/{user-id}\").To(m.getUser).\n\t\tDoc(\"Get a user.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"dashboard\"}).\n\t\tParam(ws.PathParameter(\"user-id\", \"identifier of the user\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", User{}).\n\t\tWrites(User{}))\n\t// Get a user feedback\n\tws.Route(ws.GET(\"/dashboard/user/{user-id}/feedback/\").To(m.getUserFeedback).\n\t\tDoc(\"Get feedback by user id.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"feedback\"}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"secret key for RESTful API\")).\n\t\tParam(ws.PathParameter(\"user-id\", \"identifier of the user\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"number of returned feedback\").DataType(\"int\")).\n\t\tParam(ws.QueryParameter(\"offset\", \"offset of returned feedback\").DataType(\"int\")).\n\t\tReturns(http.StatusOK, \"OK\", []DetailedFeedback{}).\n\t\tWrites([]DetailedFeedback{}))\n\tws.Route(ws.GET(\"/dashboard/user/{user-id}/feedback/{feedback-type}\").To(m.getUserFeedback).\n\t\tDoc(\"Get feedback by user id with feedback type.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"feedback\"}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"secret key for RESTful API\")).\n\t\tParam(ws.PathParameter(\"user-id\", \"identifier of the user\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"feedback-type\", \"feedback type\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"number of returned feedback\").DataType(\"int\")).\n\t\tParam(ws.QueryParameter(\"offset\", \"offset of returned feedback\").DataType(\"int\")).\n\t\tReturns(http.StatusOK, \"OK\", []DetailedFeedback{}).\n\t\tWrites([]DetailedFeedback{}))\n\t// Get users\n\tws.Route(ws.GET(\"/dashboard/users\").To(m.getUsers).\n\t\tDoc(\"Get users.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"dashboard\"}).\n\t\tParam(ws.QueryParameter(\"n\", \"number of returned users\").DataType(\"int\")).\n\t\tParam(ws.QueryParameter(\"cursor\", \"cursor for next page\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", UserIterator{}).\n\t\tWrites(UserIterator{}))\n\t// Get non-personalized recommendation\n\tws.Route(ws.GET(\"/dashboard/latest\").To(m.getLatest).\n\t\tDoc(\"Get latest items.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"dashboard\"}).\n\t\tParam(ws.QueryParameter(\"category\", \"Category of returned items.\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"Number of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"offset\", \"Offset of returned items\").DataType(\"integer\")).\n\t\tReturns(http.StatusOK, \"OK\", []data.Item{}).\n\t\tWrites([]ScoredItem{}))\n\tws.Route(ws.GET(\"/dashboard/non-personalized/{name}\").To(m.getNonPersonalized).\n\t\tDoc(\"Get non-personalized recommendations.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"dashboard\"}).\n\t\tParam(ws.QueryParameter(\"category\", \"Category of returned items.\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"Number of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"offset\", \"Offset of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"user-id\", \"Remove read items of a user\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", []cache.Score{}).\n\t\tWrites([]cache.Score{}))\n\tws.Route(ws.GET(\"/dashboard/recommend/{user-id}\").To(m.getRecommend).\n\t\tDoc(\"Get recommendation for user.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"dashboard\"}).\n\t\tParam(ws.PathParameter(\"user-id\", \"identifier of the user\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"category\", \"category of items\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"number of returned items\").DataType(\"int\")).\n\t\tReturns(http.StatusOK, \"OK\", []data.Item{}).\n\t\tWrites([]data.Item{}))\n\tws.Route(ws.GET(\"/dashboard/recommend/{user-id}/{recommender}\").To(m.getRecommend).\n\t\tDoc(\"Get recommendation for user.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"dashboard\"}).\n\t\tParam(ws.PathParameter(\"user-id\", \"identifier of the user\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"recommender\", \"one of `final`, `collaborative`, `user_based` and `item_based`\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"category\", \"category of items\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"number of returned items\").DataType(\"int\")).\n\t\tReturns(http.StatusOK, \"OK\", []data.Item{}).\n\t\tWrites([]data.Item{}))\n\tws.Route(ws.GET(\"/dashboard/recommend/{user-id}/{recommender}/{name}\").To(m.getRecommend).\n\t\tDoc(\"Get recommendation for user.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"dashboard\"}).\n\t\tParam(ws.PathParameter(\"user-id\", \"identifier of the user\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"recommender\", \"one of `final`, `collaborative`, `user_based` and `item_based`\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"name\", \"name of the recommender\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"category\", \"category of items\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"number of returned items\").DataType(\"int\")).\n\t\tReturns(http.StatusOK, \"OK\", []data.Item{}).\n\t\tWrites([]data.Item{}))\n\tws.Route(ws.GET(\"/dashboard/item-to-item/{name}/{item-id}\").To(m.getItemToItem).\n\t\tDoc(\"get neighbors of a item\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"recommendation\"}).\n\t\tParam(ws.PathParameter(\"item-id\", \"identifier of the item\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"number of returned items\").DataType(\"int\")).\n\t\tParam(ws.QueryParameter(\"offset\", \"offset of the list\").DataType(\"int\")).\n\t\tReturns(http.StatusOK, \"OK\", []ScoredItem{}).\n\t\tWrites([]ScoredItem{}))\n\tws.Route(ws.GET(\"/dashboard/user-to-user/{name}/{user-id}\").To(m.getUserToUser).\n\t\tDoc(\"get neighbors of a user\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"dashboard\"}).\n\t\tParam(ws.PathParameter(\"user-id\", \"identifier of the user\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"number of returned users\").DataType(\"int\")).\n\t\tParam(ws.QueryParameter(\"offset\", \"offset of the list\").DataType(\"int\")).\n\t\tReturns(http.StatusOK, \"OK\", []ScoreUser{}).\n\t\tWrites([]ScoreUser{}))\n\tws.Route(ws.GET(\"/dashboard/external\").To(m.getExternal).\n\t\tDoc(\"get external recommendations preview\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"dashboard\"}).\n\t\tParam(ws.QueryParameter(\"script\", \"external script\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"user-id\", \"identifier of the user\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", []string{}).\n\t\tWrites([]string{}))\n\tws.Route(ws.GET(\"/dashboard/ranker/prompt\").To(m.getRankerPrompt).\n\t\tDoc(\"Get ranker prompt.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{\"dashboard\"}).\n\t\tParam(ws.QueryParameter(\"query-template\", \"query template (base64)\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"document-template\", \"document template (base64)\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"user-id\", \"identifier of the user\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", RerankerPrompt{}).\n\t\tWrites(RerankerPrompt{}))\n}\n\n// SinglePageAppFileSystem is the file system for single page app.\ntype SinglePageAppFileSystem struct {\n\troot http.FileSystem\n}\n\n// Open index.html if required file not exists.\nfunc (fs *SinglePageAppFileSystem) Open(name string) (http.File, error) {\n\tf, err := fs.root.Open(name)\n\tif os.IsNotExist(err) {\n\t\treturn fs.root.Open(\"/index.html\")\n\t}\n\treturn f, err\n}\n\nfunc (m *Master) StartHttpServer() {\n\tm.CreateWebService()\n\tcontainer := restful.NewContainer()\n\tcontainer.Handle(\"/\", http.HandlerFunc(m.dashboard))\n\tcontainer.Handle(\"/login\", http.HandlerFunc(m.login))\n\tcontainer.Handle(\"/logout\", http.HandlerFunc(m.logout))\n\tcontainer.Handle(\"/callback/oauth2\", http.HandlerFunc(m.handleOAuth2Callback))\n\tcontainer.Handle(\"/api/purge\", http.HandlerFunc(m.purge))\n\tcontainer.Handle(\"/api/bulk/users\", http.HandlerFunc(m.importExportUsers))\n\tcontainer.Handle(\"/api/bulk/items\", http.HandlerFunc(m.importExportItems))\n\tcontainer.Handle(\"/api/bulk/feedback\", http.HandlerFunc(m.importExportFeedback))\n\tcontainer.Handle(\"/api/dump\", http.HandlerFunc(m.dump))\n\tcontainer.Handle(\"/api/restore\", http.HandlerFunc(m.restore))\n\tcontainer.Handle(\"/api/chat\", http.HandlerFunc(m.chat))\n\tm.RestServer.StartHttpServer(container)\n}\n\nvar (\n\tcookieHandler = securecookie.New(\n\t\tsecurecookie.GenerateRandomKey(64),\n\t\tsecurecookie.GenerateRandomKey(32))\n\tstaticFileSystem http.FileSystem\n\tstaticFileServer http.Handler\n)\n\nfunc init() {\n\tvar err error\n\tstaticFileSystem, err = fs.New()\n\tif err != nil {\n\t\tlog.Logger().Fatal(\"failed to load statik files\", zap.Error(err))\n\t}\n\tstaticFileServer = http.FileServer(&SinglePageAppFileSystem{staticFileSystem})\n\n\t// Create temporary directory if not exist\n\ttempDir := os.TempDir()\n\tif err = os.MkdirAll(tempDir, 1777); err != nil {\n\t\tlog.Logger().Fatal(\"failed to create temporary directory\", zap.String(\"directory\", tempDir), zap.Error(err))\n\t}\n}\n\n// Taken from https://github.com/mytrile/nocache\nvar noCacheHeaders = map[string]string{\n\t\"Expires\":         time.Unix(0, 0).Format(time.RFC1123),\n\t\"Cache-Control\":   \"no-cache, no-store, no-transform, must-revalidate, private, max-age=0\",\n\t\"Pragma\":          \"no-cache\",\n\t\"X-Accel-Expires\": \"0\",\n}\n\nvar etagHeaders = []string{\n\t\"ETag\",\n\t\"If-Modified-Since\",\n\t\"If-Match\",\n\t\"If-None-Match\",\n\t\"If-Range\",\n\t\"If-Unmodified-Since\",\n}\n\n// noCache is a simple piece of middleware that sets a number of HTTP headers to prevent\n// a router (or subrouter) from being cached by an upstream proxy and/or client.\n//\n// As per http://wiki.nginx.org/HttpProxyModule - noCache sets:\n//\n//\tExpires: Thu, 01 Jan 1970 00:00:00 UTC\n//\tCache-Control: no-cache, private, max-age=0\n//\tX-Accel-Expires: 0\n//\tPragma: no-cache (for HTTP/1.0 proxies/clients)\nfunc noCache(h http.Handler) http.Handler {\n\tfn := func(w http.ResponseWriter, r *http.Request) {\n\t\t// Delete any ETag headers that may have been set\n\t\tfor _, v := range etagHeaders {\n\t\t\tif r.Header.Get(v) != \"\" {\n\t\t\t\tr.Header.Del(v)\n\t\t\t}\n\t\t}\n\t\t// Set our noCache headers\n\t\tfor k, v := range noCacheHeaders {\n\t\t\tw.Header().Set(k, v)\n\t\t}\n\t\th.ServeHTTP(w, r)\n\t}\n\treturn http.HandlerFunc(fn)\n}\n\nfunc (m *Master) dashboard(response http.ResponseWriter, request *http.Request) {\n\t_, err := staticFileSystem.Open(request.RequestURI)\n\tif request.RequestURI == \"/\" || os.IsNotExist(err) {\n\t\tif !m.checkLogin(request) {\n\t\t\tif m.Config.OIDC.Enable {\n\t\t\t\t// Redirect to OIDC login\n\t\t\t\thttp.Redirect(response, request, m.oauth2Config.AuthCodeURL(\"\"), http.StatusFound)\n\t\t\t} else {\n\t\t\t\thttp.Redirect(response, request, \"/login\", http.StatusFound)\n\t\t\t\tlog.Logger().Info(fmt.Sprintf(\"%s %s\", request.Method, request.URL), zap.Int(\"status_code\", http.StatusFound))\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tnoCache(staticFileServer).ServeHTTP(response, request)\n\t\treturn\n\t}\n\tstaticFileServer.ServeHTTP(response, request)\n}\n\nfunc (m *Master) login(response http.ResponseWriter, request *http.Request) {\n\tswitch request.Method {\n\tcase http.MethodGet:\n\t\tlog.Logger().Info(\"GET /login\", zap.Int(\"status_code\", http.StatusOK))\n\t\tstaticFileServer.ServeHTTP(response, request)\n\tcase http.MethodPost:\n\t\tname := request.FormValue(\"user_name\")\n\t\tpass := request.FormValue(\"password\")\n\t\tif m.Config.Master.DashboardUserName != \"\" || m.Config.Master.DashboardPassword != \"\" {\n\t\t\tif name != m.Config.Master.DashboardUserName || pass != m.Config.Master.DashboardPassword {\n\t\t\t\thttp.Redirect(response, request, \"login?msg=incorrect\", http.StatusFound)\n\t\t\t\tlog.Logger().Info(\"POST /login\", zap.Int(\"status_code\", http.StatusUnauthorized))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tvalue := map[string]string{\n\t\t\t\t\"user_name\": name,\n\t\t\t\t\"password\":  pass,\n\t\t\t}\n\t\t\tif encoded, err := cookieHandler.Encode(\"session\", value); err != nil {\n\t\t\t\tserver.InternalServerError(restful.NewResponse(response), err)\n\t\t\t\treturn\n\t\t\t} else {\n\t\t\t\tcookie := &http.Cookie{\n\t\t\t\t\tName:  \"session\",\n\t\t\t\t\tValue: encoded,\n\t\t\t\t\tPath:  \"/\",\n\t\t\t\t}\n\t\t\t\thttp.SetCookie(response, cookie)\n\t\t\t\thttp.Redirect(response, request, \"/\", http.StatusFound)\n\t\t\t\tlog.Logger().Info(\"POST /login\", zap.Int(\"status_code\", http.StatusFound))\n\t\t\t\treturn\n\t\t\t}\n\t\t} else {\n\t\t\thttp.Redirect(response, request, \"/\", http.StatusFound)\n\t\t\tlog.Logger().Info(\"POST /login\", zap.Int(\"status_code\", http.StatusFound))\n\t\t}\n\tdefault:\n\t\tserver.BadRequest(restful.NewResponse(response), errors.New(\"unsupported method\"))\n\t}\n}\n\nfunc (m *Master) logout(response http.ResponseWriter, request *http.Request) {\n\tcookie := &http.Cookie{\n\t\tName:   \"session\",\n\t\tValue:  \"\",\n\t\tPath:   \"/\",\n\t\tMaxAge: -1,\n\t}\n\thttp.SetCookie(response, cookie)\n\thttp.Redirect(response, request, \"/login\", http.StatusFound)\n\tlog.Logger().Info(fmt.Sprintf(\"%s %s\", request.Method, request.RequestURI), zap.Int(\"status_code\", http.StatusFound))\n}\n\nfunc (m *Master) LoginFilter(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {\n\tif m.checkLogin(req.Request) {\n\t\treq.Request.Header.Set(\"X-API-Key\", m.Config.Server.APIKey)\n\t\tchain.ProcessFilter(req, resp)\n\t} else if !strings.HasPrefix(req.SelectedRoutePath(), \"/api/dashboard\") {\n\t\tchain.ProcessFilter(req, resp)\n\t} else {\n\t\tif err := resp.WriteError(http.StatusUnauthorized, fmt.Errorf(\"unauthorized\")); err != nil {\n\t\t\tlog.ResponseLogger(resp).Error(\"failed to write error\", zap.Error(err))\n\t\t}\n\t}\n}\n\nfunc (m *Master) checkLogin(request *http.Request) bool {\n\tif m.Config.Master.AdminAPIKey != \"\" && m.Config.Master.AdminAPIKey == request.Header.Get(\"X-Api-Key\") {\n\t\treturn true\n\t}\n\tif m.Config.OIDC.Enable {\n\t\tif tokenCookie, err := request.Cookie(\"id_token\"); err == nil {\n\t\t\tvar token string\n\t\t\tif err = cookieHandler.Decode(\"id_token\", tokenCookie.Value, &token); err == nil {\n\t\t\t\tif m.tokenCache.Get(token) != nil {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false\n\t} else if m.Config.Master.DashboardUserName != \"\" || m.Config.Master.DashboardPassword != \"\" {\n\t\tif sessionCookie, err := request.Cookie(\"session\"); err == nil {\n\t\t\tcookieValue := make(map[string]string)\n\t\t\tif err = cookieHandler.Decode(\"session\", sessionCookie.Value, &cookieValue); err == nil {\n\t\t\t\tuserName := cookieValue[\"user_name\"]\n\t\t\t\tpassword := cookieValue[\"password\"]\n\t\t\t\tif userName == m.Config.Master.DashboardUserName && password == m.Config.Master.DashboardPassword {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (m *Master) handleUserInfo(request *restful.Request, response *restful.Response) {\n\tif m.Config.OIDC.Enable {\n\t\tif tokenCookie, err := request.Request.Cookie(\"id_token\"); err == nil {\n\t\t\tvar token string\n\t\t\tif err = cookieHandler.Decode(\"id_token\", tokenCookie.Value, &token); err == nil {\n\t\t\t\tif item := m.tokenCache.Get(token); item != nil {\n\t\t\t\t\tuserInfo := item.Value()\n\t\t\t\t\tuserInfo.AuthType = \"OIDC\"\n\t\t\t\t\tserver.Ok(response, userInfo)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else if m.Config.Master.DashboardUserName != \"\" {\n\t\tserver.Ok(response, UserInfo{\n\t\t\tName: m.Config.Master.DashboardUserName,\n\t\t})\n\t} else {\n\t\tresponse.Header().Set(\"Content-Type\", \"application/json\")\n\t\tif _, err := response.Write([]byte(\"null\")); err != nil {\n\t\t\tlog.ResponseLogger(response).Error(\"failed to write response\", zap.Error(err))\n\t\t}\n\t}\n}\n\nfunc (m *Master) getCategories(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\tcategoryScores, err := m.CacheClient.SearchScores(ctx, cache.ItemCategories, \"\", nil, 0, -1)\n\tif err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\tcategories := make([]string, len(categoryScores))\n\tfor i, score := range categoryScores {\n\t\tcategories[i] = score.Id\n\t}\n\tserver.Ok(response, categories)\n}\n\nfunc (m *Master) getCluster(_ *restful.Request, response *restful.Response) {\n\tnodes, err := m.metaStore.ListNodes()\n\tif err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\tsort.Slice(nodes, func(i, j int) bool {\n\t\treturn nodes[i].Type < nodes[j].Type\n\t})\n\tserver.Ok(response, nodes)\n}\n\nfunc formatConfig(configMap map[string]interface{}) map[string]interface{} {\n\treturn lo.MapValues(configMap, func(v interface{}, _ string) interface{} {\n\t\tswitch value := v.(type) {\n\t\tcase time.Duration:\n\t\t\ts := value.String()\n\t\t\tif strings.HasSuffix(s, \"m0s\") {\n\t\t\t\ts = s[:len(s)-2]\n\t\t\t}\n\t\t\tif strings.HasSuffix(s, \"h0m\") {\n\t\t\t\ts = s[:len(s)-2]\n\t\t\t}\n\t\t\treturn s\n\t\tcase map[string]interface{}:\n\t\t\treturn formatConfig(value)\n\t\tdefault:\n\t\t\treturn v\n\t\t}\n\t})\n}\n\nfunc (m *Master) postConfig(request *restful.Request, response *restful.Response) {\n\tvar newConfigMap map[string]any\n\tif err := request.ReadEntity(&newConfigMap); err != nil {\n\t\tserver.BadRequest(response, err)\n\t\treturn\n\t}\n\tvar newConfig config.Config\n\tdecoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{\n\t\tDecodeHook: mapstructure.ComposeDecodeHookFunc(\n\t\t\tmapstructure.StringToTimeDurationHookFunc(),\n\t\t\tconfig.StringToFeedbackTypeHookFunc(),\n\t\t),\n\t\tResult: &newConfig,\n\t})\n\tif err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\tif err = decoder.Decode(newConfigMap); err != nil {\n\t\tserver.BadRequest(response, err)\n\t\treturn\n\t}\n\tconfigForValidation := *m.Config\n\tconfigForValidation.Recommend = newConfig.Recommend\n\tif err = configForValidation.Validate(); err != nil {\n\t\tserver.BadRequest(response, err)\n\t\treturn\n\t}\n\trecommendConfigBytes, err := json.Marshal(newConfig.Recommend)\n\tif err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\tif err = m.metaStore.Put(meta.RECOMMEND_CONFIG, string(recommendConfigBytes)); err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\tm.Config.Recommend = newConfig.Recommend\n\n\tm.cancel()\n\tselect {\n\tcase m.scheduled <- struct{}{}:\n\tdefault:\n\t}\n\tserver.Ok(response, newConfig)\n}\n\nfunc (m *Master) getConfig(_ *restful.Request, response *restful.Response) {\n\tvar configMap map[string]interface{}\n\terr := mapstructure.Decode(m.Config, &configMap)\n\tif err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\tif m.Config.Master.DashboardRedacted {\n\t\tdelete(configMap, \"database\")\n\t}\n\tserver.Ok(response, formatConfig(configMap))\n}\n\nfunc (m *Master) deleteConfig(_request *restful.Request, response *restful.Response) {\n\tif err := m.metaStore.Delete(meta.RECOMMEND_CONFIG); err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\tnewConfig, err := config.LoadConfig(m.configPath)\n\tif err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\tm.Config.Recommend = newConfig.Recommend\n\n\tm.cancel()\n\tselect {\n\tcase m.scheduled <- struct{}{}:\n\tdefault:\n\t}\n\tserver.Ok(response, struct{}{})\n}\n\nfunc (m *Master) getConfigSchema(_ *restful.Request, response *restful.Response) {\n\tserver.Ok(response, jsonschema.Reflect(m.Config))\n}\n\ntype Status struct {\n\tBinaryVersion          string\n\tNumServers             int\n\tNumWorkers             int\n\tNumUsers               int\n\tNumItems               int\n\tNumUserLabels          int\n\tNumItemLabels          int\n\tNumTotalPosFeedback    int\n\tNumValidPosFeedback    int\n\tNumValidNegFeedback    int\n\tPopularItemsUpdateTime time.Time\n\tLatestItemsUpdateTime  time.Time\n\tMatchingModelFitTime   time.Time\n\tMatchingModelScore     cf.Score\n\tRankingModelFitTime    time.Time\n\tRankingModelScore      ctr.Score\n}\n\nfunc (m *Master) getStats(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\tstatus := Status{BinaryVersion: version.Version}\n\tvar err error\n\t// read number of users\n\tif status.NumUsers, err = m.CacheClient.Get(ctx, cache.Key(cache.GlobalMeta, cache.NumUsers)).Integer(); err != nil {\n\t\tlog.ResponseLogger(response).Warn(\"failed to get number of users\", zap.Error(err))\n\t}\n\t// read number of items\n\tif status.NumItems, err = m.CacheClient.Get(ctx, cache.Key(cache.GlobalMeta, cache.NumItems)).Integer(); err != nil {\n\t\tlog.ResponseLogger(response).Warn(\"failed to get number of items\", zap.Error(err))\n\t}\n\t// read number of user labels\n\tif status.NumUserLabels, err = m.CacheClient.Get(ctx, cache.Key(cache.GlobalMeta, cache.NumUserLabels)).Integer(); err != nil {\n\t\tlog.ResponseLogger(response).Warn(\"failed to get number of user labels\", zap.Error(err))\n\t}\n\t// read number of item labels\n\tif status.NumItemLabels, err = m.CacheClient.Get(ctx, cache.Key(cache.GlobalMeta, cache.NumItemLabels)).Integer(); err != nil {\n\t\tlog.ResponseLogger(response).Warn(\"failed to get number of item labels\", zap.Error(err))\n\t}\n\t// read number of total positive feedback\n\tif status.NumTotalPosFeedback, err = m.CacheClient.Get(ctx, cache.Key(cache.GlobalMeta, cache.NumTotalPosFeedbacks)).Integer(); err != nil {\n\t\tlog.ResponseLogger(response).Warn(\"failed to get number of total positive feedbacks\", zap.Error(err))\n\t}\n\t// read number of valid positive feedback\n\tif status.NumValidPosFeedback, err = m.CacheClient.Get(ctx, cache.Key(cache.GlobalMeta, cache.NumValidPosFeedbacks)).Integer(); err != nil {\n\t\tlog.ResponseLogger(response).Warn(\"failed to get number of valid positive feedbacks\", zap.Error(err))\n\t}\n\t// read number of valid negative feedback\n\tif status.NumValidNegFeedback, err = m.CacheClient.Get(ctx, cache.Key(cache.GlobalMeta, cache.NumValidNegFeedbacks)).Integer(); err != nil {\n\t\tlog.ResponseLogger(response).Warn(\"failed to get number of valid negative feedbacks\", zap.Error(err))\n\t}\n\t// count the number of workers and servers\n\tnodes, err := m.metaStore.ListNodes()\n\tif err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\tfor _, node := range nodes {\n\t\tswitch node.Type {\n\t\tcase protocol.NodeType_Server.String():\n\t\t\tstatus.NumServers++\n\t\tcase protocol.NodeType_Worker.String():\n\t\t\tstatus.NumWorkers++\n\t\t}\n\t}\n\t// read popular items update time\n\tif status.PopularItemsUpdateTime, err = m.CacheClient.Get(ctx, cache.Key(cache.GlobalMeta, cache.LastUpdatePopularItemsTime)).Time(); err != nil {\n\t\tlog.ResponseLogger(response).Warn(\"failed to get popular items update time\", zap.Error(err))\n\t}\n\t// read the latest items update time\n\tif status.LatestItemsUpdateTime, err = m.CacheClient.Get(ctx, cache.Key(cache.GlobalMeta, cache.LastUpdateLatestItemsTime)).Time(); err != nil {\n\t\tlog.ResponseLogger(response).Warn(\"failed to get latest items update time\", zap.Error(err))\n\t}\n\t// read last fit matching model time\n\tif status.MatchingModelFitTime, err = m.CacheClient.Get(ctx, cache.Key(cache.GlobalMeta, cache.LastFitMatchingModelTime)).Time(); err != nil {\n\t\tlog.ResponseLogger(response).Warn(\"failed to get last fit matching model time\", zap.Error(err))\n\t}\n\t// read last fit ranking model time\n\tif status.RankingModelFitTime, err = m.CacheClient.Get(ctx, cache.Key(cache.GlobalMeta, cache.LastFitRankingModelTime)).Time(); err != nil {\n\t\tlog.ResponseLogger(response).Warn(\"failed to get last fit ranking model time\", zap.Error(err))\n\t}\n\tserver.Ok(response, status)\n}\n\nfunc (m *Master) getTasks(_ *restful.Request, response *restful.Response) {\n\t// List workers\n\tworkers := mapset.NewSet[string]()\n\tnodes, err := m.metaStore.ListNodes()\n\tif err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\tfor _, node := range nodes {\n\t\tif node.Type == protocol.NodeType_Worker.String() {\n\t\t\tworkers.Add(node.UUID)\n\t\t}\n\t}\n\t// List local progress\n\tprogressList := m.tracer.List()\n\t// list remote progress\n\tm.remoteProgress.Range(func(key, value interface{}) bool {\n\t\tif workers.Contains(key.(string)) {\n\t\t\tprogressList = append(progressList, value.([]monitor.Progress)...)\n\t\t}\n\t\treturn true\n\t})\n\tserver.Ok(response, progressList)\n}\n\nfunc (m *Master) getTimeseries(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\t// get time series name\n\tname := request.PathParameter(\"name\")\n\t// get begin time\n\tbeginStr := request.QueryParameter(\"begin\")\n\tif beginStr == \"\" {\n\t\tbeginStr = time.Now().Add(-7 * 24 * time.Hour).Format(time.RFC3339)\n\t}\n\tbegin, err := dateparse.ParseAny(beginStr)\n\tif err != nil {\n\t\tserver.BadRequest(response, err)\n\t\treturn\n\t}\n\t// get end time\n\tendStr := request.QueryParameter(\"end\")\n\tif endStr == \"\" {\n\t\tendStr = time.Now().Format(time.RFC3339)\n\t}\n\tend, err := dateparse.ParseAny(endStr)\n\tif err != nil {\n\t\tserver.BadRequest(response, err)\n\t\treturn\n\t}\n\t// get duration\n\tdurationStr := request.QueryParameter(\"duration\")\n\tif durationStr == \"\" {\n\t\tdurationStr = \"24h\"\n\t}\n\tduration, err := time.ParseDuration(durationStr)\n\tif err != nil {\n\t\tserver.BadRequest(response, err)\n\t\treturn\n\t}\n\t// get time series data\n\tdata, err := m.CacheClient.GetTimeSeriesPoints(ctx, cache.Key(name), begin, end, duration)\n\tif err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\tserver.Ok(response, data)\n}\n\ntype UserIterator struct {\n\tCursor string\n\tUsers  []User\n}\n\ntype User struct {\n\tdata.User\n\tLastActiveTime time.Time\n\tLastUpdateTime time.Time\n}\n\nfunc (m *Master) getUser(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\t// get user id\n\tuserId := request.PathParameter(\"user-id\")\n\t// get user\n\tuser, err := m.DataClient.GetUser(ctx, userId)\n\tif err != nil {\n\t\tif errors.Is(err, errors.NotFound) {\n\t\t\tserver.PageNotFound(response, err)\n\t\t} else {\n\t\t\tserver.InternalServerError(response, err)\n\t\t}\n\t\treturn\n\t}\n\tdetail := User{User: user}\n\tif detail.LastActiveTime, err = m.CacheClient.Get(ctx, cache.Key(cache.LastModifyUserTime, user.UserId)).Time(); err != nil && !errors.Is(err, errors.NotFound) {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\tif detail.LastUpdateTime, err = m.CacheClient.Get(ctx, cache.Key(cache.RecommendUpdateTime, user.UserId)).Time(); err != nil && !errors.Is(err, errors.NotFound) {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\tserver.Ok(response, detail)\n}\n\nfunc (m *Master) getUsers(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\t// Authorize\n\tcursor := request.QueryParameter(\"cursor\")\n\tn, err := server.ParseInt(request, \"n\", m.Config.Server.DefaultN)\n\tif err != nil {\n\t\tserver.BadRequest(response, err)\n\t\treturn\n\t}\n\t// get all users\n\tcursor, users, err := m.DataClient.GetUsers(ctx, cursor, n)\n\tif err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\tdetails := make([]User, len(users))\n\tfor i, user := range users {\n\t\tdetails[i].User = user\n\t\tif details[i].LastActiveTime, err = m.CacheClient.Get(ctx, cache.Key(cache.LastModifyUserTime, user.UserId)).Time(); err != nil && !errors.Is(err, errors.NotFound) {\n\t\t\tserver.InternalServerError(response, err)\n\t\t\treturn\n\t\t}\n\t\tif details[i].LastUpdateTime, err = m.CacheClient.Get(ctx, cache.Key(cache.RecommendUpdateTime, user.UserId)).Time(); err != nil && !errors.Is(err, errors.NotFound) {\n\t\t\tserver.InternalServerError(response, err)\n\t\t\treturn\n\t\t}\n\t}\n\tserver.Ok(response, UserIterator{Cursor: cursor, Users: details})\n}\n\nfunc (m *Master) getRecommend(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\t// parse arguments\n\trecommenderType := request.PathParameter(\"recommender\")\n\trecommenderName := request.PathParameter(\"name\")\n\tuserId := request.PathParameter(\"user-id\")\n\tcategories := server.ReadCategories(request, nil)\n\tn, err := server.ParseInt(request, \"n\", m.Config.Server.DefaultN)\n\tif err != nil {\n\t\tserver.BadRequest(response, err)\n\t\treturn\n\t}\n\n\trecommender, err := logics.NewRecommender(m.Config.Recommend, m.CacheClient, m.DataClient, true, userId, categories)\n\tif err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\tvar scores []cache.Score\n\tif recommenderType != \"\" {\n\t\tvar name string\n\t\tif recommenderName != \"\" {\n\t\t\tname = recommenderType + \"/\" + recommenderName\n\t\t} else {\n\t\t\tname = recommenderType\n\t\t}\n\t\tscores, _, err = recommender.RecommendSequential(ctx, scores, n, name)\n\t} else {\n\t\tscores, err = recommender.Recommend(ctx, n)\n\t}\n\tif err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\tresults := lo.Map(scores, func(item cache.Score, index int) string {\n\t\treturn item.Id\n\t})\n\n\t// Get item details\n\titems, err := m.DataClient.BatchGetItems(ctx, lo.Map(results, func(id string, _ int) string {\n\t\treturn id\n\t}))\n\tif err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\titemsMap := make(map[string]data.Item, len(items))\n\tfor _, item := range items {\n\t\titemsMap[item.ItemId] = item\n\t}\n\n\t// Send result\n\tdetails := make([]ScoredItem, 0, len(results))\n\tfor i := range results {\n\t\tdetail, exist := itemsMap[results[i]]\n\t\tif exist {\n\t\t\tdetails = append(details, ScoredItem{Item: detail, Score: scores[i].Score})\n\t\t} else {\n\t\t\tlog.Logger().Warn(\"recommended item doesn't exist\", zap.String(\"item_id\", results[i]))\n\t\t}\n\t}\n\tserver.Ok(response, details)\n}\n\ntype DetailedFeedback struct {\n\tFeedbackType string\n\tUserId       string\n\tItem         data.Item\n\tValue        float64\n\tTimestamp    time.Time\n\tComment      string\n}\n\n// get feedback by user-id with feedback type\nfunc (m *Master) getUserFeedback(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\n\t// parse feedback type\n\tfeedbackType := request.PathParameter(\"feedback-type\")\n\tvar feedbackTypeExpressions []expression.FeedbackTypeExpression\n\tif feedbackType != \"\" {\n\t\tfeedbackTypeExpressions = make([]expression.FeedbackTypeExpression, 1)\n\t\tif err := feedbackTypeExpressions[0].FromString(feedbackType); err != nil {\n\t\t\tserver.BadRequest(response, fmt.Errorf(\"invalid feedback type `%s`: %w\", feedbackType, err))\n\t\t\treturn\n\t\t}\n\t} else {\n\t\tfeedbackTypeExpressions = m.Config.Recommend.DataSource.PositiveFeedbackTypes\n\t}\n\n\t// parse n\n\tnStr := request.QueryParameter(\"n\")\n\tn := m.Config.Server.DefaultN\n\tif nStr != \"\" {\n\t\tvar err error\n\t\tn, err = server.ParseInt(request, \"n\", m.Config.Server.DefaultN)\n\t\tif err != nil {\n\t\t\tserver.BadRequest(response, err)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// parse offset\n\toffsetStr := request.QueryParameter(\"offset\")\n\toffset := 0\n\tif offsetStr != \"\" {\n\t\tvar err error\n\t\toffset, err = server.ParseInt(request, \"offset\", 0)\n\t\tif err != nil {\n\t\t\tserver.BadRequest(response, err)\n\t\t\treturn\n\t\t}\n\t}\n\n\tuserId := request.PathParameter(\"user-id\")\n\tfeedback, err := m.DataClient.GetUserFeedback(ctx, userId, m.Config.Now(), feedbackTypeExpressions...)\n\tif err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\tsort.Slice(feedback, func(i, j int) bool {\n\t\treturn feedback[i].Timestamp.After(feedback[j].Timestamp)\n\t})\n\tif offset <= len(feedback) {\n\t\tfeedback = feedback[offset:]\n\t}\n\tif n < len(feedback) {\n\t\tfeedback = feedback[:n]\n\t}\n\n\t// Get item details\n\titems, err := m.DataClient.BatchGetItems(ctx, lo.Map(feedback, func(f data.Feedback, _ int) string {\n\t\treturn f.ItemId\n\t}))\n\tif err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\titemsMap := make(map[string]data.Item, len(items))\n\tfor _, item := range items {\n\t\titemsMap[item.ItemId] = item\n\t}\n\n\tdetails := make([]DetailedFeedback, len(feedback))\n\tfor i := range feedback {\n\t\tdetails[i].FeedbackType = feedback[i].FeedbackType\n\t\tdetails[i].UserId = feedback[i].UserId\n\t\tdetails[i].Value = feedback[i].Value\n\t\tdetails[i].Timestamp = feedback[i].Timestamp\n\t\tdetails[i].Comment = feedback[i].Comment\n\t\tvar exist bool\n\t\tdetails[i].Item, exist = itemsMap[feedback[i].ItemId]\n\t\tif !exist {\n\t\t\tdetails[i].Item = data.Item{ItemId: feedback[i].ItemId, Comment: \"** This item doesn't exist in Gorse **\"}\n\t\t}\n\t}\n\tserver.Ok(response, details)\n}\n\ntype ScoredItem struct {\n\tdata.Item\n\tScore float64\n}\n\ntype ScoreUser struct {\n\tdata.User\n\tScore float64\n}\n\nfunc (m *Master) GetItem(score cache.Score) (any, error) {\n\tvar item ScoredItem\n\tvar err error\n\titem.Score = score.Score\n\titem.Item, err = m.DataClient.GetItem(context.Background(), score.Id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn item, nil\n}\n\nfunc (m *Master) GetUser(score cache.Score) (any, error) {\n\tvar user ScoreUser\n\tvar err error\n\tuser.Score = score.Score\n\tuser.User, err = m.DataClient.GetUser(context.Background(), score.Id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn user, nil\n}\n\nfunc (m *Master) getLatest(request *restful.Request, response *restful.Response) {\n\tvar (\n\t\toffset int\n\t\tn      int\n\t\terr    error\n\t)\n\tcategories := server.ReadCategories(request, nil)\n\tif offset, err = server.ParseInt(request, \"offset\", 0); err != nil {\n\t\tserver.BadRequest(response, err)\n\t\treturn\n\t}\n\tif n, err = server.ParseInt(request, \"n\", m.Config.Server.DefaultN); err != nil {\n\t\tserver.BadRequest(response, err)\n\t\treturn\n\t}\n\titems, err := m.DataClient.GetLatestItems(context.Background(), offset+n, categories)\n\tif err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\tif offset < len(items) {\n\t\titems = items[offset:]\n\t}\n\tif n < len(items) {\n\t\titems = items[:n]\n\t}\n\tscores := make([]ScoredItem, len(items))\n\tfor i := range items {\n\t\tscores[i] = ScoredItem{Item: items[i], Score: float64(items[i].Timestamp.Unix())}\n\t}\n\tm.SetLastModified(request, response, time.Now().String())\n\tserver.Ok(response, scores)\n}\n\nfunc (m *Master) getNonPersonalized(request *restful.Request, response *restful.Response) {\n\tname := request.PathParameter(\"name\")\n\tcategories := server.ReadCategories(request, []string{\"\"})\n\tm.SetLastModified(request, response, cache.Key(cache.NonPersonalizedUpdateTime, name))\n\tm.SearchDocuments(cache.NonPersonalized, name, categories, m.GetItem, request, response)\n}\n\nfunc (m *Master) getItemToItem(request *restful.Request, response *restful.Response) {\n\tname := request.PathParameter(\"name\")\n\titemId := request.PathParameter(\"item-id\")\n\tcategories := request.QueryParameters(\"category\")\n\tm.SetLastModified(request, response, cache.Key(cache.ItemToItemUpdateTime, name, itemId))\n\tm.SearchDocuments(cache.ItemToItem, cache.Key(name, itemId), categories, m.GetItem, request, response)\n}\n\nfunc (m *Master) getUserToUser(request *restful.Request, response *restful.Response) {\n\tuserId := request.PathParameter(\"user-id\")\n\tname := request.PathParameter(\"name\")\n\tm.SetLastModified(request, response, cache.Key(cache.UserToUserUpdateTime, name, userId))\n\tm.SearchDocuments(cache.UserToUser, cache.Key(name, userId), nil, m.GetUser, request, response)\n}\n\nfunc (m *Master) getExternal(request *restful.Request, response *restful.Response) {\n\tscriptBase64 := request.QueryParameter(\"script\")\n\tif scriptBase64 == \"\" {\n\t\tserver.BadRequest(response, fmt.Errorf(\"script is required\"))\n\t\treturn\n\t}\n\tuserId := request.QueryParameter(\"user-id\")\n\n\tscriptBytes, err := base64.StdEncoding.DecodeString(scriptBase64)\n\tif err != nil {\n\t\tserver.BadRequest(response, fmt.Errorf(\"invalid script encoding: %w\", err))\n\t\treturn\n\t}\n\n\texternal, err := logics.NewExternal(config.ExternalConfig{\n\t\tName:   \"preview\",\n\t\tScript: string(scriptBytes),\n\t})\n\tif err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\tdefer external.Close()\n\n\titems, err := external.Pull(userId)\n\tif err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\n\tm.SetLastModified(request, response, time.Now().String())\n\tserver.Ok(response, items)\n}\n\nfunc (m *Master) getRankerPrompt(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\tqueryTplBase64 := request.QueryParameter(\"query-template\")\n\tdocumentTplBase64 := request.QueryParameter(\"document-template\")\n\tif queryTplBase64 == \"\" || documentTplBase64 == \"\" {\n\t\tserver.BadRequest(response, fmt.Errorf(\"query-template and document-template are required\"))\n\t\treturn\n\t}\n\tuserId := request.QueryParameter(\"user-id\")\n\tif userId == \"\" {\n\t\tserver.BadRequest(response, fmt.Errorf(\"user-id is required\"))\n\t\treturn\n\t}\n\n\tqueryTplBytes, err := base64.StdEncoding.DecodeString(queryTplBase64)\n\tif err != nil {\n\t\tserver.BadRequest(response, fmt.Errorf(\"invalid query-template encoding: %w\", err))\n\t\treturn\n\t}\n\tqueryTpl, err := gonja.FromString(string(queryTplBytes))\n\tif err != nil {\n\t\tserver.BadRequest(response, err)\n\t\treturn\n\t}\n\n\tdocumentTplBytes, err := base64.StdEncoding.DecodeString(documentTplBase64)\n\tif err != nil {\n\t\tserver.BadRequest(response, fmt.Errorf(\"invalid document-template encoding: %w\", err))\n\t\treturn\n\t}\n\tdocumentTpl, err := gonja.FromString(string(documentTplBytes))\n\tif err != nil {\n\t\tserver.BadRequest(response, err)\n\t\treturn\n\t}\n\n\tuser, err := m.DataClient.GetUser(ctx, userId)\n\tif err != nil {\n\t\tif errors.Is(err, errors.NotFound) {\n\t\t\tserver.PageNotFound(response, err)\n\t\t} else {\n\t\t\tserver.InternalServerError(response, err)\n\t\t}\n\t\treturn\n\t}\n\tfeedbacks, err := m.DataClient.GetUserFeedback(ctx, userId, m.Config.Now(), m.Config.Recommend.DataSource.PositiveFeedbackTypes...)\n\tif err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\tdata.SortFeedbacks(feedbacks)\n\tif len(feedbacks) > 10 {\n\t\tfeedbacks = feedbacks[:10]\n\t}\n\titemsById := map[string]data.Item{}\n\tif len(feedbacks) > 0 {\n\t\tfeedbackItemIds := lo.Map(feedbacks, func(fb data.Feedback, _ int) string {\n\t\t\treturn fb.ItemId\n\t\t})\n\t\tfeedbackItems, err := m.DataClient.BatchGetItems(ctx, feedbackItemIds)\n\t\tif err != nil {\n\t\t\tserver.InternalServerError(response, err)\n\t\t\treturn\n\t\t}\n\t\titemsById = make(map[string]data.Item, len(feedbackItems))\n\t\tfor _, item := range feedbackItems {\n\t\t\titemsById[item.ItemId] = item\n\t\t}\n\t}\n\tfeedbackItems := make([]*logics.FeedbackItem, 0, len(feedbacks))\n\tfor _, fb := range feedbacks {\n\t\tif item, exist := itemsById[fb.ItemId]; exist {\n\t\t\tfeedbackItems = append(feedbackItems, &logics.FeedbackItem{\n\t\t\t\tFeedbackType: fb.FeedbackType,\n\t\t\t\tItem:         item,\n\t\t\t})\n\t\t}\n\t}\n\n\tlatestItems, err := m.DataClient.GetLatestItems(ctx, 100, nil)\n\tif err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\n\t// render query\n\tvar queryBuf strings.Builder\n\tqueryCtx := exec.NewContext(map[string]any{\n\t\t\"user\":     &user,\n\t\t\"feedback\": feedbackItems,\n\t})\n\tif err := queryTpl.Execute(&queryBuf, queryCtx); err != nil {\n\t\tserver.InternalServerError(response, err)\n\t\treturn\n\t}\n\n\t// render documents\n\tdocuments := make([]string, len(latestItems))\n\tfor i, item := range latestItems {\n\t\tvar docBuf strings.Builder\n\t\tdocCtx := exec.NewContext(map[string]any{\n\t\t\t\"item\": item,\n\t\t})\n\t\tif err := documentTpl.Execute(&docBuf, docCtx); err != nil {\n\t\t\tserver.InternalServerError(response, err)\n\t\t\treturn\n\t\t}\n\t\tdocuments[i] = docBuf.String()\n\t}\n\n\tserver.Ok(response, RerankerPrompt{\n\t\tQuery:     queryBuf.String(),\n\t\tDocuments: documents,\n\t})\n}\n\nfunc (m *Master) importExportUsers(response http.ResponseWriter, request *http.Request) {\n\tctx := context.Background()\n\tif request != nil {\n\t\tctx = request.Context()\n\t}\n\tif !m.checkLogin(request) {\n\t\tresp := restful.NewResponse(response)\n\t\terr := resp.WriteErrorString(http.StatusUnauthorized, \"unauthorized\")\n\t\tif err != nil {\n\t\t\tserver.InternalServerError(resp, err)\n\t\t\treturn\n\t\t}\n\t\treturn\n\t}\n\tswitch request.Method {\n\tcase http.MethodGet:\n\t\tvar err error\n\t\tresponse.Header().Set(\"Content-Type\", \"application/jsonl\")\n\t\tresponse.Header().Set(\"Content-Disposition\", \"attachment;filename=users.jsonl\")\n\t\tencoder := json.NewEncoder(response)\n\t\tuserStream, errChan := m.DataClient.GetUserStream(ctx, batchSize)\n\t\tfor users := range userStream {\n\t\t\tfor _, user := range users {\n\t\t\t\tif err = encoder.Encode(user); err != nil {\n\t\t\t\t\tserver.InternalServerError(restful.NewResponse(response), err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif err = <-errChan; err != nil {\n\t\t\tserver.InternalServerError(restful.NewResponse(response), errors.Trace(err))\n\t\t\treturn\n\t\t}\n\tcase http.MethodPost:\n\t\t// open file\n\t\tfile, _, err := request.FormFile(\"file\")\n\t\tif err != nil {\n\t\t\tserver.BadRequest(restful.NewResponse(response), err)\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\t\t// parse and import users\n\t\tdecoder := json.NewDecoder(file)\n\t\tlineCount := 0\n\t\ttimeStart := time.Now()\n\t\tusers := make([]data.User, 0, batchSize)\n\t\tfor {\n\t\t\t// parse line\n\t\t\tvar user data.User\n\t\t\tif err = decoder.Decode(&user); err != nil {\n\t\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tserver.BadRequest(restful.NewResponse(response), err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// validate user id\n\t\t\tif err = util.ValidateId(user.UserId); err != nil {\n\t\t\t\tserver.BadRequest(restful.NewResponse(response),\n\t\t\t\t\tfmt.Errorf(\"invalid user id `%v` at line %d (%s)\", user.UserId, lineCount, err.Error()))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tusers = append(users, user)\n\t\t\t// batch insert\n\t\t\tif len(users) == batchSize {\n\t\t\t\terr = m.DataClient.BatchInsertUsers(ctx, users)\n\t\t\t\tif err != nil {\n\t\t\t\t\tserver.InternalServerError(restful.NewResponse(response), err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tusers = make([]data.User, 0, batchSize)\n\t\t\t}\n\t\t\tlineCount++\n\t\t}\n\t\tif len(users) > 0 {\n\t\t\terr = m.DataClient.BatchInsertUsers(ctx, users)\n\t\t\tif err != nil {\n\t\t\t\tserver.InternalServerError(restful.NewResponse(response), err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tm.cancel()\n\t\tselect {\n\t\tcase m.scheduled <- struct{}{}:\n\t\tdefault:\n\t\t}\n\n\t\ttimeUsed := time.Since(timeStart)\n\t\tlog.Logger().Info(\"complete import users\",\n\t\t\tzap.Duration(\"time_used\", timeUsed),\n\t\t\tzap.Int(\"num_users\", lineCount))\n\t\tserver.Ok(restful.NewResponse(response), server.Success{RowAffected: lineCount})\n\tdefault:\n\t\twriteError(response, http.StatusMethodNotAllowed, \"method not allowed\")\n\t}\n}\n\nfunc (m *Master) importExportItems(response http.ResponseWriter, request *http.Request) {\n\tctx := context.Background()\n\tif request != nil {\n\t\tctx = request.Context()\n\t}\n\tif !m.checkLogin(request) {\n\t\tresp := restful.NewResponse(response)\n\t\terr := resp.WriteErrorString(http.StatusUnauthorized, \"unauthorized\")\n\t\tif err != nil {\n\t\t\tserver.InternalServerError(resp, err)\n\t\t\treturn\n\t\t}\n\t\treturn\n\t}\n\tswitch request.Method {\n\tcase http.MethodGet:\n\t\tvar err error\n\t\tresponse.Header().Set(\"Content-Type\", \"application/jsonl\")\n\t\tresponse.Header().Set(\"Content-Disposition\", \"attachment;filename=items.jsonl\")\n\t\tencoder := json.NewEncoder(response)\n\t\titemStream, errChan := m.DataClient.GetItemStream(ctx, batchSize, nil)\n\t\tfor items := range itemStream {\n\t\t\tfor _, item := range items {\n\t\t\t\tif err = encoder.Encode(item); err != nil {\n\t\t\t\t\tserver.InternalServerError(restful.NewResponse(response), err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif err = <-errChan; err != nil {\n\t\t\tserver.InternalServerError(restful.NewResponse(response), errors.Trace(err))\n\t\t\treturn\n\t\t}\n\tcase http.MethodPost:\n\t\t// open file\n\t\tfile, _, err := request.FormFile(\"file\")\n\t\tif err != nil {\n\t\t\tserver.BadRequest(restful.NewResponse(response), err)\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\t\t// parse and import items\n\t\tdecoder := json.NewDecoder(file)\n\t\tlineCount := 0\n\t\ttimeStart := time.Now()\n\t\titems := make([]data.Item, 0, batchSize)\n\t\tfor {\n\t\t\t// parse line\n\t\t\tvar item server.Item\n\t\t\tif err = decoder.Decode(&item); err != nil {\n\t\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tserver.BadRequest(restful.NewResponse(response), err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// validate item id\n\t\t\tif err = util.ValidateId(item.ItemId); err != nil {\n\t\t\t\tserver.BadRequest(restful.NewResponse(response),\n\t\t\t\t\tfmt.Errorf(\"invalid item id `%v` at line %d (%s)\", item.ItemId, lineCount, err.Error()))\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// validate categories\n\t\t\tfor _, category := range item.Categories {\n\t\t\t\tif err = util.ValidateId(category); err != nil {\n\t\t\t\t\tserver.BadRequest(restful.NewResponse(response),\n\t\t\t\t\t\tfmt.Errorf(\"invalid category `%v` at line %d (%s)\", category, lineCount, err.Error()))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\t// parse timestamp\n\t\t\tvar timestamp time.Time\n\t\t\tif item.Timestamp != \"\" {\n\t\t\t\ttimestamp, err = dateparse.ParseAny(item.Timestamp)\n\t\t\t\tif err != nil {\n\t\t\t\t\tserver.BadRequest(restful.NewResponse(response),\n\t\t\t\t\t\tfmt.Errorf(\"failed to parse datetime `%v` at line %v\", item.Timestamp, lineCount))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\titems = append(items, data.Item{\n\t\t\t\tItemId:     item.ItemId,\n\t\t\t\tIsHidden:   item.IsHidden,\n\t\t\t\tCategories: item.Categories,\n\t\t\t\tTimestamp:  timestamp,\n\t\t\t\tLabels:     item.Labels,\n\t\t\t\tComment:    item.Comment,\n\t\t\t})\n\t\t\t// batch insert\n\t\t\tif len(items) == batchSize {\n\t\t\t\terr = m.DataClient.BatchInsertItems(ctx, items)\n\t\t\t\tif err != nil {\n\t\t\t\t\tserver.InternalServerError(restful.NewResponse(response), err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\titems = make([]data.Item, 0, batchSize)\n\t\t\t}\n\t\t\tlineCount++\n\t\t}\n\t\tif len(items) > 0 {\n\t\t\terr = m.DataClient.BatchInsertItems(ctx, items)\n\t\t\tif err != nil {\n\t\t\t\tserver.InternalServerError(restful.NewResponse(response), err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tm.cancel()\n\t\tselect {\n\t\tcase m.scheduled <- struct{}{}:\n\t\tdefault:\n\t\t}\n\n\t\ttimeUsed := time.Since(timeStart)\n\t\tlog.Logger().Info(\"complete import items\",\n\t\t\tzap.Duration(\"time_used\", timeUsed),\n\t\t\tzap.Int(\"num_items\", lineCount))\n\t\tserver.Ok(restful.NewResponse(response), server.Success{RowAffected: lineCount})\n\tdefault:\n\t\twriteError(response, http.StatusMethodNotAllowed, \"method not allowed\")\n\t}\n}\n\nfunc (m *Master) importExportFeedback(response http.ResponseWriter, request *http.Request) {\n\tctx := context.Background()\n\tif request != nil {\n\t\tctx = request.Context()\n\t}\n\tif !m.checkLogin(request) {\n\t\twriteError(response, http.StatusUnauthorized, \"unauthorized\")\n\t\treturn\n\t}\n\tswitch request.Method {\n\tcase http.MethodGet:\n\t\tvar err error\n\t\tresponse.Header().Set(\"Content-Type\", \"application/jsonl\")\n\t\tresponse.Header().Set(\"Content-Disposition\", \"attachment;filename=feedback.jsonl\")\n\t\tencoder := json.NewEncoder(response)\n\t\tfeedbackStream, errChan := m.DataClient.GetFeedbackStream(ctx, batchSize, data.WithEndTime(*m.Config.Now()))\n\t\tfor feedback := range feedbackStream {\n\t\t\tfor _, v := range feedback {\n\t\t\t\tif err = encoder.Encode(v); err != nil {\n\t\t\t\t\tserver.InternalServerError(restful.NewResponse(response), err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif err = <-errChan; err != nil {\n\t\t\tserver.InternalServerError(restful.NewResponse(response), errors.Trace(err))\n\t\t\treturn\n\t\t}\n\tcase http.MethodPost:\n\t\t// open file\n\t\tfile, _, err := request.FormFile(\"file\")\n\t\tif err != nil {\n\t\t\tserver.BadRequest(restful.NewResponse(response), err)\n\t\t\treturn\n\t\t}\n\t\tdefer file.Close()\n\t\t// parse and import feedback\n\t\tdecoder := json.NewDecoder(file)\n\t\tlineCount := 0\n\t\ttimeStart := time.Now()\n\t\tfeedbacks := make([]data.Feedback, 0, batchSize)\n\t\tfor {\n\t\t\t// parse line\n\t\t\tvar feedback server.Feedback\n\t\t\tif err = decoder.Decode(&feedback); err != nil {\n\t\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tserver.BadRequest(restful.NewResponse(response), err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// validate feedback type\n\t\t\tif err = util.ValidateId(feedback.FeedbackType); err != nil {\n\t\t\t\tserver.BadRequest(restful.NewResponse(response),\n\t\t\t\t\tfmt.Errorf(\"invalid feedback type `%v` at line %d (%s)\", feedback.FeedbackType, lineCount, err.Error()))\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// validate user id\n\t\t\tif err = util.ValidateId(feedback.UserId); err != nil {\n\t\t\t\tserver.BadRequest(restful.NewResponse(response),\n\t\t\t\t\tfmt.Errorf(\"invalid user id `%v` at line %d (%s)\", feedback.UserId, lineCount, err.Error()))\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// validate item id\n\t\t\tif err = util.ValidateId(feedback.ItemId); err != nil {\n\t\t\t\tserver.BadRequest(restful.NewResponse(response),\n\t\t\t\t\tfmt.Errorf(\"invalid item id `%v` at line %d (%s)\", feedback.ItemId, lineCount, err.Error()))\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// parse timestamp\n\t\t\tvar timestamp time.Time\n\t\t\tif feedback.Timestamp != \"\" {\n\t\t\t\ttimestamp, err = dateparse.ParseAny(feedback.Timestamp)\n\t\t\t\tif err != nil {\n\t\t\t\t\tserver.BadRequest(restful.NewResponse(response),\n\t\t\t\t\t\tfmt.Errorf(\"failed to parse datetime `%v` at line %d\", feedback.Timestamp, lineCount))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tfeedbacks = append(feedbacks, data.Feedback{\n\t\t\t\tFeedbackKey: feedback.FeedbackKey,\n\t\t\t\tValue:       feedback.Value,\n\t\t\t\tTimestamp:   timestamp,\n\t\t\t\tComment:     feedback.Comment,\n\t\t\t})\n\t\t\t// batch insert\n\t\t\tif len(feedbacks) == batchSize {\n\t\t\t\t// batch insert to data store\n\t\t\t\terr = m.DataClient.BatchInsertFeedback(ctx, feedbacks,\n\t\t\t\t\tm.Config.Server.AutoInsertUser,\n\t\t\t\t\tm.Config.Server.AutoInsertItem, true)\n\t\t\t\tif err != nil {\n\t\t\t\t\tserver.InternalServerError(restful.NewResponse(response), err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tfeedbacks = make([]data.Feedback, 0, batchSize)\n\t\t\t}\n\t\t\tlineCount++\n\t\t}\n\t\t// insert to cache store\n\t\tif len(feedbacks) > 0 {\n\t\t\t// insert to data store\n\t\t\terr = m.DataClient.BatchInsertFeedback(ctx, feedbacks,\n\t\t\t\tm.Config.Server.AutoInsertUser,\n\t\t\t\tm.Config.Server.AutoInsertItem, true)\n\t\t\tif err != nil {\n\t\t\t\tserver.InternalServerError(restful.NewResponse(response), err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tm.cancel()\n\t\tselect {\n\t\tcase m.scheduled <- struct{}{}:\n\t\tdefault:\n\t\t}\n\n\t\ttimeUsed := time.Since(timeStart)\n\t\tlog.Logger().Info(\"complete import feedback\",\n\t\t\tzap.Duration(\"time_used\", timeUsed),\n\t\t\tzap.Int(\"num_items\", lineCount))\n\t\tserver.Ok(restful.NewResponse(response), server.Success{RowAffected: lineCount})\n\tdefault:\n\t\twriteError(response, http.StatusMethodNotAllowed, \"method not allowed\")\n\t}\n}\n\nvar checkList = mapset.NewSet(\"delete_users\", \"delete_items\", \"delete_feedback\", \"delete_cache\")\n\nfunc (m *Master) purge(response http.ResponseWriter, request *http.Request) {\n\t// check method\n\tif request.Method != http.MethodPost {\n\t\twriteError(response, http.StatusMethodNotAllowed, \"method not allowed\")\n\t\treturn\n\t}\n\t// check login\n\tif !m.checkLogin(request) {\n\t\tresp := restful.NewResponse(response)\n\t\terr := resp.WriteErrorString(http.StatusUnauthorized, \"unauthorized\")\n\t\tif err != nil {\n\t\t\tserver.InternalServerError(resp, err)\n\t\t\treturn\n\t\t}\n\t\treturn\n\t}\n\t// check password\n\tif m.Config.Master.DashboardPassword == \"\" {\n\t\twriteError(response, http.StatusUnauthorized, \"purge is not allowed without dashboard password\")\n\t\treturn\n\t}\n\t// check list\n\tif err := request.ParseForm(); err != nil {\n\t\tserver.BadRequest(restful.NewResponse(response), err)\n\t\treturn\n\t}\n\tcheckedList := strings.Split(request.Form.Get(\"check_list\"), \",\")\n\tif !checkList.Equal(mapset.NewSet(checkedList...)) {\n\t\twriteError(response, http.StatusUnauthorized, \"please confirm by checking all\")\n\t\treturn\n\t}\n\t// purge data\n\tif err := m.DataClient.Purge(); err != nil {\n\t\twriteError(response, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tif err := m.CacheClient.Purge(); err != nil {\n\t\twriteError(response, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n}\n\nfunc writeError(response http.ResponseWriter, httpStatus int, message string) {\n\tlog.Logger().Error(strings.ToLower(http.StatusText(httpStatus)), zap.String(\"error\", message))\n\tresponse.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\tresponse.WriteHeader(httpStatus)\n\tif _, err := response.Write([]byte(message)); err != nil {\n\t\tlog.Logger().Error(\"failed to write error\", zap.Error(err))\n\t}\n}\n\nfunc (m *Master) checkAdmin(request *http.Request) bool {\n\tif m.Config.Master.AdminAPIKey == \"\" {\n\t\treturn true\n\t}\n\tif request.FormValue(\"X-API-Key\") == m.Config.Master.AdminAPIKey {\n\t\treturn true\n\t}\n\treturn false\n}\n\nconst (\n\tEOF            = int64(0)\n\tUserStream     = int64(-1)\n\tItemStream     = int64(-2)\n\tFeedbackStream = int64(-3)\n)\n\ntype DumpStats struct {\n\tUsers    int\n\tItems    int\n\tFeedback int\n\tDuration time.Duration\n}\n\nfunc writeDump[T proto.Message](w io.Writer, data T) error {\n\tbytes, err := proto.Marshal(data)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err = binary.Write(w, binary.LittleEndian, int64(len(bytes))); err != nil {\n\t\treturn err\n\t}\n\tif _, err = w.Write(bytes); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc readDump[T proto.Message](r io.Reader, data T) (int64, error) {\n\tvar size int64\n\tif err := binary.Read(r, binary.LittleEndian, &size); err != nil {\n\t\treturn 0, err\n\t}\n\tif size <= 0 {\n\t\treturn size, nil\n\t}\n\tbytes := make([]byte, size)\n\tif _, err := io.ReadFull(r, bytes); err != nil {\n\t\treturn 0, err\n\t}\n\treturn size, proto.Unmarshal(bytes, data)\n}\n\nfunc (m *Master) dump(response http.ResponseWriter, request *http.Request) {\n\tif !m.checkAdmin(request) {\n\t\twriteError(response, http.StatusUnauthorized, \"unauthorized\")\n\t\treturn\n\t}\n\tif request.Method != http.MethodGet {\n\t\twriteError(response, http.StatusMethodNotAllowed, \"method not allowed\")\n\t\treturn\n\t}\n\tresponse.Header().Set(\"Content-Type\", \"application/octet-stream\")\n\tvar stats DumpStats\n\tstart := time.Now()\n\t// dump users\n\tif err := binary.Write(response, binary.LittleEndian, UserStream); err != nil {\n\t\twriteError(response, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tuserStream, errChan := m.DataClient.GetUserStream(context.Background(), batchSize)\n\tfor users := range userStream {\n\t\tfor _, user := range users {\n\t\t\tlabels, err := json.Marshal(user.Labels)\n\t\t\tif err != nil {\n\t\t\t\twriteError(response, http.StatusInternalServerError, err.Error())\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err := writeDump(response, &protocol.User{\n\t\t\t\tUserId:  user.UserId,\n\t\t\t\tLabels:  labels,\n\t\t\t\tComment: user.Comment,\n\t\t\t}); err != nil {\n\t\t\t\twriteError(response, http.StatusInternalServerError, err.Error())\n\t\t\t\treturn\n\t\t\t}\n\t\t\tstats.Users++\n\t\t}\n\t}\n\tif err := <-errChan; err != nil {\n\t\twriteError(response, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\t// dump items\n\tif err := binary.Write(response, binary.LittleEndian, ItemStream); err != nil {\n\t\twriteError(response, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\titemStream, errChan := m.DataClient.GetItemStream(context.Background(), batchSize, nil)\n\tfor items := range itemStream {\n\t\tfor _, item := range items {\n\t\t\tlabels, err := json.Marshal(item.Labels)\n\t\t\tif err != nil {\n\t\t\t\twriteError(response, http.StatusInternalServerError, err.Error())\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err := writeDump(response, &protocol.Item{\n\t\t\t\tItemId:     item.ItemId,\n\t\t\t\tIsHidden:   item.IsHidden,\n\t\t\t\tCategories: item.Categories,\n\t\t\t\tTimestamp:  timestamppb.New(item.Timestamp),\n\t\t\t\tLabels:     labels,\n\t\t\t\tComment:    item.Comment,\n\t\t\t}); err != nil {\n\t\t\t\twriteError(response, http.StatusInternalServerError, err.Error())\n\t\t\t\treturn\n\t\t\t}\n\t\t\tstats.Items++\n\t\t}\n\t}\n\tif err := <-errChan; err != nil {\n\t\twriteError(response, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\t// dump feedback\n\tif err := binary.Write(response, binary.LittleEndian, FeedbackStream); err != nil {\n\t\twriteError(response, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tfeedbackStream, errChan := m.DataClient.GetFeedbackStream(context.Background(), batchSize, data.WithEndTime(*m.Config.Now()))\n\tfor feedbacks := range feedbackStream {\n\t\tfor _, feedback := range feedbacks {\n\t\t\tif err := writeDump(response, &protocol.Feedback{\n\t\t\t\tFeedbackType: feedback.FeedbackType,\n\t\t\t\tUserId:       feedback.UserId,\n\t\t\t\tItemId:       feedback.ItemId,\n\t\t\t\tValue:        feedback.Value,\n\t\t\t\tTimestamp:    timestamppb.New(feedback.Timestamp),\n\t\t\t\tComment:      feedback.Comment,\n\t\t\t}); err != nil {\n\t\t\t\twriteError(response, http.StatusInternalServerError, err.Error())\n\t\t\t\treturn\n\t\t\t}\n\t\t\tstats.Feedback++\n\t\t}\n\t}\n\tif err := <-errChan; err != nil {\n\t\twriteError(response, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\t// dump EOF\n\tif err := binary.Write(response, binary.LittleEndian, EOF); err != nil {\n\t\twriteError(response, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tstats.Duration = time.Since(start)\n\tlog.Logger().Info(\"complete dump\",\n\t\tzap.Int(\"users\", stats.Users),\n\t\tzap.Int(\"items\", stats.Items),\n\t\tzap.Int(\"feedback\", stats.Feedback),\n\t\tzap.Duration(\"duration\", stats.Duration))\n\tserver.Ok(restful.NewResponse(response), stats)\n}\n\nfunc (m *Master) Restore(r io.ReadCloser) (stats DumpStats, err error) {\n\tflag := EOF\n\tif err = binary.Read(r, binary.LittleEndian, &flag); err != nil {\n\t\treturn\n\t}\n\tfor flag != EOF {\n\t\tswitch flag {\n\t\tcase UserStream:\n\t\t\tusers := make([]data.User, 0, batchSize)\n\t\t\tfor {\n\t\t\t\tvar user protocol.User\n\t\t\t\tif flag, err = readDump(r, &user); err != nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif flag <= 0 {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tvar labels any\n\t\t\t\tif err = json.Unmarshal(user.Labels, &labels); err != nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tusers = append(users, data.User{\n\t\t\t\t\tUserId:  user.UserId,\n\t\t\t\t\tLabels:  labels,\n\t\t\t\t\tComment: user.Comment,\n\t\t\t\t})\n\t\t\t\tstats.Users++\n\t\t\t\tif len(users) == batchSize {\n\t\t\t\t\tif err = m.DataClient.BatchInsertUsers(context.Background(), users); err != nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tusers = users[:0]\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(users) > 0 {\n\t\t\t\tif err = m.DataClient.BatchInsertUsers(context.Background(), users); err != nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\tcase ItemStream:\n\t\t\titems := make([]data.Item, 0, batchSize)\n\t\t\tfor {\n\t\t\t\tvar item protocol.Item\n\t\t\t\tif flag, err = readDump(r, &item); err != nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif flag <= 0 {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tvar labels any\n\t\t\t\tif err = json.Unmarshal(item.Labels, &labels); err != nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\titems = append(items, data.Item{\n\t\t\t\t\tItemId:     item.ItemId,\n\t\t\t\t\tIsHidden:   item.IsHidden,\n\t\t\t\t\tCategories: item.Categories,\n\t\t\t\t\tTimestamp:  item.Timestamp.AsTime(),\n\t\t\t\t\tLabels:     labels,\n\t\t\t\t\tComment:    item.Comment,\n\t\t\t\t})\n\t\t\t\tstats.Items++\n\t\t\t\tif len(items) == batchSize {\n\t\t\t\t\tif err = m.DataClient.BatchInsertItems(context.Background(), items); err != nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\titems = items[:0]\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(items) > 0 {\n\t\t\t\tif err = m.DataClient.BatchInsertItems(context.Background(), items); err != nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\tcase FeedbackStream:\n\t\t\tfeedbacks := make([]data.Feedback, 0, batchSize)\n\t\t\tfor {\n\t\t\t\tvar feedback protocol.Feedback\n\t\t\t\tif flag, err = readDump(r, &feedback); err != nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif flag <= 0 {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tfeedbacks = append(feedbacks, data.Feedback{\n\t\t\t\t\tFeedbackKey: data.FeedbackKey{\n\t\t\t\t\t\tFeedbackType: feedback.FeedbackType,\n\t\t\t\t\t\tUserId:       feedback.UserId,\n\t\t\t\t\t\tItemId:       feedback.ItemId,\n\t\t\t\t\t},\n\t\t\t\t\tValue:     feedback.Value,\n\t\t\t\t\tTimestamp: feedback.Timestamp.AsTime(),\n\t\t\t\t\tComment:   feedback.Comment,\n\t\t\t\t})\n\t\t\t\tstats.Feedback++\n\t\t\t\tif len(feedbacks) == batchSize {\n\t\t\t\t\tif err = m.DataClient.BatchInsertFeedback(context.Background(), feedbacks, false, false, true); err != nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tfeedbacks = feedbacks[:0]\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(feedbacks) > 0 {\n\t\t\t\tif err = m.DataClient.BatchInsertFeedback(context.Background(), feedbacks, false, false, true); err != nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\tdefault:\n\t\t\terr = fmt.Errorf(\"unknown flag %v\", flag)\n\t\t\treturn\n\t\t}\n\t}\n\treturn\n}\n\nfunc (m *Master) restore(response http.ResponseWriter, request *http.Request) {\n\tif !m.checkAdmin(request) {\n\t\twriteError(response, http.StatusUnauthorized, \"unauthorized\")\n\t\treturn\n\t}\n\tif request.Method != http.MethodPost {\n\t\twriteError(response, http.StatusMethodNotAllowed, \"method not allowed\")\n\t\treturn\n\t}\n\tstart := time.Now()\n\tstats, err := m.Restore(request.Body)\n\tif err != nil {\n\t\twriteError(response, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\n\tm.cancel()\n\tselect {\n\tcase m.scheduled <- struct{}{}:\n\tdefault:\n\t}\n\n\tstats.Duration = time.Since(start)\n\tlog.Logger().Info(\"complete restore\",\n\t\tzap.Int(\"users\", stats.Users),\n\t\tzap.Int(\"items\", stats.Items),\n\t\tzap.Int(\"feedback\", stats.Feedback),\n\t\tzap.Duration(\"duration\", stats.Duration))\n\tserver.Ok(restful.NewResponse(response), stats)\n}\n\nfunc (m *Master) handleOAuth2Callback(w http.ResponseWriter, r *http.Request) {\n\t// Verify state and errors.\n\toauth2Token, err := m.oauth2Config.Exchange(r.Context(), r.URL.Query().Get(\"code\"))\n\tif err != nil {\n\t\tserver.InternalServerError(restful.NewResponse(w), err)\n\t\treturn\n\t}\n\t// Extract the ID Token from OAuth2 token.\n\trawIDToken, ok := oauth2Token.Extra(\"id_token\").(string)\n\tif !ok {\n\t\tserver.InternalServerError(restful.NewResponse(w), errors.New(\"missing id_token\"))\n\t\treturn\n\t}\n\t// Parse and verify ID Token payload.\n\tidToken, err := m.verifier.Verify(r.Context(), rawIDToken)\n\tif err != nil {\n\t\tserver.InternalServerError(restful.NewResponse(w), err)\n\t\treturn\n\t}\n\t// Extract custom claims\n\tvar claims UserInfo\n\tif err := idToken.Claims(&claims); err != nil {\n\t\tserver.InternalServerError(restful.NewResponse(w), err)\n\t\treturn\n\t}\n\t// Set token cache and cookie\n\tm.tokenCache.Set(rawIDToken, claims, time.Until(idToken.Expiry))\n\tif encoded, err := cookieHandler.Encode(\"id_token\", rawIDToken); err != nil {\n\t\tserver.InternalServerError(restful.NewResponse(w), err)\n\t\treturn\n\t} else {\n\t\thttp.SetCookie(w, &http.Cookie{\n\t\t\tName:    \"id_token\",\n\t\t\tValue:   encoded,\n\t\t\tPath:    \"/\",\n\t\t\tExpires: idToken.Expiry,\n\t\t})\n\t\thttp.Redirect(w, r, \"/\", http.StatusFound)\n\t\tlog.Logger().Info(\"login success via OIDC\",\n\t\t\tzap.String(\"name\", claims.Name),\n\t\t\tzap.String(\"email\", claims.Email))\n\t}\n}\n\nfunc (m *Master) chat(response http.ResponseWriter, request *http.Request) {\n\tif !m.checkAdmin(request) {\n\t\twriteError(response, http.StatusUnauthorized, \"unauthorized\")\n\t\treturn\n\t}\n\tcontent, err := io.ReadAll(request.Body)\n\tif err != nil {\n\t\twriteError(response, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tstream, err := m.openAIClient.CreateChatCompletionStream(\n\t\trequest.Context(),\n\t\topenai.ChatCompletionRequest{\n\t\t\tModel: m.Config.OpenAI.ChatCompletionModel,\n\t\t\tMessages: []openai.ChatCompletionMessage{\n\t\t\t\t{\n\t\t\t\t\tRole:    openai.ChatMessageRoleUser,\n\t\t\t\t\tContent: string(content),\n\t\t\t\t},\n\t\t\t},\n\t\t\tStream: true,\n\t\t},\n\t)\n\tif err != nil {\n\t\twriteError(response, http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\t// read response\n\tdefer stream.Close()\n\tfor {\n\t\tvar resp openai.ChatCompletionStreamResponse\n\t\tresp, err = stream.Recv()\n\t\tif errors.Is(err, io.EOF) {\n\t\t\treturn\n\t\t}\n\t\tif err != nil {\n\t\t\twriteError(response, http.StatusInternalServerError, err.Error())\n\t\t\treturn\n\t\t}\n\t\tif len(resp.Choices) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif _, err = response.Write([]byte(resp.Choices[0].Delta.Content)); err != nil {\n\t\t\tlog.Logger().Error(\"failed to write response\", zap.Error(err))\n\t\t\treturn\n\t\t}\n\t\t// flush response\n\t\tif f, ok := response.(http.Flusher); ok {\n\t\t\tf.Flush()\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "master/rest_test.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage master\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/emicklei/go-restful/v3\"\n\t\"github.com/go-viper/mapstructure/v2\"\n\t\"github.com/gorse-io/gorse/common/expression\"\n\t\"github.com/gorse-io/gorse/common/mock\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/protocol\"\n\t\"github.com/gorse-io/gorse/server\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/gorse-io/gorse/storage/meta\"\n\t\"github.com/invopop/jsonschema\"\n\t\"github.com/samber/lo\"\n\t\"github.com/sashabaranov/go-openai\"\n\t\"github.com/steinfletcher/apitest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\nconst (\n\tmockMasterUsername = \"admin\"\n\tmockMasterPassword = \"pass\"\n)\n\nfunc marshal(t *testing.T, v interface{}) string {\n\ts, err := json.Marshal(v)\n\tassert.NoError(t, err)\n\treturn string(s)\n}\n\nfunc marshalJSONLines[T any](t *testing.T, v []T) string {\n\tvar buf bytes.Buffer\n\tencoder := json.NewEncoder(&buf)\n\tfor _, item := range v {\n\t\terr := encoder.Encode(item)\n\t\tassert.NoError(t, err)\n\t}\n\treturn buf.String()\n}\n\nfunc convertToMapStructure(t *testing.T, v interface{}) map[string]interface{} {\n\tvar m map[string]interface{}\n\terr := mapstructure.Decode(v, &m)\n\tassert.NoError(t, err)\n\treturn m\n}\n\ntype MasterAPITestSuite struct {\n\tsuite.Suite\n\tMaster\n\thandler      *restful.Container\n\topenAIServer *mock.OpenAIServer\n\tcookie       string\n}\n\nfunc (suite *MasterAPITestSuite) SetupTest() {\n\t// open database\n\tvar err error\n\tsuite.Config = config.GetDefaultConfig()\n\tsuite.Config.Recommend.Ranker.Type = \"fm\"\n\tsuite.Config.Database.DataStore = fmt.Sprintf(\"sqlite://%s/data.db\", suite.T().TempDir())\n\tsuite.Config.Database.CacheStore = fmt.Sprintf(\"sqlite://%s/cache.db\", suite.T().TempDir())\n\tsuite.metaStore, err = meta.Open(fmt.Sprintf(\"sqlite://%s/meta.db\", suite.T().TempDir()), suite.Config.Master.MetaTimeout)\n\tsuite.NoError(err)\n\tsuite.DataClient, err = data.Open(suite.Config.Database.DataStore, \"\")\n\tsuite.NoError(err)\n\tsuite.CacheClient, err = cache.Open(suite.Config.Database.CacheStore, \"\")\n\tsuite.NoError(err)\n\t// init database\n\terr = suite.metaStore.Init()\n\tsuite.NoError(err)\n\terr = suite.DataClient.Init()\n\tsuite.NoError(err)\n\terr = suite.CacheClient.Init()\n\tsuite.NoError(err)\n\t// create server\n\tsuite.Config.Master.DashboardUserName = mockMasterUsername\n\tsuite.Config.Master.DashboardPassword = mockMasterPassword\n\tsuite.WebService = new(restful.WebService)\n\tsuite.CreateWebService()\n\tsuite.RestServer.CreateWebService()\n\tsuite.cancel = func() {}\n\tsuite.scheduled = make(chan struct{}, 1)\n\t// create handler\n\tsuite.handler = restful.NewContainer()\n\tsuite.handler.Add(suite.WebService)\n\t// creat mock AI server\n\tsuite.openAIServer = mock.NewOpenAIServer()\n\tgo func() {\n\t\t_ = suite.openAIServer.Start()\n\t}()\n\tsuite.openAIServer.Ready()\n\tclientConfig := openai.DefaultConfig(suite.openAIServer.AuthToken())\n\tclientConfig.BaseURL = suite.openAIServer.BaseURL()\n\tsuite.openAIClient = openai.NewClientWithConfig(clientConfig)\n\t// login\n\treq, err := http.NewRequest(\"POST\", \"/login\",\n\t\tstrings.NewReader(fmt.Sprintf(\"user_name=%s&password=%s\", mockMasterUsername, mockMasterPassword)))\n\tsuite.NoError(err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tresp := httptest.NewRecorder()\n\tsuite.login(resp, req)\n\tsuite.Equal(http.StatusFound, resp.Code)\n\tsuite.cookie = resp.Header().Get(\"Set-Cookie\")\n}\n\nfunc (suite *MasterAPITestSuite) TearDownTest() {\n\terr := suite.metaStore.Close()\n\tsuite.NoError(err)\n\terr = suite.DataClient.Close()\n\tsuite.NoError(err)\n\terr = suite.CacheClient.Close()\n\tsuite.NoError(err)\n\terr = suite.openAIServer.Close()\n\tsuite.NoError(err)\n}\n\nfunc (suite *MasterAPITestSuite) TestExportUsers() {\n\tctx := suite.T().Context()\n\t// insert users\n\tusers := []data.User{\n\t\t{UserId: \"1\", Labels: map[string]any{\"gender\": \"male\", \"job\": \"engineer\"}},\n\t\t{UserId: \"2\", Labels: map[string]any{\"gender\": \"male\", \"job\": \"lawyer\"}},\n\t\t{UserId: \"3\", Labels: map[string]any{\"gender\": \"female\", \"job\": \"teacher\"}},\n\t}\n\terr := suite.DataClient.BatchInsertUsers(ctx, users)\n\tsuite.NoError(err)\n\t// send request\n\treq := httptest.NewRequest(\"GET\", \"https://example.com/\", nil)\n\treq.Header.Set(\"Cookie\", suite.cookie)\n\tw := httptest.NewRecorder()\n\tsuite.importExportUsers(w, req)\n\tsuite.Equal(http.StatusOK, w.Result().StatusCode)\n\tsuite.Equal(\"application/jsonl\", w.Header().Get(\"Content-Type\"))\n\tsuite.Equal(\"attachment;filename=users.jsonl\", w.Header().Get(\"Content-Disposition\"))\n\tsuite.Equal(marshalJSONLines(suite.T(), users), w.Body.String())\n}\n\nfunc (suite *MasterAPITestSuite) TestExportItems() {\n\tctx := suite.T().Context()\n\t// insert items\n\titems := []data.Item{\n\t\t{\n\t\t\tItemId:     \"1\",\n\t\t\tIsHidden:   false,\n\t\t\tCategories: []string{\"x\"},\n\t\t\tTimestamp:  time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC),\n\t\t\tLabels:     map[string]any{\"genre\": []string{\"comedy\", \"sci-fi\"}},\n\t\t\tComment:    \"o,n,e\",\n\t\t},\n\t\t{\n\t\t\tItemId:     \"2\",\n\t\t\tIsHidden:   false,\n\t\t\tCategories: []string{\"x\", \"y\"},\n\t\t\tTimestamp:  time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC),\n\t\t\tLabels:     map[string]any{\"genre\": []string{\"documentary\", \"sci-fi\"}},\n\t\t\tComment:    \"t\\r\\nw\\r\\no\",\n\t\t},\n\t\t{\n\t\t\tItemId:     \"3\",\n\t\t\tIsHidden:   true,\n\t\t\tCategories: nil,\n\t\t\tTimestamp:  time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),\n\t\t\tLabels:     nil,\n\t\t\tComment:    \"\\\"three\\\"\",\n\t\t},\n\t}\n\terr := suite.DataClient.BatchInsertItems(ctx, items)\n\tsuite.NoError(err)\n\t// send request\n\treq := httptest.NewRequest(\"GET\", \"https://example.com/\", nil)\n\treq.Header.Set(\"Cookie\", suite.cookie)\n\tw := httptest.NewRecorder()\n\tsuite.importExportItems(w, req)\n\tsuite.Equal(http.StatusOK, w.Result().StatusCode)\n\tsuite.Equal(\"application/jsonl\", w.Header().Get(\"Content-Type\"))\n\tsuite.Equal(\"attachment;filename=items.jsonl\", w.Header().Get(\"Content-Disposition\"))\n\tsuite.Equal(marshalJSONLines(suite.T(), items), w.Body.String())\n}\n\nfunc (suite *MasterAPITestSuite) TestExportFeedback() {\n\tctx := suite.T().Context()\n\t// insert feedback\n\tfeedbacks := []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"click\", UserId: \"0\", ItemId: \"2\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"read\", UserId: \"2\", ItemId: \"6\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"share\", UserId: \"1\", ItemId: \"4\"}},\n\t}\n\terr := suite.DataClient.BatchInsertFeedback(ctx, feedbacks, true, true, true)\n\tsuite.NoError(err)\n\t// send request\n\treq := httptest.NewRequest(\"GET\", \"https://example.com/\", nil)\n\treq.Header.Set(\"Cookie\", suite.cookie)\n\tw := httptest.NewRecorder()\n\tsuite.importExportFeedback(w, req)\n\tsuite.Equal(http.StatusOK, w.Result().StatusCode)\n\tsuite.Equal(\"application/jsonl\", w.Header().Get(\"Content-Type\"))\n\tsuite.Equal(\"attachment;filename=feedback.jsonl\", w.Header().Get(\"Content-Disposition\"))\n\tsuite.Equal(marshalJSONLines(suite.T(), feedbacks), w.Body.String())\n}\n\nfunc (suite *MasterAPITestSuite) TestImportUsers() {\n\tctx := suite.T().Context()\n\t// send request\n\tbuf := bytes.NewBuffer(nil)\n\twriter := multipart.NewWriter(buf)\n\tfile, err := writer.CreateFormFile(\"file\", \"users.jsonl\")\n\tsuite.NoError(err)\n\t_, err = file.Write([]byte(`{\"UserId\":\"1\",\"Labels\":{\"性别\":\"男\",\"职业\":\"工程师\"}}\n{\"UserId\":\"2\",\"Labels\":{\"性别\":\"男\",\"职业\":\"律师\"}}\n{\"UserId\":\"3\",\"Labels\":{\"性别\":\"女\",\"职业\":\"教师\"}}`))\n\tsuite.NoError(err)\n\terr = writer.Close()\n\tsuite.NoError(err)\n\treq := httptest.NewRequest(\"POST\", \"https://example.com/\", buf)\n\treq.Header.Set(\"Cookie\", suite.cookie)\n\treq.Header.Set(\"Content-Type\", writer.FormDataContentType())\n\tw := httptest.NewRecorder()\n\tsuite.importExportUsers(w, req)\n\t// check\n\tsuite.Equal(http.StatusOK, w.Result().StatusCode)\n\tsuite.JSONEq(marshal(suite.T(), server.Success{RowAffected: 3}), w.Body.String())\n\t_, items, err := suite.DataClient.GetUsers(ctx, \"\", 100)\n\tsuite.NoError(err)\n\tsuite.Equal([]data.User{\n\t\t{UserId: \"1\", Labels: map[string]any{\"性别\": \"男\", \"职业\": \"工程师\"}},\n\t\t{UserId: \"2\", Labels: map[string]any{\"性别\": \"男\", \"职业\": \"律师\"}},\n\t\t{UserId: \"3\", Labels: map[string]any{\"性别\": \"女\", \"职业\": \"教师\"}},\n\t}, items)\n\tsuite.NotEmpty(suite.scheduled)\n}\n\nfunc (suite *MasterAPITestSuite) TestImportItems() {\n\tctx := suite.T().Context()\n\t// send request\n\tbuf := bytes.NewBuffer(nil)\n\twriter := multipart.NewWriter(buf)\n\tfile, err := writer.CreateFormFile(\"file\", \"items.jsonl\")\n\tsuite.NoError(err)\n\t_, err = file.Write([]byte(`{\"ItemId\":\"1\",\"IsHidden\":false,\"Categories\":[\"x\"],\"Timestamp\":\"2020-01-01 01:01:01.000000001 +0000 UTC\",\"Labels\":{\"类型\":[\"喜剧\",\"科幻\"]},\"Comment\":\"one\"}\n{\"ItemId\":\"2\",\"IsHidden\":false,\"Categories\":[\"x\",\"y\"],\"Timestamp\":\"2021-01-01 01:01:01.000000001 +0000 UTC\",\"Labels\":{\"类型\":[\"卡通\",\"科幻\"]},\"Comment\":\"two\"}\n{\"ItemId\":\"3\",\"IsHidden\":true,\"Timestamp\":\"2022-01-01 01:01:01.000000001 +0000 UTC\",\"Comment\":\"three\"}`))\n\tsuite.NoError(err)\n\terr = writer.Close()\n\tsuite.NoError(err)\n\treq := httptest.NewRequest(\"POST\", \"https://example.com/\", buf)\n\treq.Header.Set(\"Cookie\", suite.cookie)\n\treq.Header.Set(\"Content-Type\", writer.FormDataContentType())\n\tw := httptest.NewRecorder()\n\tsuite.importExportItems(w, req)\n\t// check\n\tsuite.Equal(http.StatusOK, w.Result().StatusCode)\n\tsuite.JSONEq(marshal(suite.T(), server.Success{RowAffected: 3}), w.Body.String())\n\t_, items, err := suite.DataClient.GetItems(ctx, \"\", 100, nil)\n\tsuite.NoError(err)\n\tsuite.Equal([]data.Item{\n\t\t{\n\t\t\tItemId:     \"1\",\n\t\t\tIsHidden:   false,\n\t\t\tCategories: []string{\"x\"},\n\t\t\tTimestamp:  time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC),\n\t\t\tLabels:     map[string]any{\"类型\": []any{\"喜剧\", \"科幻\"}},\n\t\t\tComment:    \"one\"},\n\t\t{\n\t\t\tItemId:     \"2\",\n\t\t\tIsHidden:   false,\n\t\t\tCategories: []string{\"x\", \"y\"},\n\t\t\tTimestamp:  time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC),\n\t\t\tLabels:     map[string]any{\"类型\": []any{\"卡通\", \"科幻\"}},\n\t\t\tComment:    \"two\",\n\t\t},\n\t\t{\n\t\t\tItemId:     \"3\",\n\t\t\tIsHidden:   true,\n\t\t\tCategories: nil,\n\t\t\tTimestamp:  time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),\n\t\t\tLabels:     nil,\n\t\t\tComment:    \"three\",\n\t\t},\n\t}, items)\n\tsuite.NotEmpty(suite.scheduled)\n}\n\nfunc (suite *MasterAPITestSuite) TestImportFeedback() {\n\t// send request\n\tctx := suite.T().Context()\n\tbuf := bytes.NewBuffer(nil)\n\twriter := multipart.NewWriter(buf)\n\tfile, err := writer.CreateFormFile(\"file\", \"feedback.jsonl\")\n\tsuite.NoError(err)\n\t_, err = file.Write([]byte(`{\"FeedbackType\":\"click\",\"UserId\":\"0\",\"ItemId\":\"2\",\"Timestamp\":\"0001-01-01 00:00:00 +0000 UTC\"}\n{\"FeedbackType\":\"read\",\"UserId\":\"2\",\"ItemId\":\"6\",\"Timestamp\":\"0001-01-01 00:00:00 +0000 UTC\"}\n{\"FeedbackType\":\"share\",\"UserId\":\"1\",\"ItemId\":\"4\",\"Timestamp\":\"0001-01-01 00:00:00 +0000 UTC\"}`))\n\tsuite.NoError(err)\n\terr = writer.Close()\n\tsuite.NoError(err)\n\treq := httptest.NewRequest(\"POST\", \"https://example.com/\", buf)\n\treq.Header.Set(\"Cookie\", suite.cookie)\n\treq.Header.Set(\"Content-Type\", writer.FormDataContentType())\n\tw := httptest.NewRecorder()\n\tsuite.importExportFeedback(w, req)\n\t// check\n\tsuite.Equal(http.StatusOK, w.Result().StatusCode)\n\tsuite.JSONEq(marshal(suite.T(), server.Success{RowAffected: 3}), w.Body.String())\n\t_, feedback, err := suite.DataClient.GetFeedback(ctx, \"\", 100, nil, lo.ToPtr(time.Now()))\n\tsuite.NoError(err)\n\tsuite.Equal([]data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"click\", UserId: \"0\", ItemId: \"2\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"read\", UserId: \"2\", ItemId: \"6\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"share\", UserId: \"1\", ItemId: \"4\"}},\n\t}, feedback)\n\tsuite.NotEmpty(suite.scheduled)\n}\n\nfunc (suite *MasterAPITestSuite) TestGetCluster() {\n\t// add nodes\n\tserverNode := &meta.Node{\n\t\tUUID:       \"alan turnin\",\n\t\tHostname:   \"192.168.1.100\",\n\t\tType:       protocol.NodeType_Server.String(),\n\t\tVersion:    \"server_version\",\n\t\tUpdateTime: time.Now().UTC(),\n\t}\n\tworkerNode := &meta.Node{\n\t\tUUID:       \"dennis ritchie\",\n\t\tHostname:   \"192.168.1.101\",\n\t\tType:       protocol.NodeType_Worker.String(),\n\t\tVersion:    \"worker_version\",\n\t\tUpdateTime: time.Now().UTC(),\n\t}\n\terr := suite.metaStore.UpdateNode(serverNode)\n\tsuite.NoError(err)\n\terr = suite.metaStore.UpdateNode(workerNode)\n\tsuite.NoError(err)\n\t// get nodes\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/dashboard/cluster\").\n\t\tHeader(\"Cookie\", suite.cookie).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(marshal(suite.T(), []*meta.Node{serverNode, workerNode})).\n\t\tEnd()\n}\n\nfunc (suite *MasterAPITestSuite) TestGetStats() {\n\tctx := suite.T().Context()\n\t// set stats\n\terr := suite.CacheClient.Set(ctx, cache.Integer(cache.Key(cache.GlobalMeta, cache.NumUsers), 123))\n\tsuite.NoError(err)\n\terr = suite.CacheClient.Set(ctx, cache.Integer(cache.Key(cache.GlobalMeta, cache.NumItems), 234))\n\tsuite.NoError(err)\n\terr = suite.CacheClient.Set(ctx, cache.Integer(cache.Key(cache.GlobalMeta, cache.NumValidPosFeedbacks), 345))\n\tsuite.NoError(err)\n\terr = suite.CacheClient.Set(ctx, cache.Integer(cache.Key(cache.GlobalMeta, cache.NumValidNegFeedbacks), 456))\n\tsuite.NoError(err)\n\t// get stats\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/dashboard/stats\").\n\t\tHeader(\"Cookie\", suite.cookie).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(marshal(suite.T(), Status{\n\t\t\tNumUsers:            123,\n\t\t\tNumItems:            234,\n\t\t\tNumValidPosFeedback: 345,\n\t\t\tNumValidNegFeedback: 456,\n\t\t\tBinaryVersion:       \"unknown-version\",\n\t\t})).\n\t\tEnd()\n}\n\nfunc (suite *MasterAPITestSuite) TestGetCategories() {\n\tctx := suite.T().Context()\n\t// insert categories\n\tcategoryScores := []cache.Score{\n\t\t{Id: \"a\", Score: 3},\n\t\t{Id: \"b\", Score: 2},\n\t\t{Id: \"c\", Score: 1},\n\t}\n\terr := suite.CacheClient.AddScores(ctx, cache.ItemCategories, \"\", categoryScores)\n\tsuite.NoError(err)\n\t// get categories\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/dashboard/categories\").\n\t\tHeader(\"Cookie\", suite.cookie).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(marshal(suite.T(), []string{\"a\", \"b\", \"c\"})).\n\t\tEnd()\n}\n\nfunc (suite *MasterAPITestSuite) TestGetUsers() {\n\tctx := suite.T().Context()\n\t// add users\n\tusers := []User{\n\t\t{data.User{UserId: \"0\"}, time.Date(2000, 1, 1, 1, 1, 1, 1, time.UTC), time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t\t{data.User{UserId: \"1\"}, time.Date(2001, 1, 1, 1, 1, 1, 1, time.UTC), time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t\t{data.User{UserId: \"2\"}, time.Date(2002, 1, 1, 1, 1, 1, 1, time.UTC), time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t}\n\tfor _, user := range users {\n\t\terr := suite.DataClient.BatchInsertUsers(ctx, []data.User{user.User})\n\t\tsuite.NoError(err)\n\t\terr = suite.CacheClient.Set(ctx, cache.Time(cache.Key(cache.LastModifyUserTime, user.UserId), user.LastActiveTime))\n\t\tsuite.NoError(err)\n\t\terr = suite.CacheClient.Set(ctx, cache.Time(cache.Key(cache.RecommendUpdateTime, user.UserId), user.LastUpdateTime))\n\t\tsuite.NoError(err)\n\t}\n\t// get users\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/dashboard/users\").\n\t\tHeader(\"Cookie\", suite.cookie).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(marshal(suite.T(), UserIterator{\n\t\t\tCursor: \"\",\n\t\t\tUsers:  users,\n\t\t})).\n\t\tEnd()\n\t// get a user\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/dashboard/user/1\").\n\t\tHeader(\"Cookie\", suite.cookie).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(marshal(suite.T(), users[1])).\n\t\tEnd()\n}\n\nfunc (suite *MasterAPITestSuite) TestGetLatestItems() {\n\tctx := suite.T().Context()\n\t// add items\n\titems := []data.Item{\n\t\t{ItemId: \"0\", Timestamp: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t\t{ItemId: \"1\", Timestamp: time.Date(2024, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t\t{ItemId: \"2\", Timestamp: time.Date(2023, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t}\n\terr := suite.DataClient.BatchInsertItems(ctx, items)\n\tsuite.NoError(err)\n\t// get latest items\n\tscores := lo.Map(items, func(item data.Item, _ int) ScoredItem {\n\t\treturn ScoredItem{Item: item, Score: float64(item.Timestamp.Unix())}\n\t})\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/dashboard/latest\").\n\t\tHeader(\"Cookie\", suite.cookie).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(marshal(suite.T(), scores)).\n\t\tEnd()\n}\n\nfunc (suite *MasterAPITestSuite) TestSearchDocumentsOfItems() {\n\ttype ListOperator struct {\n\t\tName       string\n\t\tCollection string\n\t\tSubset     string\n\t\tCategory   string\n\t\tGet        string\n\t}\n\tctx := suite.T().Context()\n\toperators := []ListOperator{\n\t\t{\"ItemToItem\", cache.ItemToItem, cache.Key(\"neighbors\", \"0\"), \"\", \"/api/dashboard/item-to-item/neighbors/0\"},\n\t\t{\"ItemToItemCategory\", cache.ItemToItem, cache.Key(\"neighbors\", \"0\"), \"*\", \"/api/dashboard/item-to-item/neighbors/0\"},\n\t\t{\"LatestItems\", cache.NonPersonalized, \"latest\", \"\", \"/api/dashboard/non-personalized/latest/\"},\n\t\t{\"PopularItems\", cache.NonPersonalized, \"popular\", \"\", \"/api/dashboard/non-personalized/popular/\"},\n\t\t{\"LatestItemsCategory\", cache.NonPersonalized, \"latest\", \"*\", \"/api/dashboard/non-personalized/latest/\"},\n\t\t{\"PopularItemsCategory\", cache.NonPersonalized, \"popular\", \"*\", \"/api/dashboard/non-personalized/popular/\"},\n\t}\n\tlastModified := time.Now()\n\tfor i, operator := range operators {\n\t\tsuite.T().Run(operator.Name, func(t *testing.T) {\n\t\t\t// Put scores\n\t\t\tscores := []cache.Score{\n\t\t\t\t{Id: strconv.Itoa(i) + \"0\", Score: 100, Categories: []string{operator.Category}},\n\t\t\t\t{Id: strconv.Itoa(i) + \"1\", Score: 99, Categories: []string{operator.Category}},\n\t\t\t\t{Id: strconv.Itoa(i) + \"2\", Score: 98, Categories: []string{operator.Category}},\n\t\t\t\t{Id: strconv.Itoa(i) + \"3\", Score: 97, Categories: []string{operator.Category}},\n\t\t\t\t{Id: strconv.Itoa(i) + \"4\", Score: 96, Categories: []string{operator.Category}},\n\t\t\t}\n\t\t\terr := suite.CacheClient.AddScores(ctx, operator.Collection, operator.Subset, scores)\n\t\t\tsuite.NoError(err)\n\t\t\terr = suite.CacheClient.Set(ctx, cache.Time(cache.Key(operator.Collection+\"_update_time\", operator.Subset), lastModified))\n\t\t\tassert.NoError(t, err)\n\t\t\titems := make([]ScoredItem, 0)\n\t\t\tfor _, score := range scores {\n\t\t\t\titems = append(items, ScoredItem{Item: data.Item{ItemId: score.Id}, Score: score.Score})\n\t\t\t\terr = suite.DataClient.BatchInsertItems(ctx, []data.Item{{ItemId: score.Id}})\n\t\t\t\tsuite.NoError(err)\n\t\t\t}\n\t\t\t// hide item\n\t\t\tapitest.New().\n\t\t\t\tHandler(suite.handler).\n\t\t\t\tPatch(\"/api/item/\"+strconv.Itoa(i)+\"3\").\n\t\t\t\tHeader(\"Cookie\", suite.cookie).\n\t\t\t\tJSON(data.ItemPatch{IsHidden: new(true)}).\n\t\t\t\tExpect(t).\n\t\t\t\tStatus(http.StatusOK).\n\t\t\t\tEnd()\n\t\t\tapitest.New().\n\t\t\t\tHandler(suite.handler).\n\t\t\t\tGet(operator.Get).\n\t\t\t\tHeader(\"Cookie\", suite.cookie).\n\t\t\t\tQuery(\"category\", operator.Category).\n\t\t\t\tExpect(t).\n\t\t\t\tStatus(http.StatusOK).\n\t\t\t\tHeaderPresent(\"Last-Modified\").\n\t\t\t\tBody(marshal(t, []ScoredItem{items[0], items[1], items[2], items[4]})).\n\t\t\t\tEnd()\n\t\t})\n\t}\n}\n\nfunc (suite *MasterAPITestSuite) TestSearchDocumentsOfUsers() {\n\ttype ListOperator struct {\n\t\tPrefix string\n\t\tLabel  string\n\t\tGet    string\n\t}\n\tctx := suite.T().Context()\n\toperators := []ListOperator{\n\t\t{cache.UserToUser, cache.Key(\"neighbors\", \"0\"), \"/api/dashboard/user-to-user/neighbors/0/\"},\n\t}\n\tlastModified := time.Now()\n\tfor _, operator := range operators {\n\t\tsuite.T().Logf(\"test RESTful API: %v\", operator.Get)\n\t\t// Put scores\n\t\tscores := []cache.Score{\n\t\t\t{Id: \"0\", Score: 100, Categories: []string{\"\"}},\n\t\t\t{Id: \"1\", Score: 99, Categories: []string{\"\"}},\n\t\t\t{Id: \"2\", Score: 98, Categories: []string{\"\"}},\n\t\t\t{Id: \"3\", Score: 97, Categories: []string{\"\"}},\n\t\t\t{Id: \"4\", Score: 96, Categories: []string{\"\"}},\n\t\t}\n\t\terr := suite.CacheClient.AddScores(ctx, operator.Prefix, operator.Label, scores)\n\t\tsuite.NoError(err)\n\t\terr = suite.CacheClient.Set(ctx, cache.Time(cache.Key(operator.Prefix+\"_update_time\", operator.Label), lastModified))\n\t\tsuite.NoError(err)\n\t\tusers := make([]ScoreUser, 0)\n\t\tfor _, score := range scores {\n\t\t\tusers = append(users, ScoreUser{User: data.User{UserId: score.Id}, Score: score.Score})\n\t\t\terr = suite.DataClient.BatchInsertUsers(ctx, []data.User{{UserId: score.Id}})\n\t\t\tsuite.NoError(err)\n\t\t}\n\t\tapitest.New().\n\t\t\tHandler(suite.handler).\n\t\t\tGet(operator.Get).\n\t\t\tHeader(\"Cookie\", suite.cookie).\n\t\t\tExpect(suite.T()).\n\t\t\tStatus(http.StatusOK).\n\t\t\tHeaderPresent(\"Last-Modified\").\n\t\t\tBody(marshal(suite.T(), users)).\n\t\t\tEnd()\n\t}\n}\n\nfunc (suite *MasterAPITestSuite) TestFeedback() {\n\tctx := suite.T().Context()\n\t// insert feedback\n\tfeedback := []DetailedFeedback{\n\t\t{FeedbackType: \"click\", UserId: \"0\", Item: data.Item{ItemId: \"0\"}},\n\t\t{FeedbackType: \"click\", UserId: \"0\", Item: data.Item{ItemId: \"2\"}},\n\t\t{FeedbackType: \"click\", UserId: \"0\", Item: data.Item{ItemId: \"4\"}},\n\t\t{FeedbackType: \"click\", UserId: \"0\", Item: data.Item{ItemId: \"6\"}},\n\t\t{FeedbackType: \"click\", UserId: \"0\", Item: data.Item{ItemId: \"8\"}},\n\t}\n\tfor _, v := range feedback {\n\t\terr := suite.DataClient.BatchInsertFeedback(ctx, []data.Feedback{{\n\t\t\tFeedbackKey: data.FeedbackKey{FeedbackType: v.FeedbackType, UserId: v.UserId, ItemId: v.Item.ItemId},\n\t\t}}, true, true, true)\n\t\tsuite.NoError(err)\n\t}\n\t// get feedback\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/dashboard/user/0/feedback/click\").\n\t\tHeader(\"Cookie\", suite.cookie).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(marshal(suite.T(), feedback)).\n\t\tEnd()\n}\n\nfunc (suite *MasterAPITestSuite) TestGetRecommends() {\n\t// inset recommendation\n\titemIds := []cache.Score{\n\t\t{Id: \"1\", Score: 99, Categories: []string{\"\"}},\n\t\t{Id: \"2\", Score: 98, Categories: []string{\"\"}},\n\t\t{Id: \"3\", Score: 97, Categories: []string{\"\"}},\n\t\t{Id: \"4\", Score: 96, Categories: []string{\"\"}},\n\t\t{Id: \"5\", Score: 95, Categories: []string{\"\"}},\n\t\t{Id: \"6\", Score: 94, Categories: []string{\"\"}},\n\t\t{Id: \"7\", Score: 93, Categories: []string{\"\"}},\n\t\t{Id: \"8\", Score: 92, Categories: []string{\"\"}},\n\t}\n\tctx := suite.T().Context()\n\terr := suite.CacheClient.AddScores(ctx, cache.Recommend, \"0\", itemIds)\n\tsuite.NoError(err)\n\t// insert feedback\n\tfeedback := []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"2\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"4\"}},\n\t}\n\terr = suite.DataClient.BatchInsertFeedback(ctx, feedback, true, true, true)\n\tsuite.NoError(err)\n\t// insert items\n\tfor _, item := range itemIds {\n\t\terr = suite.DataClient.BatchInsertItems(ctx, []data.Item{{ItemId: item.Id}})\n\t\tsuite.NoError(err)\n\t}\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/dashboard/recommend/0\").\n\t\tHeader(\"Cookie\", suite.cookie).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(marshal(suite.T(), []ScoredItem{\n\t\t\t{data.Item{ItemId: \"1\"}, 99},\n\t\t\t{data.Item{ItemId: \"3\"}, 97},\n\t\t\t{data.Item{ItemId: \"5\"}, 95},\n\t\t\t{data.Item{ItemId: \"6\"}, 94},\n\t\t\t{data.Item{ItemId: \"7\"}, 93},\n\t\t\t{data.Item{ItemId: \"8\"}, 92},\n\t\t})).\n\t\tEnd()\n}\n\nfunc (suite *MasterAPITestSuite) TestGetNonPersonalizedRecommends() {\n\tctx := suite.T().Context()\n\t// insert offline recommendation\n\terr := suite.CacheClient.AddScores(ctx, cache.Recommend, \"0\", []cache.Score{\n\t\t{Id: \"1\", Score: 99, Categories: []string{\"\"}},\n\t\t{Id: \"2\", Score: 98, Categories: []string{\"\"}},\n\t\t{Id: \"3\", Score: 97, Categories: []string{\"\"}},\n\t})\n\tsuite.NoError(err)\n\t// insert non-personalized latest recommendation\n\terr = suite.CacheClient.AddScores(ctx, cache.NonPersonalized, \"popular\", []cache.Score{\n\t\t{Id: \"10\", Score: 100, Categories: []string{\"\"}},\n\t\t{Id: \"20\", Score: 99, Categories: []string{\"\"}},\n\t\t{Id: \"30\", Score: 98, Categories: []string{\"\"}},\n\t})\n\tsuite.NoError(err)\n\t// insert items\n\terr = suite.DataClient.BatchInsertItems(ctx, []data.Item{\n\t\t{ItemId: \"1\", Timestamp: time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t\t{ItemId: \"2\", Timestamp: time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t\t{ItemId: \"3\", Timestamp: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t\t{ItemId: \"10\", Timestamp: time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t\t{ItemId: \"20\", Timestamp: time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t\t{ItemId: \"30\", Timestamp: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t})\n\tsuite.NoError(err)\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/dashboard/recommend/0/non-personalized/popular\").\n\t\tHeader(\"Cookie\", suite.cookie).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(marshal(suite.T(), []ScoredItem{\n\t\t\t{data.Item{ItemId: \"10\", Timestamp: time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC)}, 100},\n\t\t\t{data.Item{ItemId: \"20\", Timestamp: time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC)}, 99},\n\t\t\t{data.Item{ItemId: \"30\", Timestamp: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC)}, 98},\n\t\t})).\n\t\tEnd()\n}\n\nfunc (suite *MasterAPITestSuite) TestGetExternal() {\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Query().Get(\"user_id\") == \"1\" {\n\t\t\tfmt.Fprintln(w, `[\"x\",\"y\"]`)\n\t\t\treturn\n\t\t}\n\t\thttp.NotFound(w, r)\n\t}))\n\tdefer ts.Close()\n\n\tscript := fmt.Sprintf(`fetch(\"%s?user_id=\" + user_id).body`, ts.URL)\n\tscriptBase64 := base64.StdEncoding.EncodeToString([]byte(script))\n\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/dashboard/external\").\n\t\tHeader(\"Cookie\", suite.cookie).\n\t\tQuery(\"script\", scriptBase64).\n\t\tQuery(\"user-id\", \"1\").\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(marshal(suite.T(), []string{\"x\", \"y\"})).\n\t\tEnd()\n}\n\nfunc (suite *MasterAPITestSuite) TestPurge() {\n\tctx := suite.T().Context()\n\t// insert data\n\terr := suite.CacheClient.Set(ctx, cache.String(\"key\", \"value\"))\n\tsuite.NoError(err)\n\tret, err := suite.CacheClient.Get(ctx, \"key\").String()\n\tsuite.NoError(err)\n\tsuite.Equal(\"value\", ret)\n\n\terr = suite.CacheClient.AddScores(ctx, \"sorted\", \"\", []cache.Score{\n\t\t{Id: \"a\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"b\", Score: 2, Categories: []string{\"\"}},\n\t\t{Id: \"c\", Score: 3, Categories: []string{\"\"}}})\n\tsuite.NoError(err)\n\tz, err := suite.CacheClient.SearchScores(ctx, \"sorted\", \"\", []string{\"\"}, 0, -1)\n\tsuite.NoError(err)\n\tsuite.ElementsMatch([]cache.Score{\n\t\t{Id: \"a\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"b\", Score: 2, Categories: []string{\"\"}},\n\t\t{Id: \"c\", Score: 3, Categories: []string{\"\"}}}, z)\n\n\terr = suite.DataClient.BatchInsertFeedback(ctx, lo.Map(lo.Range(100), func(t int, i int) data.Feedback {\n\t\treturn data.Feedback{FeedbackKey: data.FeedbackKey{\n\t\t\tFeedbackType: \"click\",\n\t\t\tUserId:       strconv.Itoa(t),\n\t\t\tItemId:       strconv.Itoa(t),\n\t\t}}\n\t}), true, true, true)\n\tsuite.NoError(err)\n\t_, users, err := suite.DataClient.GetUsers(ctx, \"\", 100)\n\tsuite.NoError(err)\n\tsuite.Equal(100, len(users))\n\t_, items, err := suite.DataClient.GetItems(ctx, \"\", 100, nil)\n\tsuite.NoError(err)\n\tsuite.Equal(100, len(items))\n\t_, feedbacks, err := suite.DataClient.GetFeedback(ctx, \"\", 100, nil, lo.ToPtr(time.Now()))\n\tsuite.NoError(err)\n\tsuite.Equal(100, len(feedbacks))\n\n\t// purge data\n\treq := httptest.NewRequest(\"POST\", \"https://example.com/\",\n\t\tstrings.NewReader(\"check_list=delete_users,delete_items,delete_feedback,delete_cache\"))\n\treq.Header.Set(\"Cookie\", suite.cookie)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tw := httptest.NewRecorder()\n\tsuite.purge(w, req)\n\tsuite.Equal(http.StatusOK, w.Code)\n\n\tv := suite.CacheClient.Get(ctx, \"key\")\n\tsuite.False(v.Exists())\n\tz, err = suite.CacheClient.SearchScores(ctx, \"sorted\", \"\", []string{\"\"}, 0, -1)\n\tsuite.NoError(err)\n\tsuite.Empty(z)\n\n\t_, users, err = suite.DataClient.GetUsers(ctx, \"\", 100)\n\tsuite.NoError(err)\n\tsuite.Empty(users)\n\t_, items, err = suite.DataClient.GetItems(ctx, \"\", 100, nil)\n\tsuite.NoError(err)\n\tsuite.Empty(items)\n\t_, feedbacks, err = suite.DataClient.GetFeedback(ctx, \"\", 100, nil, lo.ToPtr(time.Now()))\n\tsuite.NoError(err)\n\tsuite.Empty(feedbacks)\n}\n\nfunc (suite *MasterAPITestSuite) TestConfig() {\n\tsuite.Config.Recommend.DataSource.PositiveFeedbackTypes = []expression.FeedbackTypeExpression{\n\t\texpression.MustParseFeedbackTypeExpression(\"a\")}\n\tsuite.Config.Recommend.DataSource.ReadFeedbackTypes = []expression.FeedbackTypeExpression{\n\t\texpression.MustParseFeedbackTypeExpression(\"b\")}\n\tsuite.Config.Recommend.Ranker.Recommenders = []string{\"latest\"}\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/dashboard/config\").\n\t\tHeader(\"Cookie\", suite.cookie).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(marshal(suite.T(), formatConfig(convertToMapStructure(suite.T(), suite.Config)))).\n\t\tEnd()\n\n\tsuite.Config.Master.DashboardRedacted = true\n\tredactedConfig := formatConfig(convertToMapStructure(suite.T(), suite.Config))\n\tdelete(redactedConfig, \"database\")\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/dashboard/config\").\n\t\tHeader(\"Cookie\", suite.cookie).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(marshal(suite.T(), redactedConfig)).\n\t\tEnd()\n\n\tsuite.Config.Master.DashboardRedacted = false\n\tnewConfig := *suite.Config\n\tnewConfig.Recommend.Ranker.Type = \"llm\"\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/dashboard/config\").\n\t\tHeader(\"Cookie\", suite.cookie).\n\t\tHeader(\"Content-Type\", \"application/json\").\n\t\tBody(marshal(suite.T(), formatConfig(convertToMapStructure(suite.T(), newConfig)))).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\tsuite.Equal(\"llm\", suite.Config.Recommend.Ranker.Type)\n\tsuite.NotEmpty(suite.scheduled)\n\n\tnewConfig.Recommend.Ranker.Type = \"xxx\"\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/dashboard/config\").\n\t\tHeader(\"Cookie\", suite.cookie).\n\t\tJSON(newConfig).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusBadRequest).\n\t\tEnd()\n\tsuite.Equal(\"llm\", suite.Config.Recommend.Ranker.Type)\n\n\tconfigBytes, err := json.Marshal(suite.Config.Recommend)\n\tsuite.NoError(err)\n\terr = suite.metaStore.Put(meta.RECOMMEND_CONFIG, string(configBytes))\n\tsuite.NoError(err)\n\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tDelete(\"/api/dashboard/config\").\n\t\tHeader(\"Cookie\", suite.cookie).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\n\tvalue, err := suite.metaStore.Get(meta.RECOMMEND_CONFIG)\n\tsuite.NoError(err)\n\tsuite.Nil(value)\n}\n\nfunc (suite *MasterAPITestSuite) TestGetConfigSchema() {\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/dashboard/config/schema\").\n\t\tHeader(\"Cookie\", suite.cookie).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(marshal(suite.T(), jsonschema.Reflect(suite.Config))).\n\t\tEnd()\n}\n\nfunc (suite *MasterAPITestSuite) TestGetTimeseries() {\n\tctx := suite.T().Context()\n\terr := suite.CacheClient.AddTimeSeriesPoints(ctx, []cache.TimeSeriesPoint{\n\t\t{Name: \"test_timeseries\", Timestamp: time.Now().Add(-24 * time.Hour), Value: 1},\n\t\t{Name: \"test_timeseries\", Timestamp: time.Now().Add(-48 * time.Hour), Value: 2},\n\t\t{Name: \"test_timeseries\", Timestamp: time.Now().Add(-72 * time.Hour), Value: 3},\n\t})\n\tsuite.NoError(err)\n\n\treq := httptest.NewRequest(\"GET\", \"/api/dashboard/timeseries/test_timeseries\", nil)\n\treq.Header.Set(\"Cookie\", suite.cookie)\n\tw := httptest.NewRecorder()\n\tsuite.handler.ServeHTTP(w, req)\n\tsuite.Equal(http.StatusOK, w.Code, w.Body.String())\n\tvar got []cache.TimeSeriesPoint\n\terr = json.Unmarshal(w.Body.Bytes(), &got)\n\tsuite.NoError(err)\n\tsuite.Len(got, 3)\n}\n\nfunc (suite *MasterAPITestSuite) TestGetRankerPrompt() {\n\tctx := suite.T().Context()\n\t// insert user\n\tuser := data.User{UserId: \"u1\"}\n\terr := suite.DataClient.BatchInsertUsers(ctx, []data.User{user})\n\tsuite.NoError(err)\n\n\t// insert items for feedback (hidden items)\n\tfeedbackItems := make([]data.Item, 12)\n\tfeedbacks := make([]data.Feedback, 12)\n\tfor i := 0; i < 12; i++ {\n\t\titemId := fmt.Sprintf(\"fb-%02d\", i)\n\t\tfeedbackItems[i] = data.Item{\n\t\t\tItemId:    itemId,\n\t\t\tIsHidden:  true,\n\t\t\tTimestamp: time.Date(2020, 1, 1, 0, 0, i, 0, time.UTC),\n\t\t}\n\t\tfeedbacks[i] = data.Feedback{\n\t\t\tFeedbackKey: data.FeedbackKey{\n\t\t\t\tFeedbackType: \"click\",\n\t\t\t\tUserId:       user.UserId,\n\t\t\t\tItemId:       itemId,\n\t\t\t},\n\t\t\tTimestamp: time.Date(2021, 1, 1, 0, 0, i, 0, time.UTC),\n\t\t}\n\t}\n\terr = suite.DataClient.BatchInsertItems(ctx, feedbackItems)\n\tsuite.NoError(err)\n\terr = suite.DataClient.BatchInsertFeedback(ctx, feedbacks, true, true, true)\n\tsuite.NoError(err)\n\n\t// insert latest items (visible)\n\tlatestItems := []data.Item{\n\t\t{ItemId: \"lt-1\", IsHidden: false, Timestamp: time.Date(2024, 1, 1, 0, 0, 1, 0, time.UTC)},\n\t\t{ItemId: \"lt-2\", IsHidden: false, Timestamp: time.Date(2024, 1, 1, 0, 0, 2, 0, time.UTC)},\n\t\t{ItemId: \"lt-3\", IsHidden: false, Timestamp: time.Date(2024, 1, 1, 0, 0, 3, 0, time.UTC)},\n\t}\n\terr = suite.DataClient.BatchInsertItems(ctx, latestItems)\n\tsuite.NoError(err)\n\n\t// render template\n\tqueryTpl := \"user={{ user.UserId }}\\n\" +\n\t\t\"feedback={% for f in feedback %}{{ f.Item.ItemId }}{% if not loop.last %}, {% endif %}{% endfor %}\"\n\tdocumentTpl := \"item={{ item.ItemId }}\"\n\tqueryTplBase64 := base64.StdEncoding.EncodeToString([]byte(queryTpl))\n\tdocumentTplBase64 := base64.StdEncoding.EncodeToString([]byte(documentTpl))\n\n\t// latest 10 feedback items: fb-11 to fb-02\n\tfeedbackList := []string{}\n\tfor i := 11; i >= 2; i-- {\n\t\tfeedbackList = append(feedbackList, fmt.Sprintf(\"fb-%02d\", i))\n\t}\n\texpectedQuery := fmt.Sprintf(\n\t\t\"user=%s\\nfeedback=%s\",\n\t\tuser.UserId,\n\t\tstrings.Join(feedbackList, \", \"),\n\t)\n\texpectedDocuments := []string{\"item=lt-3\", \"item=lt-2\", \"item=lt-1\"}\n\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/dashboard/ranker/prompt\").\n\t\tHeader(\"Cookie\", suite.cookie).\n\t\tQuery(\"query-template\", queryTplBase64).\n\t\tQuery(\"document-template\", documentTplBase64).\n\t\tQuery(\"user-id\", user.UserId).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(marshal(suite.T(), RerankerPrompt{\n\t\t\tQuery:     expectedQuery,\n\t\t\tDocuments: expectedDocuments,\n\t\t})).\n\t\tEnd()\n}\n\nfunc (suite *MasterAPITestSuite) TestDumpAndRestore() {\n\tctx := suite.T().Context()\n\t// insert users\n\tusers := make([]data.User, batchSize+1)\n\tfor i := range users {\n\t\tusers[i] = data.User{\n\t\t\tUserId: fmt.Sprintf(\"%05d\", i),\n\t\t\tLabels: map[string]any{\"a\": fmt.Sprintf(\"%d\", 2*i+1), \"b\": fmt.Sprintf(\"%d\", 2*i+2)},\n\t\t}\n\t}\n\terr := suite.DataClient.BatchInsertUsers(ctx, users)\n\tsuite.NoError(err)\n\t// insert items\n\titems := make([]data.Item, batchSize+1)\n\tfor i := range items {\n\t\titems[i] = data.Item{\n\t\t\tItemId: fmt.Sprintf(\"%05d\", i),\n\t\t\tLabels: map[string]any{\"a\": fmt.Sprintf(\"%d\", 2*i+1), \"b\": fmt.Sprintf(\"%d\", 2*i+2)},\n\t\t}\n\t}\n\terr = suite.DataClient.BatchInsertItems(ctx, items)\n\tsuite.NoError(err)\n\t// insert feedback\n\tfeedback := make([]data.Feedback, batchSize+1)\n\tfor i := range feedback {\n\t\tfeedback[i] = data.Feedback{\n\t\t\tFeedbackKey: data.FeedbackKey{\n\t\t\t\tFeedbackType: \"click\",\n\t\t\t\tUserId:       fmt.Sprintf(\"%05d\", i),\n\t\t\t\tItemId:       fmt.Sprintf(\"%05d\", i),\n\t\t\t},\n\t\t}\n\t}\n\terr = suite.DataClient.BatchInsertFeedback(ctx, feedback, true, true, true)\n\tsuite.NoError(err)\n\n\t// dump data\n\treq := httptest.NewRequest(\"GET\", \"https://example.com/\", nil)\n\treq.Header.Set(\"Cookie\", suite.cookie)\n\tw := httptest.NewRecorder()\n\tsuite.dump(w, req)\n\tsuite.Equal(http.StatusOK, w.Code)\n\n\t// restore data\n\terr = suite.DataClient.Purge()\n\tsuite.NoError(err)\n\treq = httptest.NewRequest(\"POST\", \"https://example.com/\", bytes.NewReader(w.Body.Bytes()))\n\treq.Header.Set(\"Cookie\", suite.cookie)\n\treq.Header.Set(\"Content-Type\", \"application/octet-stream\")\n\tw = httptest.NewRecorder()\n\tsuite.restore(w, req)\n\tsuite.Equal(http.StatusOK, w.Code)\n\n\t// check data\n\t_, returnUsers, err := suite.DataClient.GetUsers(ctx, \"\", len(users))\n\tsuite.NoError(err)\n\tif suite.Equal(len(users), len(returnUsers)) {\n\t\tsuite.Equal(users, returnUsers)\n\t}\n\t_, returnItems, err := suite.DataClient.GetItems(ctx, \"\", len(items), nil)\n\tsuite.NoError(err)\n\tif suite.Equal(len(items), len(returnItems)) {\n\t\tsuite.Equal(items, returnItems)\n\t}\n\t_, returnFeedback, err := suite.DataClient.GetFeedback(ctx, \"\", len(feedback), nil, lo.ToPtr(time.Now()))\n\tsuite.NoError(err)\n\tif suite.Equal(len(feedback), len(returnFeedback)) {\n\t\tsuite.Equal(feedback, returnFeedback)\n\t}\n\tsuite.NotEmpty(suite.scheduled)\n}\n\nfunc (suite *MasterAPITestSuite) TestExportAndImport() {\n\tctx := suite.T().Context()\n\t// insert users\n\tusers := make([]data.User, batchSize+1)\n\tfor i := range users {\n\t\tusers[i] = data.User{\n\t\t\tUserId: fmt.Sprintf(\"%05d\", i),\n\t\t\tLabels: map[string]any{\"a\": fmt.Sprintf(\"%d\", 2*i+1), \"b\": fmt.Sprintf(\"%d\", 2*i+2)},\n\t\t}\n\t}\n\terr := suite.DataClient.BatchInsertUsers(ctx, users)\n\tsuite.NoError(err)\n\t// insert items\n\titems := make([]data.Item, batchSize+1)\n\tfor i := range items {\n\t\titems[i] = data.Item{\n\t\t\tItemId: fmt.Sprintf(\"%05d\", i),\n\t\t\tLabels: map[string]any{\"a\": fmt.Sprintf(\"%d\", 2*i+1), \"b\": fmt.Sprintf(\"%d\", 2*i+2)},\n\t\t}\n\t}\n\terr = suite.DataClient.BatchInsertItems(ctx, items)\n\tsuite.NoError(err)\n\t// insert feedback\n\tfeedback := make([]data.Feedback, batchSize+1)\n\tfor i := range feedback {\n\t\tfeedback[i] = data.Feedback{\n\t\t\tFeedbackKey: data.FeedbackKey{\n\t\t\t\tFeedbackType: \"click\",\n\t\t\t\tUserId:       fmt.Sprintf(\"%05d\", i),\n\t\t\t\tItemId:       fmt.Sprintf(\"%05d\", i),\n\t\t\t},\n\t\t\tValue: 1.0,\n\t\t}\n\t}\n\terr = suite.DataClient.BatchInsertFeedback(ctx, feedback, true, true, true)\n\tsuite.NoError(err)\n\n\t// export users\n\treq := httptest.NewRequest(\"GET\", \"https://example.com/\", nil)\n\treq.Header.Set(\"Cookie\", suite.cookie)\n\tw := httptest.NewRecorder()\n\tsuite.importExportUsers(w, req)\n\tsuite.Equal(http.StatusOK, w.Code)\n\tusersData := w.Body.Bytes()\n\t// export items\n\treq = httptest.NewRequest(\"GET\", \"https://example.com/\", nil)\n\treq.Header.Set(\"Cookie\", suite.cookie)\n\tw = httptest.NewRecorder()\n\tsuite.importExportItems(w, req)\n\tsuite.Equal(http.StatusOK, w.Code)\n\titemsData := w.Body.Bytes()\n\t// export feedback\n\treq = httptest.NewRequest(\"GET\", \"https://example.com/\", nil)\n\treq.Header.Set(\"Cookie\", suite.cookie)\n\tw = httptest.NewRecorder()\n\tsuite.importExportFeedback(w, req)\n\tsuite.Equal(http.StatusOK, w.Code)\n\tfeedbackData := w.Body.Bytes()\n\n\terr = suite.DataClient.Purge()\n\tsuite.NoError(err)\n\t// import users\n\tbuf := bytes.NewBuffer(nil)\n\twriter := multipart.NewWriter(buf)\n\tfile, err := writer.CreateFormFile(\"file\", \"users.jsonl\")\n\tsuite.NoError(err)\n\t_, err = file.Write(usersData)\n\tsuite.NoError(err)\n\terr = writer.Close()\n\tsuite.NoError(err)\n\treq = httptest.NewRequest(\"POST\", \"https://example.com/\", buf)\n\treq.Header.Set(\"Cookie\", suite.cookie)\n\treq.Header.Set(\"Content-Type\", writer.FormDataContentType())\n\tw = httptest.NewRecorder()\n\tsuite.importExportUsers(w, req)\n\tsuite.Equal(http.StatusOK, w.Code)\n\t// import items\n\tbuf = bytes.NewBuffer(nil)\n\twriter = multipart.NewWriter(buf)\n\tfile, err = writer.CreateFormFile(\"file\", \"items.jsonl\")\n\tsuite.NoError(err)\n\t_, err = file.Write(itemsData)\n\tsuite.NoError(err)\n\terr = writer.Close()\n\tsuite.NoError(err)\n\treq = httptest.NewRequest(\"POST\", \"https://example.com/\", buf)\n\treq.Header.Set(\"Cookie\", suite.cookie)\n\treq.Header.Set(\"Content-Type\", writer.FormDataContentType())\n\tw = httptest.NewRecorder()\n\tsuite.importExportItems(w, req)\n\tsuite.Equal(http.StatusOK, w.Code)\n\t// import feedback\n\tbuf = bytes.NewBuffer(nil)\n\twriter = multipart.NewWriter(buf)\n\tfile, err = writer.CreateFormFile(\"file\", \"feedback.jsonl\")\n\tsuite.NoError(err)\n\t_, err = file.Write(feedbackData)\n\tsuite.NoError(err)\n\terr = writer.Close()\n\tsuite.NoError(err)\n\treq = httptest.NewRequest(\"POST\", \"https://example.com/\", buf)\n\treq.Header.Set(\"Cookie\", suite.cookie)\n\treq.Header.Set(\"Content-Type\", writer.FormDataContentType())\n\tw = httptest.NewRecorder()\n\tsuite.importExportFeedback(w, req)\n\tsuite.Equal(http.StatusOK, w.Code)\n\n\t// check data\n\t_, returnUsers, err := suite.DataClient.GetUsers(ctx, \"\", len(users))\n\tsuite.NoError(err)\n\tif suite.Equal(len(users), len(returnUsers)) {\n\t\tsuite.Equal(users, returnUsers)\n\t}\n\t_, returnItems, err := suite.DataClient.GetItems(ctx, \"\", len(items), nil)\n\tsuite.NoError(err)\n\tif suite.Equal(len(items), len(returnItems)) {\n\t\tsuite.Equal(items, returnItems)\n\t}\n\t_, returnFeedback, err := suite.DataClient.GetFeedback(ctx, \"\", len(feedback), nil, lo.ToPtr(time.Now()))\n\tsuite.NoError(err)\n\tif suite.Equal(len(feedback), len(returnFeedback)) {\n\t\tsuite.Equal(feedback, returnFeedback)\n\t}\n}\n\nfunc (suite *MasterAPITestSuite) TestChat() {\n\tcontent := \"In my younger and more vulnerable years my father gave me some advice that I've been turning over in\" +\n\t\t\" my mind ever since. \\\"Whenever you feel like criticizing any one,\\\" he told me, \\\" just remember that all \" +\n\t\t\"the people in this world haven't had the advantages that you've had.\\\"\"\n\tbuf := strings.NewReader(content)\n\treq := httptest.NewRequest(\"POST\", \"https://example.com/\", buf)\n\treq.Header.Set(\"Cookie\", suite.cookie)\n\tw := httptest.NewRecorder()\n\tsuite.chat(w, req)\n\tsuite.Equal(http.StatusOK, w.Code, w.Body.String())\n\tsuite.Equal(content, w.Body.String())\n}\n\nfunc TestMasterAPI(t *testing.T) {\n\tsuite.Run(t, new(MasterAPITestSuite))\n}\n"
  },
  {
    "path": "master/rpc.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage master\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/common/monitor\"\n\t\"github.com/gorse-io/gorse/protocol\"\n\t\"github.com/gorse-io/gorse/storage/meta\"\n\t\"github.com/juju/errors\"\n)\n\n// GetMeta returns latest configuration.\nfunc (m *Master) GetMeta(ctx context.Context, nodeInfo *protocol.NodeInfo) (*protocol.Meta, error) {\n\t// register node\n\tnode := &meta.Node{\n\t\tUUID:       nodeInfo.Uuid,\n\t\tHostname:   nodeInfo.Hostname,\n\t\tType:       nodeInfo.NodeType.String(),\n\t\tVersion:    nodeInfo.BinaryVersion,\n\t\tUpdateTime: time.Now().UTC(),\n\t}\n\tif err := m.metaStore.UpdateNode(node); err != nil {\n\t\treturn nil, err\n\t}\n\t// marshall config\n\ts, err := json.Marshal(m.Config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// save ranking model version\n\tm.collaborativeFilteringModelMutex.RLock()\n\tcollaborativeFilteringModelId := m.collaborativeFilteringMeta.ID\n\tm.collaborativeFilteringModelMutex.RUnlock()\n\t// save click model version\n\tm.clickThroughRateModelMutex.RLock()\n\tclickThroughRateModelId := m.clickThroughRateMeta.ID\n\tm.clickThroughRateModelMutex.RUnlock()\n\t// collect nodes\n\tworkers := make([]string, 0)\n\tservers := make([]string, 0)\n\tnodes, err := m.metaStore.ListNodes()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, n := range nodes {\n\t\tswitch n.Type {\n\t\tcase protocol.NodeType_Worker.String():\n\t\t\tworkers = append(workers, n.UUID)\n\t\tcase protocol.NodeType_Server.String():\n\t\t\tservers = append(servers, n.UUID)\n\t\t}\n\t}\n\treturn &protocol.Meta{\n\t\tConfig:                        string(s),\n\t\tCollaborativeFilteringModelId: collaborativeFilteringModelId,\n\t\tClickThroughRateModelId:       clickThroughRateModelId,\n\t\tMe:                            nodeInfo.Uuid,\n\t\tWorkers:                       workers,\n\t\tServers:                       servers,\n\t}, nil\n}\n\nfunc (m *Master) PushProgress(\n\t_ context.Context,\n\tin *protocol.PushProgressRequest) (*protocol.PushProgressResponse, error) {\n\t// check empty progress\n\tif len(in.Progress) == 0 {\n\t\treturn &protocol.PushProgressResponse{}, nil\n\t}\n\t// check tracers\n\ttracer := in.Progress[0].Tracer\n\tfor _, p := range in.Progress {\n\t\tif p.Tracer != tracer {\n\t\t\treturn nil, errors.Errorf(\"tracers must be the same, expect %v, got %v\", tracer, p.Tracer)\n\t\t}\n\t}\n\t// store progress\n\tm.remoteProgress.Store(tracer, monitor.DecodeProgress(in))\n\treturn &protocol.PushProgressResponse{}, nil\n}\n"
  },
  {
    "path": "master/rpc_test.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage master\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/common/monitor\"\n\t\"github.com/gorse-io/gorse/common/util\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/model\"\n\t\"github.com/gorse-io/gorse/model/cf\"\n\t\"github.com/gorse-io/gorse/model/ctr\"\n\t\"github.com/gorse-io/gorse/protocol\"\n\t\"github.com/gorse-io/gorse/server\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/gorse-io/gorse/storage/meta\"\n\t\"github.com/madflojo/testcerts\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n)\n\nfunc newRankingDataset() (*dataset.Dataset, *dataset.Dataset) {\n\treturn dataset.NewDataset(time.Now(), 0, 0), dataset.NewDataset(time.Now(), 0, 0)\n}\n\nfunc newClickDataset() (*ctr.Dataset, *ctr.Dataset) {\n\tdataSet := &ctr.Dataset{\n\t\tIndex: dataset.NewUnifiedMapIndexBuilder().Build(),\n\t}\n\treturn dataSet, dataSet\n}\n\ntype mockMasterRPC struct {\n\tMaster\n\taddr       chan string\n\tgrpcServer *grpc.Server\n}\n\nfunc newMockMasterRPC(t *testing.T) *mockMasterRPC {\n\t// create meta store\n\tmetaStore, err := meta.Open(fmt.Sprintf(\"sqlite://%s/meta.db\", t.TempDir()), time.Second)\n\tassert.NoError(t, err)\n\terr = metaStore.Init()\n\tassert.NoError(t, err)\n\t// create click model\n\ttrain, test := newClickDataset()\n\tfm := ctr.NewAFM(model.Params{model.NEpochs: 0})\n\tfm.Fit(t.Context(), train, test, &ctr.FitConfig{})\n\t// create ranking model\n\ttrainSet, testSet := newRankingDataset()\n\tbpr := cf.NewBPR(model.Params{model.NEpochs: 0})\n\tbpr.Fit(t.Context(), trainSet, testSet, cf.NewFitConfig())\n\treturn &mockMasterRPC{\n\t\tMaster: Master{\n\t\t\tRestServer: server.RestServer{\n\t\t\t\tConfig:      config.GetDefaultConfig(),\n\t\t\t\tCacheClient: cache.NoDatabase{},\n\t\t\t\tDataClient:  data.NoDatabase{},\n\t\t\t},\n\t\t\tmetaStore:                  metaStore,\n\t\t\tcollaborativeFilteringMeta: meta.Model[cf.Score]{ID: 123},\n\t\t\tclickThroughRateMeta:       meta.Model[ctr.Score]{ID: 456},\n\t\t},\n\t\taddr: make(chan string),\n\t}\n}\n\nfunc (m *mockMasterRPC) Start(t *testing.T) {\n\tlisten, err := net.Listen(\"tcp\", \":0\")\n\tassert.NoError(t, err)\n\tm.addr <- listen.Addr().String()\n\tvar opts []grpc.ServerOption\n\tm.grpcServer = grpc.NewServer(opts...)\n\tprotocol.RegisterMasterServer(m.grpcServer, m)\n\terr = m.grpcServer.Serve(listen)\n\tassert.NoError(t, err)\n}\n\nfunc (m *mockMasterRPC) StartTLS(t *testing.T, o *util.TLSConfig) {\n\tlisten, err := net.Listen(\"tcp\", \":0\")\n\tassert.NoError(t, err)\n\tm.addr <- listen.Addr().String()\n\tcreds, err := util.NewServerCreds(&util.TLSConfig{\n\t\tSSLCA:   o.SSLCA,\n\t\tSSLCert: o.SSLCert,\n\t\tSSLKey:  o.SSLKey,\n\t})\n\tassert.NoError(t, err)\n\tm.grpcServer = grpc.NewServer(grpc.Creds(creds))\n\tprotocol.RegisterMasterServer(m.grpcServer, m)\n\terr = m.grpcServer.Serve(listen)\n\tassert.NoError(t, err)\n}\n\nfunc (m *mockMasterRPC) Stop() {\n\t_ = m.metaStore.Close()\n\tm.grpcServer.Stop()\n}\n\nfunc TestRPC(t *testing.T) {\n\trpcServer := newMockMasterRPC(t)\n\tgo rpcServer.Start(t)\n\tdefer rpcServer.Stop()\n\taddress := <-rpcServer.addr\n\tconn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials()))\n\tassert.NoError(t, err)\n\tclient := protocol.NewMasterClient(conn)\n\tctx := t.Context()\n\n\tprogressList := []monitor.Progress{{\n\t\tTracer:     \"tracer\",\n\t\tName:       \"a\",\n\t\tStatus:     monitor.StatusRunning,\n\t\tTotal:      12,\n\t\tCount:      6,\n\t\tStartTime:  time.Date(2018, time.January, 1, 0, 0, 0, 0, time.Local),\n\t\tFinishTime: time.Date(2018, time.January, 2, 0, 0, 0, 0, time.Local),\n\t}}\n\t_, err = client.PushProgress(ctx, monitor.EncodeProgress(progressList))\n\tassert.NoError(t, err)\n\ti, ok := rpcServer.remoteProgress.Load(\"tracer\")\n\tassert.True(t, ok)\n\tremoteProgressList := i.([]monitor.Progress)\n\tassert.Equal(t, progressList, remoteProgressList)\n\n\t// test get meta\n\t_, err = client.GetMeta(ctx,\n\t\t&protocol.NodeInfo{NodeType: protocol.NodeType_Server, Uuid: \"server1\", Hostname: \"yoga\"})\n\tassert.NoError(t, err)\n\tmetaResp, err := client.GetMeta(ctx,\n\t\t&protocol.NodeInfo{NodeType: protocol.NodeType_Worker, Uuid: \"worker1\", Hostname: \"yoga\"})\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(123), metaResp.CollaborativeFilteringModelId)\n\tassert.Equal(t, int64(456), metaResp.ClickThroughRateModelId)\n\tassert.Equal(t, \"worker1\", metaResp.Me)\n\tassert.Equal(t, []string{\"server1\"}, metaResp.Servers)\n\tassert.Equal(t, []string{\"worker1\"}, metaResp.Workers)\n\tvar cfg config.Config\n\terr = json.Unmarshal([]byte(metaResp.Config), &cfg)\n\tassert.NoError(t, err)\n\tassert.Equal(t, rpcServer.Config, &cfg)\n\n\ttime.Sleep(time.Second * 2)\n\tmetaResp, err = client.GetMeta(ctx,\n\t\t&protocol.NodeInfo{NodeType: protocol.NodeType_Worker, Uuid: \"worker2\", Hostname: \"yoga\"})\n\tassert.NoError(t, err)\n\tassert.Equal(t, []string{\"worker2\"}, metaResp.Workers)\n\n\trpcServer.Stop()\n}\n\nfunc generateToTempFile(t *testing.T) (string, string, string) {\n\t// Generate Certificate Authority\n\tca := testcerts.NewCA()\n\t// Create a signed Certificate and Key\n\tcerts, err := ca.NewKeyPair()\n\tassert.NoError(t, err)\n\t// Write certificates to a file\n\tcaFile := filepath.Join(t.TempDir(), \"ca.pem\")\n\tcertFile := filepath.Join(t.TempDir(), \"cert.pem\")\n\tkeyFile := filepath.Join(t.TempDir(), \"key.pem\")\n\tpem := ca.PublicKey()\n\terr = os.WriteFile(caFile, pem, 0640)\n\tassert.NoError(t, err)\n\terr = certs.ToFile(certFile, keyFile)\n\tassert.NoError(t, err)\n\treturn caFile, certFile, keyFile\n}\n\nfunc TestSSL(t *testing.T) {\n\tcaFile, certFile, keyFile := generateToTempFile(t)\n\to := &util.TLSConfig{\n\t\tSSLCA:   caFile,\n\t\tSSLCert: certFile,\n\t\tSSLKey:  keyFile,\n\t}\n\trpcServer := newMockMasterRPC(t)\n\tgo rpcServer.StartTLS(t, o)\n\tdefer rpcServer.Stop()\n\taddress := <-rpcServer.addr\n\n\t// success\n\tc, err := util.NewClientCreds(o)\n\tassert.NoError(t, err)\n\tconn, err := grpc.Dial(address, grpc.WithTransportCredentials(c))\n\tassert.NoError(t, err)\n\tclient := protocol.NewMasterClient(conn)\n\t_, err = client.GetMeta(t.Context(), &protocol.NodeInfo{NodeType: protocol.NodeType_Server, Uuid: \"server1\", Hostname: \"yoga\"})\n\tassert.NoError(t, err)\n\n\t// insecure\n\tconn, err = grpc.Dial(address, grpc.WithInsecure())\n\tassert.NoError(t, err)\n\tclient = protocol.NewMasterClient(conn)\n\t_, err = client.GetMeta(t.Context(), &protocol.NodeInfo{NodeType: protocol.NodeType_Server, Uuid: \"server1\", Hostname: \"yoga\"})\n\tassert.Error(t, err)\n\n\t// certificate mismatch\n\tcaFile2, certFile2, keyFile2 := generateToTempFile(t)\n\to2 := &util.TLSConfig{\n\t\tSSLCA:   caFile2,\n\t\tSSLCert: certFile2,\n\t\tSSLKey:  keyFile2,\n\t}\n\tc, err = util.NewClientCreds(o2)\n\tassert.NoError(t, err)\n\tconn, err = grpc.Dial(address, grpc.WithTransportCredentials(c))\n\tassert.NoError(t, err)\n\tclient = protocol.NewMasterClient(conn)\n\t_, err = client.GetMeta(t.Context(), &protocol.NodeInfo{NodeType: protocol.NodeType_Server, Uuid: \"server1\", Hostname: \"yoga\"})\n\tassert.Error(t, err)\n}\n"
  },
  {
    "path": "master/tasks.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage master\n\nimport (\n\t\"context\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/c-bata/goptuna\"\n\t\"github.com/c-bata/goptuna/tpe\"\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/gorse-io/gorse/common/expression\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/common/monitor\"\n\t\"github.com/gorse-io/gorse/common/parallel\"\n\t\"github.com/gorse-io/gorse/common/sizeof\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/logics\"\n\t\"github.com/gorse-io/gorse/model\"\n\t\"github.com/gorse-io/gorse/model/cf\"\n\t\"github.com/gorse-io/gorse/model/ctr\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/gorse-io/gorse/storage/meta\"\n\t\"github.com/gorse-io/gorse/worker\"\n\t\"github.com/juju/errors\"\n\t\"github.com/samber/lo\"\n\t\"go.uber.org/zap\"\n)\n\nconst batchSize = 10000\n\nfunc (m *Master) loadDataset(parent context.Context) (datasets Datasets, err error) {\n\tctx, span := m.tracer.Start(parent, \"Load Dataset\", 1)\n\tdefer span.End()\n\n\t// Build non-personalized recommenders\n\tinitialStartTime := time.Now()\n\tnonPersonalizedRecommenders := make([]*logics.NonPersonalized, 0, len(m.Config.Recommend.NonPersonalized))\n\tfor _, cfg := range m.Config.Recommend.NonPersonalized {\n\t\trecommender, err := logics.NewNonPersonalized(cfg, m.Config.Recommend.CacheSize, initialStartTime)\n\t\tif err != nil {\n\t\t\treturn Datasets{}, errors.Trace(err)\n\t\t}\n\t\tnonPersonalizedRecommenders = append(nonPersonalizedRecommenders, recommender)\n\t}\n\n\tlog.Logger().Info(\"load dataset\",\n\t\tzap.Any(\"positive_feedback_types\", m.Config.Recommend.DataSource.PositiveFeedbackTypes),\n\t\tzap.Any(\"read_feedback_types\", m.Config.Recommend.DataSource.ReadFeedbackTypes),\n\t\tzap.Uint(\"item_ttl\", m.Config.Recommend.DataSource.ItemTTL),\n\t\tzap.Uint(\"feedback_ttl\", m.Config.Recommend.DataSource.PositiveFeedbackTTL))\n\tevaluator := NewOnlineEvaluator(\n\t\tm.Config.Recommend.DataSource.PositiveFeedbackTypes,\n\t\tm.Config.Recommend.DataSource.ReadFeedbackTypes)\n\tdatasets.clickDataset, datasets.rankingDataset, err = m.LoadDataFromDatabase(ctx, m.DataClient,\n\t\tm.Config.Recommend.DataSource.PositiveFeedbackTypes,\n\t\tm.Config.Recommend.DataSource.ReadFeedbackTypes,\n\t\tm.Config.Recommend.DataSource.ItemTTL,\n\t\tm.Config.Recommend.DataSource.PositiveFeedbackTTL,\n\t\tevaluator,\n\t\tnonPersonalizedRecommenders)\n\tif err != nil {\n\t\treturn Datasets{}, errors.Trace(err)\n\t}\n\n\t// save non-personalized recommenders to cache\n\tfor i, recommender := range nonPersonalizedRecommenders {\n\t\tscores := recommender.PopAll()\n\t\tif err = m.CacheClient.AddScores(ctx, cache.NonPersonalized, recommender.Name(), scores); err != nil {\n\t\t\tlog.Logger().Error(\"failed to cache non-personalized recommenders\", zap.Error(err))\n\t\t}\n\t\tif err = m.CacheClient.DeleteScores(ctx, []string{cache.NonPersonalized},\n\t\t\tcache.ScoreCondition{\n\t\t\t\tSubset: new(recommender.Name()),\n\t\t\t\tBefore: lo.ToPtr(recommender.Timestamp()),\n\t\t\t}); err != nil {\n\t\t\tlog.Logger().Error(\"failed to reclaim outdated items\", zap.Error(err))\n\t\t}\n\t\tif err = m.CacheClient.Set(ctx,\n\t\t\tcache.Time(cache.Key(cache.NonPersonalizedUpdateTime, recommender.Name()), recommender.Timestamp()),\n\t\t\tcache.String(cache.Key(cache.NonPersonalizedDigest, recommender.Name()), m.Config.Recommend.NonPersonalized[i].Hash()),\n\t\t); err != nil {\n\t\t\tlog.Logger().Error(\"failed to write meta\", zap.Error(err))\n\t\t}\n\t}\n\n\t// write statistics to database\n\tif err = m.CacheClient.AddTimeSeriesPoints(ctx, []cache.TimeSeriesPoint{\n\t\t{Name: cache.NumUsers, Value: float64(datasets.rankingDataset.CountUsers()), Timestamp: datasets.rankingDataset.GetTimestamp()},\n\t\t{Name: cache.NumItems, Value: float64(datasets.rankingDataset.CountItems()), Timestamp: datasets.rankingDataset.GetTimestamp()},\n\t\t{Name: cache.NumFeedback, Value: float64(len(datasets.clickDataset.Target)), Timestamp: datasets.rankingDataset.GetTimestamp()},\n\t\t{Name: cache.NumPosFeedbacks, Value: float64(datasets.clickDataset.PositiveCount), Timestamp: datasets.rankingDataset.GetTimestamp()},\n\t\t{Name: cache.NumNegFeedbacks, Value: float64(datasets.clickDataset.NegativeCount), Timestamp: datasets.rankingDataset.GetTimestamp()},\n\t}); err != nil {\n\t\tlog.Logger().Error(\"failed to write timeseries points\", zap.Error(err))\n\t}\n\tUsersTotal.Set(float64(datasets.rankingDataset.CountUsers()))\n\tif err = m.CacheClient.Set(ctx, cache.Integer(cache.Key(cache.GlobalMeta, cache.NumUsers), datasets.rankingDataset.CountUsers())); err != nil {\n\t\tlog.Logger().Error(\"failed to write number of users\", zap.Error(err))\n\t}\n\tItemsTotal.Set(float64(datasets.rankingDataset.CountItems()))\n\tif err = m.CacheClient.Set(ctx, cache.Integer(cache.Key(cache.GlobalMeta, cache.NumItems), datasets.rankingDataset.CountItems())); err != nil {\n\t\tlog.Logger().Error(\"failed to write number of items\", zap.Error(err))\n\t}\n\tImplicitFeedbacksTotal.Set(float64(datasets.rankingDataset.CountFeedback()))\n\tif err = m.CacheClient.Set(ctx, cache.Integer(cache.Key(cache.GlobalMeta, cache.NumTotalPosFeedbacks), datasets.rankingDataset.CountFeedback())); err != nil {\n\t\tlog.Logger().Error(\"failed to write number of positive feedbacks\", zap.Error(err))\n\t}\n\tUserLabelsTotal.Set(float64(datasets.clickDataset.Index.CountUserLabels()))\n\tif err = m.CacheClient.Set(ctx, cache.Integer(cache.Key(cache.GlobalMeta, cache.NumUserLabels), int(datasets.clickDataset.Index.CountUserLabels()))); err != nil {\n\t\tlog.Logger().Error(\"failed to write number of user labels\", zap.Error(err))\n\t}\n\tItemLabelsTotal.Set(float64(datasets.clickDataset.Index.CountItemLabels()))\n\tif err = m.CacheClient.Set(ctx, cache.Integer(cache.Key(cache.GlobalMeta, cache.NumItemLabels), int(datasets.clickDataset.Index.CountItemLabels()))); err != nil {\n\t\tlog.Logger().Error(\"failed to write number of item labels\", zap.Error(err))\n\t}\n\tImplicitFeedbacksTotal.Set(float64(datasets.rankingDataset.CountFeedback()))\n\tPositiveFeedbacksTotal.Set(float64(datasets.clickDataset.PositiveCount))\n\tif err = m.CacheClient.Set(ctx, cache.Integer(cache.Key(cache.GlobalMeta, cache.NumValidPosFeedbacks), datasets.clickDataset.PositiveCount)); err != nil {\n\t\tlog.Logger().Error(\"failed to write number of positive feedbacks\", zap.Error(err))\n\t}\n\tNegativeFeedbackTotal.Set(float64(datasets.clickDataset.NegativeCount))\n\tif err = m.CacheClient.Set(ctx, cache.Integer(cache.Key(cache.GlobalMeta, cache.NumValidNegFeedbacks), datasets.clickDataset.NegativeCount)); err != nil {\n\t\tlog.Logger().Error(\"failed to write number of negative feedbacks\", zap.Error(err))\n\t}\n\n\t// evaluate positive feedback rate\n\tpoints := evaluator.Evaluate()\n\tif err = m.CacheClient.AddTimeSeriesPoints(ctx, points); err != nil {\n\t\tlog.Logger().Error(\"failed to insert measurement\", zap.Error(err))\n\t}\n\n\t// collect active users and items\n\tactiveUsers, activeItems, inactiveUsers, inactiveItems := 0, 0, 0, 0\n\tfor _, userFeedback := range datasets.rankingDataset.GetUserFeedback() {\n\t\tif len(userFeedback) > 0 {\n\t\t\tactiveUsers++\n\t\t} else {\n\t\t\tinactiveUsers++\n\t\t}\n\t}\n\tfor _, itemFeedback := range datasets.rankingDataset.GetItemFeedback() {\n\t\tif len(itemFeedback) > 0 {\n\t\t\tactiveItems++\n\t\t} else {\n\t\t\tinactiveItems++\n\t\t}\n\t}\n\tActiveUsersTotal.Set(float64(activeUsers))\n\tActiveItemsTotal.Set(float64(activeItems))\n\tInactiveUsersTotal.Set(float64(inactiveUsers))\n\tInactiveItemsTotal.Set(float64(inactiveItems))\n\n\t// write categories to cache\n\tcategories := datasets.rankingDataset.GetCategories()\n\tcategoryScores := make([]cache.Score, 0, len(categories))\n\tfor category, count := range categories {\n\t\tcategoryScores = append(categoryScores, cache.Score{\n\t\t\tId:        category,\n\t\t\tScore:     float64(count),\n\t\t\tTimestamp: initialStartTime,\n\t\t})\n\t}\n\tif err = m.CacheClient.AddScores(ctx, cache.ItemCategories, \"\", categoryScores); err != nil {\n\t\tlog.Logger().Error(\"failed to write categories to cache\", zap.Error(err))\n\t}\n\n\t// split ranking dataset\n\tstartTime := time.Now()\n\tdatasets.rankingTrainSet, datasets.rankingTestSet = datasets.rankingDataset.SplitCF(0, 0)\n\tLoadDatasetStepSecondsVec.WithLabelValues(\"split_ranking_dataset\").Set(time.Since(startTime).Seconds())\n\tMemoryInUseBytesVec.WithLabelValues(\"collaborative_filtering_train_set\").Set(float64(sizeof.DeepSize(datasets.rankingTrainSet)))\n\tMemoryInUseBytesVec.WithLabelValues(\"collaborative_filtering_test_set\").Set(float64(sizeof.DeepSize(datasets.rankingTestSet)))\n\n\t// split click dataset\n\tstartTime = time.Now()\n\tdatasets.clickTrainSet, datasets.clickTestSet = datasets.clickDataset.Split(0.2, 0)\n\t// After splitting, clickDataset is set to nil to free memory.\n\t// WARNING: Do not access datasets.clickDataset after this point; use clickTrainSet and clickTestSet instead.\n\tdatasets.clickDataset = nil\n\tLoadDatasetStepSecondsVec.WithLabelValues(\"split_click_dataset\").Set(time.Since(startTime).Seconds())\n\tMemoryInUseBytesVec.WithLabelValues(\"ranking_train_set\").Set(float64(sizeof.DeepSize(datasets.clickTrainSet)))\n\tMemoryInUseBytesVec.WithLabelValues(\"ranking_test_set\").Set(float64(sizeof.DeepSize(datasets.clickTestSet)))\n\n\tLoadDatasetTotalSeconds.Set(time.Since(initialStartTime).Seconds())\n\treturn\n}\n\n// runLoadDatasetTask loads dataset.\nfunc (m *Master) runLoadDatasetTask(ctx context.Context) error {\n\tdatasets, err := m.loadDataset(ctx)\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tuseCollaborativeFilteringTasks := !strings.EqualFold(m.Config.Recommend.Collaborative.Type, \"none\")\n\tuseClickThroughRateTasks := strings.EqualFold(m.Config.Recommend.Ranker.Type, \"fm\")\n\tif err = m.updateUserToUser(ctx, datasets.rankingDataset); err != nil {\n\t\tlog.Logger().Error(\"failed to update user-to-user recommendation\", zap.Error(err))\n\t}\n\tif err = m.updateItemToItem(ctx, datasets.rankingDataset); err != nil {\n\t\tlog.Logger().Error(\"failed to update item-to-item recommendation\", zap.Error(err))\n\t}\n\tif useCollaborativeFilteringTasks {\n\t\tif err = m.trainCollaborativeFiltering(ctx, datasets.rankingTrainSet, datasets.rankingTestSet); err != nil {\n\t\t\tlog.Logger().Error(\"failed to train collaborative filtering model\", zap.Error(err))\n\t\t}\n\t}\n\tif useClickThroughRateTasks {\n\t\tif err = m.trainClickThroughRatePrediction(ctx, datasets.clickTrainSet, datasets.clickTestSet); err != nil {\n\t\t\tlog.Logger().Error(\"failed to train click-through rate prediction model\", zap.Error(err))\n\t\t}\n\t}\n\tif m.standalone {\n\t\tif err = m.updateRecommend(ctx); err != nil {\n\t\t\tlog.Logger().Error(\"failed to update recommendation\", zap.Error(err))\n\t\t}\n\t}\n\tif err = m.collectGarbage(ctx, datasets.rankingDataset); err != nil {\n\t\tlog.Logger().Error(\"failed to collect garbage in cache\", zap.Error(err))\n\t}\n\tif useCollaborativeFilteringTasks && m.Config.Recommend.Collaborative.OptimizePeriod > 0 {\n\t\tif err = m.optimizeCollaborativeFiltering(ctx, datasets.rankingTrainSet, datasets.rankingTestSet); err != nil {\n\t\t\tlog.Logger().Error(\"failed to optimize collaborative filtering model\", zap.Error(err))\n\t\t}\n\t}\n\tif useClickThroughRateTasks && m.Config.Recommend.Ranker.OptimizePeriod > 0 {\n\t\tif err = m.optimizeClickThroughRatePrediction(ctx, datasets.clickTrainSet, datasets.clickTestSet); err != nil {\n\t\t\tlog.Logger().Error(\"failed to optimize click-through rate prediction model\", zap.Error(err))\n\t\t}\n\t}\n\treturn nil\n}\n\n// LoadDataFromDatabase loads dataset from data store.\nfunc (m *Master) LoadDataFromDatabase(\n\tctx context.Context,\n\tdatabase data.Database,\n\tposFeedbackTypes, readTypes []expression.FeedbackTypeExpression,\n\titemTTL, positiveFeedbackTTL uint,\n\tevaluator *OnlineEvaluator,\n\tnonPersonalizedRecommenders []*logics.NonPersonalized,\n) (ctrDataset *ctr.Dataset, dataSet *dataset.Dataset, err error) {\n\t// Estimate the number of users, items, and feedbacks\n\testimatedNumUsers, err := m.DataClient.CountUsers(ctx)\n\tif err != nil {\n\t\treturn nil, nil, errors.Trace(err)\n\t}\n\testimatedNumItems, err := m.DataClient.CountItems(ctx)\n\tif err != nil {\n\t\treturn nil, nil, errors.Trace(err)\n\t}\n\testimatedNumFeedbacks, err := m.DataClient.CountFeedback(ctx)\n\tif err != nil {\n\t\treturn nil, nil, errors.Trace(err)\n\t}\n\n\tdataSet = dataset.NewDataset(time.Now(), estimatedNumUsers, estimatedNumItems)\n\n\tnewCtx, span := monitor.Start(ctx, \"LoadDataFromDatabase\",\n\t\testimatedNumUsers+estimatedNumItems+estimatedNumFeedbacks)\n\tdefer span.End()\n\n\t// setup time limit\n\tvar feedbackTimeLimit data.ScanOption\n\tvar itemTimeLimit *time.Time\n\tif itemTTL > 0 {\n\t\ttemp := time.Now().AddDate(0, 0, -int(itemTTL))\n\t\titemTimeLimit = &temp\n\t}\n\tif positiveFeedbackTTL > 0 {\n\t\ttemp := time.Now().AddDate(0, 0, -int(positiveFeedbackTTL))\n\t\tfeedbackTimeLimit = data.WithBeginTime(temp)\n\t}\n\n\t// STEP 1: pull users\n\tuserLabelCount := make(map[string]int)\n\tuserLabelFirst := make(map[string]int32)\n\tuserLabelIndex := dataset.NewMapIndex()\n\tuserLabels := make([][]lo.Tuple2[int32, float32], 0, estimatedNumUsers)\n\tstart := time.Now()\n\tuserChan, errChan := database.GetUserStream(newCtx, batchSize)\n\tfor users := range userChan {\n\t\tfor _, user := range users {\n\t\t\tdataSet.AddUser(user)\n\t\t\tuserIndex := dataSet.GetUserDict().Id(user.UserId)\n\t\t\tif len(userLabels) == int(userIndex) {\n\t\t\t\tuserLabels = append(userLabels, nil)\n\t\t\t}\n\t\t\tfeatures := ctr.ConvertLabels(user.Labels)\n\t\t\tuserLabels[userIndex] = make([]lo.Tuple2[int32, float32], 0, len(features))\n\t\t\tfor _, feature := range features {\n\t\t\t\tuserLabelCount[feature.Name]++\n\t\t\t\t// Memorize the first occurrence.\n\t\t\t\tif userLabelCount[feature.Name] == 1 {\n\t\t\t\t\tuserLabelFirst[feature.Name] = userIndex\n\t\t\t\t}\n\t\t\t\t// Add the label to the index in second occurrence.\n\t\t\t\tif userLabelCount[feature.Name] == 2 {\n\t\t\t\t\tuserLabelIndex.Add(feature.Name)\n\t\t\t\t\tfirstUserIndex := userLabelFirst[feature.Name]\n\t\t\t\t\tuserLabels[firstUserIndex] = append(userLabels[firstUserIndex], lo.Tuple2[int32, float32]{\n\t\t\t\t\t\tA: userLabelIndex.ToNumber(feature.Name),\n\t\t\t\t\t\tB: feature.Value,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\t// Add the label to the user.\n\t\t\t\tif userLabelCount[feature.Name] > 1 {\n\t\t\t\t\tuserLabels[userIndex] = append(userLabels[userIndex], lo.Tuple2[int32, float32]{\n\t\t\t\t\t\tA: userLabelIndex.ToNumber(feature.Name),\n\t\t\t\t\t\tB: feature.Value,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tspan.Add(len(users))\n\t}\n\tif err = <-errChan; err != nil {\n\t\treturn nil, nil, errors.Trace(err)\n\t}\n\tlog.Logger().Debug(\"pulled users from database\",\n\t\tzap.Int(\"n_users\", dataSet.CountUsers()),\n\t\tzap.Int32(\"n_user_labels\", userLabelIndex.Len()),\n\t\tzap.Duration(\"used_time\", time.Since(start)))\n\tLoadDatasetStepSecondsVec.WithLabelValues(\"load_users\").Set(time.Since(start).Seconds())\n\n\t// STEP 2: pull items\n\tvar items []data.Item\n\titemLabelCount := make(map[string]int)\n\titemLabelFirst := make(map[string]int32)\n\titemLabelIndex := dataset.NewMapIndex()\n\titemLabels := make([][]lo.Tuple2[int32, float32], 0, estimatedNumItems)\n\titemEmbeddingIndexer := dataset.NewMapIndex()\n\titemEmbeddingDimension := make([]map[int]int, 0)\n\titemEmbeddings := make([][][]float32, 0, estimatedNumItems)\n\tstart = time.Now()\n\titemChan, errChan := database.GetItemStream(newCtx, batchSize, itemTimeLimit)\n\tfor batchItems := range itemChan {\n\t\titems = append(items, batchItems...)\n\t\tfor _, item := range batchItems {\n\t\t\tdataSet.AddItem(item)\n\t\t\titemIndex := dataSet.GetItemDict().Id(item.ItemId)\n\t\t\tif len(itemLabels) == int(itemIndex) {\n\t\t\t\titemLabels = append(itemLabels, nil)\n\t\t\t}\n\t\t\tif len(itemEmbeddings) == int(itemIndex) {\n\t\t\t\titemEmbeddings = append(itemEmbeddings, nil)\n\t\t\t}\n\t\t\t// load labels\n\t\t\tlabels := ctr.ConvertLabels(item.Labels)\n\t\t\titemLabels[itemIndex] = make([]lo.Tuple2[int32, float32], 0, len(labels))\n\t\t\tfor _, feature := range labels {\n\t\t\t\titemLabelCount[feature.Name]++\n\t\t\t\t// Memorize the first occurrence.\n\t\t\t\tif itemLabelCount[feature.Name] == 1 {\n\t\t\t\t\titemLabelFirst[feature.Name] = itemIndex\n\t\t\t\t}\n\t\t\t\t// Add the label to the index in second occurrence.\n\t\t\t\tif itemLabelCount[feature.Name] == 2 {\n\t\t\t\t\titemLabelIndex.Add(feature.Name)\n\t\t\t\t\tfirstItemIndex := itemLabelFirst[feature.Name]\n\t\t\t\t\titemLabels[firstItemIndex] = append(itemLabels[firstItemIndex], lo.Tuple2[int32, float32]{\n\t\t\t\t\t\tA: itemLabelIndex.ToNumber(feature.Name),\n\t\t\t\t\t\tB: feature.Value,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\t// Add the label to the item.\n\t\t\t\tif itemLabelCount[feature.Name] > 1 {\n\t\t\t\t\titemLabels[itemIndex] = append(itemLabels[itemIndex], lo.Tuple2[int32, float32]{\n\t\t\t\t\t\tA: itemLabelIndex.ToNumber(feature.Name),\n\t\t\t\t\t\tB: feature.Value,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t\t// load embeddings\n\t\t\tembeddings := ctr.ConvertEmbeddings(item.Labels)\n\t\t\titemEmbeddings[itemIndex] = make([][]float32, 0, len(embeddings))\n\t\t\tfor _, embedding := range embeddings {\n\t\t\t\titemEmbeddingIndexer.Add(embedding.Name)\n\t\t\t\titemEmbeddingIndex := itemEmbeddingIndexer.ToNumber(embedding.Name)\n\t\t\t\tfor len(itemEmbeddings[itemIndex]) <= int(itemEmbeddingIndex) {\n\t\t\t\t\titemEmbeddings[itemIndex] = append(itemEmbeddings[itemIndex], nil)\n\t\t\t\t}\n\t\t\t\titemEmbeddings[itemIndex][itemEmbeddingIndex] = embedding.Value\n\t\t\t\tfor len(itemEmbeddingDimension) <= int(itemEmbeddingIndex) {\n\t\t\t\t\titemEmbeddingDimension = append(itemEmbeddingDimension, make(map[int]int))\n\t\t\t\t}\n\t\t\t\titemEmbeddingDimension[itemEmbeddingIndex][len(itemEmbeddings[itemIndex][itemEmbeddingIndex])]++\n\t\t\t}\n\t\t}\n\t\tspan.Add(len(batchItems))\n\t}\n\tif err = <-errChan; err != nil {\n\t\treturn nil, nil, errors.Trace(err)\n\t}\n\tlog.Logger().Debug(\"pulled items from database\",\n\t\tzap.Int(\"n_items\", dataSet.CountItems()),\n\t\tzap.Int32(\"n_item_labels\", itemLabelIndex.Len()),\n\t\tzap.Duration(\"used_time\", time.Since(start)))\n\tLoadDatasetStepSecondsVec.WithLabelValues(\"load_items\").Set(time.Since(start).Seconds())\n\n\t// create positive set\n\tpositiveSet := make([]mapset.Set[int32], dataSet.CountUsers())\n\tfor i := range positiveSet {\n\t\tpositiveSet[i] = mapset.NewSet[int32]()\n\t}\n\n\t// split item groups\n\tsort.Slice(items, func(i, j int) bool {\n\t\treturn items[i].ItemId < items[j].ItemId\n\t})\n\titemGroups := parallel.Split(items, m.Config.Master.NumJobs)\n\n\t// STEP 3: pull positive feedback\n\tvar mu sync.Mutex\n\tvar posFeedbackCount int\n\tstart = time.Now()\n\terr = parallel.Parallel(newCtx, len(itemGroups), m.Config.Master.NumJobs, func(_, i int) error {\n\t\tvar itemFeedback []data.Feedback\n\t\tvar itemGroupIndex int\n\t\titemHasFeedback := make([]bool, len(itemGroups[i]))\n\t\tfeedbackChan, errChan := database.GetFeedbackStream(newCtx, batchSize,\n\t\t\tdata.WithBeginItemId(itemGroups[i][0].ItemId),\n\t\t\tdata.WithEndItemId(itemGroups[i][len(itemGroups[i])-1].ItemId),\n\t\t\tfeedbackTimeLimit,\n\t\t\tdata.WithEndTime(*m.Config.Now()),\n\t\t\tdata.WithFeedbackTypes(posFeedbackTypes...),\n\t\t\tdata.WithOrderByItemId())\n\t\tfor feedback := range feedbackChan {\n\t\t\tfor _, f := range feedback {\n\t\t\t\t// convert user and item id to index\n\t\t\t\tuserIndex := dataSet.GetUserDict().Id(f.UserId)\n\t\t\t\tif userIndex == dataset.NotId {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\titemIndex := dataSet.GetItemDict().Id(f.ItemId)\n\t\t\t\tif itemIndex == dataset.NotId {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t// insert feedback to positive set\n\t\t\t\tpositiveSet[userIndex].Add(itemIndex)\n\n\t\t\t\tmu.Lock()\n\t\t\t\tposFeedbackCount++\n\t\t\t\t// insert feedback to evaluator\n\t\t\t\tevaluator.Add(f.FeedbackType, f.Value, userIndex, itemIndex, f.Timestamp)\n\t\t\t\tmu.Unlock()\n\n\t\t\t\t// append item feedback\n\t\t\t\tif len(itemFeedback) == 0 || itemFeedback[len(itemFeedback)-1].ItemId == f.ItemId {\n\t\t\t\t\titemFeedback = append(itemFeedback, f)\n\t\t\t\t} else {\n\t\t\t\t\t// add item to non-personalized recommenders\n\t\t\t\t\titemHasFeedback[itemGroupIndex] = true\n\t\t\t\t\tfor _, recommender := range nonPersonalizedRecommenders {\n\t\t\t\t\t\trecommender.Push(itemGroups[i][itemGroupIndex], itemFeedback)\n\t\t\t\t\t}\n\t\t\t\t\titemFeedback = itemFeedback[:0]\n\t\t\t\t\titemFeedback = append(itemFeedback, f)\n\t\t\t\t}\n\t\t\t\t// find item group index\n\t\t\t\tfor itemGroupIndex = 0; itemGroupIndex < len(itemGroups[i]); itemGroupIndex++ {\n\t\t\t\t\tif itemGroups[i][itemGroupIndex].ItemId == f.ItemId {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tdataSet.AddFeedback(f.UserId, f.ItemId, f.Timestamp)\n\t\t\t}\n\t\t\tspan.Add(len(feedback))\n\t\t}\n\n\t\t// add item to non-personalized recommenders\n\t\tif len(itemFeedback) > 0 {\n\t\t\titemHasFeedback[itemGroupIndex] = true\n\t\t\tfor _, recommender := range nonPersonalizedRecommenders {\n\t\t\t\trecommender.Push(itemGroups[i][itemGroupIndex], itemFeedback)\n\t\t\t}\n\t\t}\n\t\tfor index, hasFeedback := range itemHasFeedback {\n\t\t\tif !hasFeedback {\n\t\t\t\tfor _, recommender := range nonPersonalizedRecommenders {\n\t\t\t\t\trecommender.Push(itemGroups[i][index], nil)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif err = <-errChan; err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, nil, errors.Trace(err)\n\t}\n\tlog.Logger().Debug(\"pulled positive feedback from database\",\n\t\tzap.Int(\"n_positive_feedback\", posFeedbackCount),\n\t\tzap.Duration(\"used_time\", time.Since(start)))\n\tLoadDatasetStepSecondsVec.WithLabelValues(\"load_positive_feedback\").Set(time.Since(start).Seconds())\n\n\t// create negative set\n\tnegativeSet := make([]mapset.Set[int32], dataSet.CountUsers())\n\tfor i := range negativeSet {\n\t\tnegativeSet[i] = mapset.NewSet[int32]()\n\t}\n\n\t// STEP 4: pull negative feedback\n\tstart = time.Now()\n\tvar negativeFeedbackCount float64\n\terr = parallel.Parallel(newCtx, len(itemGroups), m.Config.Master.NumJobs, func(_, i int) error {\n\t\tfeedbackChan, errChan := database.GetFeedbackStream(newCtx, batchSize,\n\t\t\tdata.WithBeginItemId(itemGroups[i][0].ItemId),\n\t\t\tdata.WithEndItemId(itemGroups[i][len(itemGroups[i])-1].ItemId),\n\t\t\tfeedbackTimeLimit,\n\t\t\tdata.WithEndTime(*m.Config.Now()),\n\t\t\tdata.WithFeedbackTypes(readTypes...))\n\t\tfor feedback := range feedbackChan {\n\t\t\tfor _, f := range feedback {\n\t\t\t\tuserIndex := dataSet.GetUserDict().Id(f.UserId)\n\t\t\t\tif userIndex == dataset.NotId {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\titemIndex := dataSet.GetItemDict().Id(f.ItemId)\n\t\t\t\tif itemIndex == dataset.NotId {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tnegativeSet[userIndex].Add(itemIndex)\n\t\t\t\tmu.Lock()\n\t\t\t\tnegativeFeedbackCount++\n\t\t\t\tevaluator.Add(f.FeedbackType, f.Value, userIndex, itemIndex, f.Timestamp)\n\t\t\t\tmu.Unlock()\n\t\t\t}\n\t\t\tspan.Add(len(feedback))\n\t\t}\n\t\tif err = <-errChan; err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, nil, errors.Trace(err)\n\t}\n\tlog.Logger().Debug(\"pulled negative feedback from database\",\n\t\tzap.Int(\"n_negative_feedback\", int(negativeFeedbackCount)),\n\t\tzap.Duration(\"used_time\", time.Since(start)))\n\tLoadDatasetStepSecondsVec.WithLabelValues(\"load_negative_feedback\").Set(time.Since(start).Seconds())\n\n\t// STEP 5: create click-through rate dataset\n\tstart = time.Now()\n\tunifiedIndex := dataset.NewUnifiedMapIndexBuilder()\n\tunifiedIndex.ItemIndex = dataSet.GetItemDict().ToIndex()\n\tunifiedIndex.UserIndex = dataSet.GetUserDict().ToIndex()\n\tunifiedIndex.ItemLabelIndex = itemLabelIndex\n\tunifiedIndex.UserLabelIndex = userLabelIndex\n\tctrDataset = &ctr.Dataset{\n\t\tIndex:      unifiedIndex.Build(),\n\t\tUserLabels: userLabels,\n\t\tItemLabels: itemLabels,\n\t\tUsers:      make([]int32, 0, estimatedNumFeedbacks),\n\t\tItems:      make([]int32, 0, estimatedNumFeedbacks),\n\t\tTarget:     make([]float32, 0, estimatedNumFeedbacks),\n\t}\n\tctrDataset.ItemEmbeddingIndex = itemEmbeddingIndexer\n\tctrDataset.ItemEmbeddingDimension = make([]int, len(itemEmbeddingDimension))\n\tfor i, dimension := range itemEmbeddingDimension {\n\t\tfor dim, cnt := range dimension {\n\t\t\tif cnt > itemEmbeddingDimension[i][ctrDataset.ItemEmbeddingDimension[i]] {\n\t\t\t\tctrDataset.ItemEmbeddingDimension[i] = dim\n\t\t\t}\n\t\t}\n\t}\n\tfor i, embeddings := range itemEmbeddings {\n\t\tfor j, embedding := range embeddings {\n\t\t\tif len(embedding) != ctrDataset.ItemEmbeddingDimension[j] {\n\t\t\t\titemEmbeddings[i][j] = nil\n\t\t\t}\n\t\t}\n\t}\n\tctrDataset.ItemEmbeddings = itemEmbeddings\n\tfor userIndex := range positiveSet {\n\t\t// insert positive feedback\n\t\tfor _, itemIndex := range positiveSet[userIndex].ToSlice() {\n\t\t\tctrDataset.Users = append(ctrDataset.Users, int32(userIndex))\n\t\t\tctrDataset.Items = append(ctrDataset.Items, itemIndex)\n\t\t\tctrDataset.Target = append(ctrDataset.Target, 1)\n\t\t\tctrDataset.PositiveCount++\n\t\t}\n\t\t// insert negative feedback\n\t\tfor _, itemIndex := range negativeSet[userIndex].ToSlice() {\n\t\t\tctrDataset.Users = append(ctrDataset.Users, int32(userIndex))\n\t\t\tctrDataset.Items = append(ctrDataset.Items, itemIndex)\n\t\t\tctrDataset.Target = append(ctrDataset.Target, -1)\n\t\t\tctrDataset.NegativeCount++\n\t\t}\n\t\t// release positive set and negative set\n\t\tpositiveSet[userIndex] = nil\n\t\tnegativeSet[userIndex] = nil\n\t}\n\tlog.Logger().Debug(\"created ranking dataset\",\n\t\tzap.Int(\"n_valid_positive\", ctrDataset.PositiveCount),\n\t\tzap.Int(\"n_valid_negative\", ctrDataset.NegativeCount),\n\t\tzap.Duration(\"used_time\", time.Since(start)))\n\tLoadDatasetStepSecondsVec.WithLabelValues(\"create_ranking_dataset\").Set(time.Since(start).Seconds())\n\treturn ctrDataset, dataSet, nil\n}\n\nfunc (m *Master) updateItemToItem(parent context.Context, dataset *dataset.Dataset) error {\n\tif len(m.Config.Recommend.ItemToItem) == 0 {\n\t\treturn nil\n\t}\n\tctx, span := m.tracer.Start(parent, \"Generate item-to-item recommendation\",\n\t\tlen(dataset.GetItems())*(len(m.Config.Recommend.ItemToItem))*2)\n\tdefer span.End()\n\n\t// Build item-to-item recommenders\n\titemToItemRecommenders := make([]logics.ItemToItem, 0, len(m.Config.Recommend.ItemToItem))\n\tfor _, cfg := range m.Config.Recommend.ItemToItem {\n\t\trecommender, err := logics.NewItemToItem(cfg, m.Config.Recommend.CacheSize, dataset.GetTimestamp(), &logics.ItemToItemOptions{\n\t\t\tTagsIDF:      dataset.GetItemColumnValuesIDF(),\n\t\t\tUsersIDF:     dataset.GetUserIDF(),\n\t\t\tOpenAIConfig: m.Config.OpenAI,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\titemToItemRecommenders = append(itemToItemRecommenders, recommender)\n\t}\n\n\t// Push items to item-to-item recommenders\n\tif err := parallel.ForEach(ctx, dataset.GetItems(), m.Config.Master.NumJobs, func(i int, item data.Item) {\n\t\tfor _, recommender := range itemToItemRecommenders {\n\t\t\trecommender.Push(&item, dataset.GetItemFeedback()[i])\n\t\t\tspan.Add(1)\n\t\t}\n\t}); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\n\t// Save item-to-item recommendations to cache\n\tfor i, recommender := range itemToItemRecommenders {\n\t\tif err := parallel.For(ctx, recommender.Count(), m.Config.Master.NumJobs, func(j int) {\n\t\t\titem := recommender.Get(j)\n\t\t\titemToItemConfig := m.Config.Recommend.ItemToItem[i]\n\t\t\tif m.needUpdateItemToItem(ctx, item.ItemId, itemToItemConfig) {\n\t\t\t\tdefer span.Add(1)\n\t\t\t\tscore := recommender.PopAll(j)\n\t\t\t\tif score == nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlog.Logger().Debug(\"update item-to-item recommendation\",\n\t\t\t\t\tzap.String(\"item_id\", item.ItemId),\n\t\t\t\t\tzap.String(\"name\", itemToItemConfig.Name),\n\t\t\t\t\tzap.Int(\"n_recommendations\", len(score)))\n\t\t\t\t// Save item-to-item recommendation to cache\n\t\t\t\tif err := m.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(itemToItemConfig.Name, item.ItemId), score); err != nil {\n\t\t\t\t\tlog.Logger().Error(\"failed to save item-to-item recommendation to cache\",\n\t\t\t\t\t\tzap.String(\"item_id\", item.ItemId), zap.Error(err))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// Save item-to-item digest and last update time to cache\n\t\t\t\tif err := m.CacheClient.Set(ctx,\n\t\t\t\t\tcache.String(cache.Key(cache.ItemToItemDigest, itemToItemConfig.Name, item.ItemId), itemToItemConfig.Hash(&m.Config.Recommend)),\n\t\t\t\t\tcache.Time(cache.Key(cache.ItemToItemUpdateTime, itemToItemConfig.Name, item.ItemId), time.Now()),\n\t\t\t\t); err != nil {\n\t\t\t\t\tlog.Logger().Error(\"failed to save item-to-item digest to cache\",\n\t\t\t\t\t\tzap.String(\"item_id\", item.ItemId), zap.Error(err))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// Remove stale item-to-item recommendation\n\t\t\t\tif err := m.CacheClient.DeleteScores(ctx, []string{cache.ItemToItem}, cache.ScoreCondition{\n\t\t\t\t\tSubset: lo.ToPtr(cache.Key(itemToItemConfig.Name, item.ItemId)),\n\t\t\t\t\tBefore: lo.ToPtr(recommender.Timestamp()),\n\t\t\t\t}); err != nil {\n\t\t\t\t\tlog.Logger().Error(\"failed to remove stale item-to-item recommendation\",\n\t\t\t\t\t\tzap.String(\"item_id\", item.ItemId), zap.Error(err))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tspan.Add(1)\n\t\t\t}\n\t\t}); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// needUpdateItemToItem checks if item-to-item recommendation needs to be updated.\nfunc (m *Master) needUpdateItemToItem(ctx context.Context, itemId string, itemToItemConfig config.ItemToItemConfig) bool {\n\t// check cache\n\titems, err := m.CacheClient.SearchScores(ctx, cache.ItemToItem,\n\t\tcache.Key(itemToItemConfig.Name, itemId), nil, 0, -1)\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to fetch item-to-item recommendation\",\n\t\t\tzap.String(\"item_id\", itemId), zap.Error(err))\n\t\treturn true\n\t} else if len(items) == 0 {\n\t\treturn true\n\t}\n\n\t// check digest\n\tdigest, err := m.CacheClient.Get(ctx, cache.Key(cache.ItemToItemDigest, itemToItemConfig.Name, itemId)).String()\n\tif err != nil {\n\t\tif !errors.Is(err, errors.NotFound) {\n\t\t\tlog.Logger().Error(\"failed to read item-to-item digest\", zap.Error(err))\n\t\t}\n\t\treturn true\n\t}\n\tif digest != itemToItemConfig.Hash(&m.Config.Recommend) {\n\t\treturn true\n\t}\n\n\t// check update time\n\tupdateTime, err := m.CacheClient.Get(ctx, cache.Key(cache.ItemToItemUpdateTime, itemToItemConfig.Name, itemId)).Time()\n\tif err != nil {\n\t\tif !errors.Is(err, errors.NotFound) {\n\t\t\tlog.Logger().Error(\"failed to read last update item neighbors time\", zap.Error(err))\n\t\t}\n\t\treturn true\n\t}\n\treturn updateTime.Before(time.Now().Add(-m.Config.Recommend.CacheExpire))\n}\n\nfunc (m *Master) updateUserToUser(parent context.Context, dataset *dataset.Dataset) error {\n\tif len(m.Config.Recommend.UserToUser) == 0 {\n\t\treturn nil\n\t}\n\tctx, span := m.tracer.Start(parent, \"Generate user-to-user recommendation\",\n\t\tlen(dataset.GetUsers())*(len(m.Config.Recommend.UserToUser))*2)\n\tdefer span.End()\n\n\tuserToUserRecommenders := make([]logics.UserToUser, 0, len(m.Config.Recommend.UserToUser))\n\tfor _, cfg := range m.Config.Recommend.UserToUser {\n\t\trecommender, err := logics.NewUserToUser(cfg, m.Config.Recommend.CacheSize, dataset.GetTimestamp(), &logics.UserToUserOptions{\n\t\t\tTagsIDF:  dataset.GetUserColumnValuesIDF(),\n\t\t\tItemsIDF: dataset.GetItemIDF(),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\tuserToUserRecommenders = append(userToUserRecommenders, recommender)\n\t}\n\n\t// Push users to user-to-user recommender\n\tif err := parallel.ForEach(ctx, dataset.GetUsers(), m.Config.Master.NumJobs, func(i int, user data.User) {\n\t\tfor _, recommender := range userToUserRecommenders {\n\t\t\trecommender.Push(&user, dataset.GetUserFeedback()[i])\n\t\t\tspan.Add(1)\n\t\t}\n\t}); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\n\t// Save user-to-user recommendations to cache\n\tfor i, recommender := range userToUserRecommenders {\n\t\tif err := parallel.ForEach(ctx, recommender.Users(), m.Config.Master.NumJobs, func(j int, user *data.User) {\n\t\t\tuserToUserConfig := m.Config.Recommend.UserToUser[i]\n\t\t\tif m.needUpdateUserToUser(ctx, user.UserId, userToUserConfig) {\n\t\t\t\tscore := recommender.PopAll(j)\n\t\t\t\tif score == nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlog.Logger().Debug(\"update user neighbors\",\n\t\t\t\t\tzap.String(\"user_id\", user.UserId),\n\t\t\t\t\tzap.Int(\"n_recommendations\", len(score)))\n\t\t\t\t// Save user-to-user recommendations to cache\n\t\t\t\tif err := m.CacheClient.AddScores(ctx, cache.UserToUser, cache.Key(userToUserConfig.Name, user.UserId), score); err != nil {\n\t\t\t\t\tlog.Logger().Error(\"failed to save user neighbors to cache\", zap.String(\"user_id\", user.UserId), zap.Error(err))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// Save user-to-user digest and last update time to cache\n\t\t\t\tif err := m.CacheClient.Set(ctx,\n\t\t\t\t\tcache.String(cache.Key(cache.UserToUserDigest, cache.Key(userToUserConfig.Name, user.UserId)), userToUserConfig.Hash(&m.Config.Recommend)),\n\t\t\t\t\tcache.Time(cache.Key(cache.UserToUserUpdateTime, cache.Key(userToUserConfig.Name, user.UserId)), time.Now()),\n\t\t\t\t); err != nil {\n\t\t\t\t\tlog.Logger().Error(\"failed to save user neighbors digest to cache\", zap.String(\"user_id\", user.UserId), zap.Error(err))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// Delete stale user-to-user recommendations\n\t\t\t\tif err := m.CacheClient.DeleteScores(ctx, []string{cache.UserToUser}, cache.ScoreCondition{\n\t\t\t\t\tSubset: lo.ToPtr(cache.Key(userToUserConfig.Name, user.UserId)),\n\t\t\t\t\tBefore: lo.ToPtr(recommender.Timestamp()),\n\t\t\t\t}); err != nil {\n\t\t\t\t\tlog.Logger().Error(\"failed to remove stale user neighbors\", zap.String(\"user_id\", user.UserId), zap.Error(err))\n\t\t\t\t}\n\t\t\t}\n\t\t\tspan.Add(1)\n\t\t}); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// needUpdateUserToUser checks if user-to-user recommendation needs to be updated.\nfunc (m *Master) needUpdateUserToUser(ctx context.Context, userId string, userToUserConfig config.UserToUserConfig) bool {\n\t// check cache\n\tif items, err := m.CacheClient.SearchScores(ctx, cache.UserToUser, cache.Key(userToUserConfig.Name, userId), nil, 0, -1); err != nil {\n\t\tlog.Logger().Error(\"failed to load user neighbors\", zap.String(\"user_id\", userId), zap.Error(err))\n\t\treturn true\n\t} else if len(items) == 0 {\n\t\treturn true\n\t}\n\n\t// read digest\n\tcacheDigest, err := m.CacheClient.Get(ctx, cache.Key(cache.UserToUserDigest, cache.Key(userToUserConfig.Name, userId))).String()\n\tif err != nil {\n\t\tif !errors.Is(err, errors.NotFound) {\n\t\t\tlog.Logger().Error(\"failed to read user neighbors digest\", zap.Error(err))\n\t\t}\n\t\treturn true\n\t}\n\tif cacheDigest != userToUserConfig.Hash(&m.Config.Recommend) {\n\t\treturn true\n\t}\n\n\t// check update time\n\tupdateTime, err := m.CacheClient.Get(ctx, cache.Key(cache.UserToUserUpdateTime, cache.Key(userToUserConfig.Name, userId))).Time()\n\tif err != nil {\n\t\tif !errors.Is(err, errors.NotFound) {\n\t\t\tlog.Logger().Error(\"failed to read last update user neighbors time\", zap.Error(err))\n\t\t}\n\t\treturn true\n\t}\n\treturn updateTime.Before(time.Now().Add(-m.Config.Recommend.CacheExpire))\n}\n\nfunc (m *Master) trainCollaborativeFiltering(parent context.Context, trainSet, testSet dataset.CFSplit) error {\n\tctx, span := m.tracer.Start(parent, \"Train Collaborative Filtering Model\", 2)\n\tdefer span.End()\n\n\tif trainSet.CountUsers() == 0 {\n\t\tspan.Fail(errors.New(\"No user found.\"))\n\t\treturn nil\n\t} else if trainSet.CountItems() == 0 {\n\t\tspan.Fail(errors.New(\"No item found.\"))\n\t\treturn nil\n\t} else if trainSet.CountFeedback() == 0 {\n\t\tspan.Fail(errors.New(\"No feedback found.\"))\n\t\treturn nil\n\t} else if trainSet.CountFeedback() == m.collaborativeFilteringTrainSetSize {\n\t\tlog.Logger().Info(\"collaborative filtering dataset not changed\")\n\t\treturn nil\n\t}\n\n\tm.collaborativeFilteringModelMutex.Lock()\n\tcollaborativeFilteringType := m.collaborativeFilteringMeta.Type\n\tcollaborativeFilteringParams := m.collaborativeFilteringMeta.Params\n\tif m.collaborativeFilteringTarget.Score.NDCG > 0 &&\n\t\t(!m.collaborativeFilteringTarget.Equal(m.collaborativeFilteringMeta)) &&\n\t\t(m.collaborativeFilteringTarget.Score.NDCG > m.collaborativeFilteringMeta.Score.NDCG) {\n\t\t// 1. best ranking model must have been found.\n\t\t// 2. best ranking model must be different from current model\n\t\t// 3. best ranking model must perform better than current model\n\t\tcollaborativeFilteringType = m.collaborativeFilteringTarget.Type\n\t\tcollaborativeFilteringParams = m.collaborativeFilteringTarget.Params\n\t\tlog.Logger().Info(\"find better collaborative filtering model\",\n\t\t\tzap.Any(\"score\", m.collaborativeFilteringTarget.Score),\n\t\t\tzap.String(\"type\", collaborativeFilteringType),\n\t\t\tzap.Any(\"params\", collaborativeFilteringParams))\n\t}\n\tm.collaborativeFilteringModelMutex.Unlock()\n\n\tstartFitTime := time.Now()\n\tfitCtx, fitSpan := monitor.Start(ctx, \"Fit\", 1)\n\tcollaborativeFilteringModel := m.newCollaborativeFilteringModel(collaborativeFilteringType, collaborativeFilteringParams)\n\tscore := collaborativeFilteringModel.Fit(fitCtx, trainSet, testSet,\n\t\tcf.NewFitConfig().\n\t\t\tSetJobs(m.Config.Master.NumJobs).\n\t\t\tSetPatience(m.Config.Recommend.Collaborative.EarlyStopping.Patience))\n\tCollaborativeFilteringFitSeconds.Set(time.Since(startFitTime).Seconds())\n\tspan.Add(1)\n\tfitSpan.End()\n\n\t_, indexSpan := monitor.Start(ctx, \"Index\", trainSet.CountItems())\n\tmatrixFactorizationItems := logics.NewMatrixFactorizationItems(time.Now())\n\tif err := parallel.For(ctx, trainSet.CountItems(), m.Config.Master.NumJobs, func(i int) {\n\t\tdefer indexSpan.Add(1)\n\t\tif itemId, ok := trainSet.GetItemDict().String(int32(i)); ok && collaborativeFilteringModel.IsItemPredictable(int32(i)) {\n\t\t\tmatrixFactorizationItems.Add(itemId, collaborativeFilteringModel.GetItemFactor(int32(i)))\n\t\t}\n\t}); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tspan.Add(1)\n\tindexSpan.End()\n\n\tmatrixFactorizationUsers := logics.NewMatrixFactorizationUsers()\n\tfor i := 0; i < trainSet.CountUsers(); i++ {\n\t\tif userId, ok := trainSet.GetUserDict().String(int32(i)); ok && collaborativeFilteringModel.IsUserPredictable(int32(i)) {\n\t\t\tmatrixFactorizationUsers.Add(userId, collaborativeFilteringModel.GetUserFactor(int32(i)))\n\t\t}\n\t}\n\n\t// update ranking model\n\tm.collaborativeFilteringModelMutex.Lock()\n\tm.collaborativeFilteringTrainSetSize = trainSet.CountFeedback()\n\tm.collaborativeFilteringModelMutex.Unlock()\n\tcollaborativeFilteringModelId := time.Now().UnixMilli()\n\tlog.Logger().Info(\"fit collaborative filtering model completed\",\n\t\tzap.Int64(\"id\", collaborativeFilteringModelId))\n\tCollaborativeFilteringNDCG10.Set(float64(score.NDCG))\n\tCollaborativeFilteringRecall10.Set(float64(score.Recall))\n\tCollaborativeFilteringPrecision10.Set(float64(score.Precision))\n\tif err := m.CacheClient.Set(ctx, cache.Time(cache.Key(cache.GlobalMeta, cache.LastFitMatchingModelTime), time.Now())); err != nil {\n\t\tlog.Logger().Error(\"failed to write meta\", zap.Error(err))\n\t}\n\n\t// upload model\n\tw, done, err := m.blobStore.Create(strconv.FormatInt(collaborativeFilteringModelId, 10))\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to create blob for collaborative filtering model\",\n\t\t\tzap.Int64(\"id\", collaborativeFilteringModelId), zap.Error(err))\n\t\treturn err\n\t}\n\tif err = matrixFactorizationItems.Marshal(w); err != nil {\n\t\tlog.Logger().Error(\"failed to matrix factorization items\",\n\t\t\tzap.Int64(\"id\", collaborativeFilteringModelId), zap.Error(err))\n\t\treturn err\n\t}\n\tif err = matrixFactorizationUsers.Marshal(w); err != nil {\n\t\tlog.Logger().Error(\"failed to matrix factorization users\",\n\t\t\tzap.Int64(\"id\", collaborativeFilteringModelId), zap.Error(err))\n\t\treturn err\n\t}\n\tif err = w.Close(); err != nil {\n\t\tlog.Logger().Error(\"failed to close blob for collaborative filtering model\",\n\t\t\tzap.Int64(\"id\", collaborativeFilteringModelId), zap.Error(err))\n\t\treturn err\n\t}\n\t<-done\n\n\t// update meta\n\tm.collaborativeFilteringModelMutex.Lock()\n\tm.collaborativeFilteringMeta.ID = collaborativeFilteringModelId\n\tm.collaborativeFilteringMeta.Type = collaborativeFilteringType\n\tm.collaborativeFilteringMeta.Params = collaborativeFilteringParams\n\tm.collaborativeFilteringMeta.Score = score\n\tm.collaborativeFilteringModelMutex.Unlock()\n\tif err = m.metaStore.Put(meta.COLLABORATIVE_FILTERING_MODEL, m.collaborativeFilteringMeta.ToJSON()); err != nil {\n\t\tlog.Logger().Error(\"failed to write collaborative filtering model meta\", zap.Error(err))\n\t\treturn err\n\t} else {\n\t\tlog.Logger().Info(\"write collaborative filtering model meta\",\n\t\t\tzap.Int64(\"id\", collaborativeFilteringModelId),\n\t\t\tzap.Float32(\"ndcg\", score.NDCG),\n\t\t\tzap.Float32(\"recall\", score.Recall),\n\t\t\tzap.Float32(\"precision\", score.Precision))\n\t}\n\n\t// update statistics\n\tif err = m.CacheClient.AddTimeSeriesPoints(ctx, []cache.TimeSeriesPoint{\n\t\t{Name: cache.CFNDCG, Value: float64(score.NDCG), Timestamp: time.Now()},\n\t\t{Name: cache.CFPrecision, Value: float64(score.Precision), Timestamp: time.Now()},\n\t\t{Name: cache.CFRecall, Value: float64(score.Recall), Timestamp: time.Now()},\n\t}); err != nil {\n\t\tlog.Logger().Error(\"failed to write time series points\", zap.Error(err))\n\t\treturn nil\n\t}\n\n\tm.removeOutOfDateModels()\n\treturn nil\n}\n\nfunc (m *Master) newCollaborativeFilteringModel(modelType string, params model.Params) cf.MatrixFactorization {\n\tswitch modelType {\n\tcase \"BPR\":\n\t\treturn cf.NewBPR(params)\n\tcase \"ALS\":\n\t\treturn cf.NewALS(params)\n\tdefault:\n\t\treturn cf.NewBPR(params)\n\t}\n}\n\nfunc (m *Master) trainClickThroughRatePrediction(parent context.Context, trainSet, testSet *ctr.Dataset) error {\n\tctx, span := m.tracer.Start(parent, \"Train Click-Through Rate Prediction Model\", 1)\n\tdefer span.End()\n\n\tif trainSet.CountUsers() == 0 {\n\t\tspan.Fail(errors.New(\"No user found.\"))\n\t\treturn nil\n\t} else if trainSet.CountItems() == 0 {\n\t\tspan.Fail(errors.New(\"No item found.\"))\n\t\treturn nil\n\t} else if trainSet.Count() == 0 {\n\t\tspan.Fail(errors.New(\"No feedback found.\"))\n\t\treturn nil\n\t} else if trainSet.Count() == m.clickThroughRateTrainSetSize {\n\t\tlog.Logger().Info(\"click dataset not changed\")\n\t\treturn nil\n\t}\n\n\tm.clickThroughRateModelMutex.Lock()\n\tclickThroughRateType := m.clickThroughRateMeta.Type\n\tclickThroughRateParams := m.clickThroughRateMeta.Params\n\tif m.clickThroughRateTarget.Score.AUC > 0 &&\n\t\t(!m.clickThroughRateTarget.Equal(m.clickThroughRateMeta)) &&\n\t\t(m.clickThroughRateTarget.Score.AUC > m.clickThroughRateMeta.Score.AUC) {\n\t\t// 1. best click model must have been found.\n\t\t// 2. best click model must be different from current model\n\t\t// 3. best click model must perform better than current model\n\t\tclickThroughRateType = m.clickThroughRateTarget.Type\n\t\tclickThroughRateParams = m.clickThroughRateTarget.Params\n\t\tlog.Logger().Info(\"find better click model\",\n\t\t\tzap.Float32(\"Precision\", m.clickThroughRateTarget.Score.Precision),\n\t\t\tzap.Float32(\"Recall\", m.clickThroughRateTarget.Score.Recall),\n\t\t\tzap.Any(\"params\", clickThroughRateParams))\n\t}\n\tclickModel := ctr.NewAFM(clickThroughRateParams)\n\tm.clickThroughRateModelMutex.Unlock()\n\n\tstartFitTime := time.Now()\n\tscore := clickModel.Fit(ctx, trainSet, testSet,\n\t\tctr.NewFitConfig().\n\t\t\tSetJobs(m.Config.Master.NumJobs).\n\t\t\tSetPatience(m.Config.Recommend.Ranker.EarlyStopping.Patience))\n\tRankingFitSeconds.Set(time.Since(startFitTime).Seconds())\n\n\t// update match model\n\tm.clickThroughRateModelMutex.Lock()\n\tm.clickThroughRateTrainSetSize = trainSet.Count()\n\tclickThroughRateModelId := time.Now().UnixMilli()\n\tm.clickThroughRateModelMutex.Unlock()\n\tlog.Logger().Info(\"fit click model complete\",\n\t\tzap.Int64(\"id\", clickThroughRateModelId))\n\tRankingPrecision.Set(float64(score.Precision))\n\tRankingRecall.Set(float64(score.Recall))\n\tRankingAUC.Set(float64(score.AUC))\n\tif err := m.CacheClient.Set(ctx, cache.Time(cache.Key(cache.GlobalMeta, cache.LastFitRankingModelTime), time.Now())); err != nil {\n\t\tlog.Logger().Error(\"failed to write meta\", zap.Error(err))\n\t}\n\n\t// upload model\n\tw, done, err := m.blobStore.Create(strconv.FormatInt(clickThroughRateModelId, 10))\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to create blob for click-through rate model\",\n\t\t\tzap.Int64(\"id\", clickThroughRateModelId), zap.Error(err))\n\t\treturn err\n\t}\n\tif err = ctr.MarshalModel(w, clickModel); err != nil {\n\t\tlog.Logger().Error(\"failed to marshal click-through rate model\",\n\t\t\tzap.Int64(\"id\", clickThroughRateModelId), zap.Error(err))\n\t\treturn err\n\t}\n\tif err = w.Close(); err != nil {\n\t\tlog.Logger().Error(\"failed to close blob for click-through rate model\",\n\t\t\tzap.Int64(\"id\", clickThroughRateModelId), zap.Error(err))\n\t\treturn err\n\t}\n\t<-done\n\n\t// update meta\n\tm.clickThroughRateModelMutex.Lock()\n\tm.clickThroughRateMeta.ID = clickThroughRateModelId\n\tm.clickThroughRateMeta.Type = clickThroughRateType\n\tm.clickThroughRateMeta.Params = clickThroughRateParams\n\tm.clickThroughRateMeta.Score = score\n\tm.clickThroughRateModelMutex.Unlock()\n\tif err = m.metaStore.Put(meta.CLICK_THROUGH_RATE_MODEL, m.clickThroughRateMeta.ToJSON()); err != nil {\n\t\tlog.Logger().Error(\"failed to write click-through rate model meta\", zap.Error(err))\n\t\treturn err\n\t} else {\n\t\tlog.Logger().Info(\"write click-through rate model meta\",\n\t\t\tzap.Int64(\"id\", clickThroughRateModelId),\n\t\t\tzap.Float32(\"precision\", score.Precision),\n\t\t\tzap.Float32(\"recall\", score.Recall),\n\t\t\tzap.Float32(\"auc\", score.AUC),\n\t\t\tzap.Any(\"params\", clickThroughRateParams))\n\t}\n\n\t// update statistics\n\tif err = m.CacheClient.AddTimeSeriesPoints(ctx, []cache.TimeSeriesPoint{\n\t\t{Name: cache.CTRPrecision, Value: float64(score.Precision), Timestamp: time.Now()},\n\t\t{Name: cache.CTRRecall, Value: float64(score.Recall), Timestamp: time.Now()},\n\t\t{Name: cache.CTRAUC, Value: float64(score.AUC), Timestamp: time.Now()},\n\t}); err != nil {\n\t\tlog.Logger().Error(\"failed to write time series points\", zap.Error(err))\n\t\treturn err\n\t}\n\n\tm.removeOutOfDateModels()\n\treturn nil\n}\n\nfunc (m *Master) removeOutOfDateModels() {\n\tm.collaborativeFilteringModelMutex.RLock()\n\tm.clickThroughRateModelMutex.RLock()\n\ttimestamp := min(m.collaborativeFilteringMeta.ID, m.clickThroughRateMeta.ID)\n\tm.clickThroughRateModelMutex.RUnlock()\n\tm.collaborativeFilteringModelMutex.RUnlock()\n\n\tfiles, err := m.blobStore.List()\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to list models in blob store\", zap.Error(err))\n\t\treturn\n\t}\n\tfor _, file := range files {\n\t\tid, err := strconv.ParseInt(file, 10, 64)\n\t\tif err != nil {\n\t\t\tlog.Logger().Info(\"failed to parse model id\", zap.String(\"file\", file), zap.Error(err))\n\t\t\tcontinue\n\t\t}\n\t\tif id < timestamp {\n\t\t\tif err = m.blobStore.Remove(file); err != nil {\n\t\t\t\tlog.Logger().Error(\"failed to delete model from blob store\", zap.Int64(\"id\", id), zap.Error(err))\n\t\t\t} else {\n\t\t\t\tlog.Logger().Info(\"deleted out-of-date model from blob store\", zap.Int64(\"id\", id))\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (m *Master) collectGarbage(parent context.Context, dataSet *dataset.Dataset) error {\n\tctx, span := m.tracer.Start(parent, \"Collect Garbage in Cache\", 1)\n\tdefer span.End()\n\terr := m.CacheClient.ScanScores(ctx, func(collection, id, subset string, timestamp time.Time) error {\n\t\tswitch collection {\n\t\tcase cache.NonPersonalized:\n\t\t\tif !lo.ContainsBy(m.Config.Recommend.NonPersonalized, func(cfg config.NonPersonalizedConfig) bool {\n\t\t\t\treturn cfg.Name == subset\n\t\t\t}) {\n\t\t\t\treturn m.CacheClient.DeleteScores(ctx, []string{cache.NonPersonalized}, cache.ScoreCondition{\n\t\t\t\t\tSubset: lo.ToPtr(subset),\n\t\t\t\t})\n\t\t\t}\n\t\tcase cache.UserToUser:\n\t\t\tsplits := strings.Split(subset, \"/\")\n\t\t\tif len(splits) != 2 {\n\t\t\t\tlog.Logger().Error(\"invalid subset\", zap.String(\"subset\", subset))\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tif dataSet.GetUserDict().Id(splits[1]) == dataset.NotId || !lo.ContainsBy(m.Config.Recommend.UserToUser, func(cfg config.UserToUserConfig) bool {\n\t\t\t\treturn cfg.Name == splits[0]\n\t\t\t}) {\n\t\t\t\treturn m.CacheClient.DeleteScores(ctx, []string{cache.UserToUser}, cache.ScoreCondition{\n\t\t\t\t\tSubset: lo.ToPtr(subset),\n\t\t\t\t\tBefore: lo.ToPtr(dataSet.GetTimestamp()),\n\t\t\t\t})\n\t\t\t}\n\t\tcase cache.ItemToItem:\n\t\t\tsplits := strings.Split(subset, \"/\")\n\t\t\tif len(splits) != 2 {\n\t\t\t\tlog.Logger().Error(\"invalid subset\", zap.String(\"subset\", subset))\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tif dataSet.GetItemDict().Id(splits[1]) == dataset.NotId || !lo.ContainsBy(m.Config.Recommend.ItemToItem, func(cfg config.ItemToItemConfig) bool {\n\t\t\t\treturn cfg.Name == splits[0]\n\t\t\t}) {\n\t\t\t\treturn m.CacheClient.DeleteScores(ctx, []string{cache.ItemToItem}, cache.ScoreCondition{\n\t\t\t\t\tSubset: lo.ToPtr(subset),\n\t\t\t\t\tBefore: lo.ToPtr(dataSet.GetTimestamp()),\n\t\t\t\t})\n\t\t\t}\n\t\tcase cache.CollaborativeFiltering:\n\t\t\tif dataSet.GetUserDict().Id(subset) == dataset.NotId {\n\t\t\t\treturn m.CacheClient.DeleteScores(ctx, []string{cache.CollaborativeFiltering}, cache.ScoreCondition{\n\t\t\t\t\tSubset: lo.ToPtr(subset),\n\t\t\t\t\tBefore: lo.ToPtr(dataSet.GetTimestamp()),\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\treturn errors.Trace(err)\n}\n\nfunc (m *Master) optimizeCollaborativeFiltering(parent context.Context, trainSet, testSet dataset.CFSplit) error {\n\tctx, span := m.tracer.Start(parent, \"Optimize Collaborative Filtering Model\", m.Config.Recommend.Collaborative.OptimizeTrials)\n\tdefer span.End()\n\n\tif trainSet.CountUsers() == 0 {\n\t\tspan.Fail(errors.New(\"No user found.\"))\n\t\treturn nil\n\t} else if trainSet.CountItems() == 0 {\n\t\tspan.Fail(errors.New(\"No item found.\"))\n\t\treturn nil\n\t} else if trainSet.CountFeedback() == 0 {\n\t\tspan.Fail(errors.New(\"No feedback found.\"))\n\t\treturn nil\n\t}\n\n\tsearch := cf.NewModelSearch(map[string]cf.ModelCreator{\n\t\t\"BPR\": func() cf.MatrixFactorization {\n\t\t\treturn cf.NewBPR(nil)\n\t\t},\n\t\t\"ALS\": func() cf.MatrixFactorization {\n\t\t\treturn cf.NewALS(nil)\n\t\t},\n\t}, trainSet, testSet,\n\t\tcf.NewFitConfig().\n\t\t\tSetJobs(m.Config.Master.NumJobs).\n\t\t\tSetPatience(m.Config.Recommend.Collaborative.EarlyStopping.Patience)).\n\t\tWithContext(ctx).\n\t\tWithSpan(span)\n\n\tstudy, err := goptuna.CreateStudy(\"optimizeCollaborativeFiltering\",\n\t\tgoptuna.StudyOptionDirection(goptuna.StudyDirectionMaximize),\n\t\tgoptuna.StudyOptionSampler(tpe.NewSampler()),\n\t\tgoptuna.StudyOptionLogger(log.NewOptunaLogger(log.Logger())))\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tstudy.WithContext(ctx)\n\tif err = study.Optimize(search.Objective, m.Config.Recommend.Collaborative.OptimizeTrials); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tm.collaborativeFilteringModelMutex.Lock()\n\tm.collaborativeFilteringTarget = search.Result()\n\tm.collaborativeFilteringModelMutex.Unlock()\n\tlog.Logger().Info(\"optimize collaborative filtering model completed\",\n\t\tzap.Any(\"score\", m.collaborativeFilteringTarget.Score),\n\t\tzap.String(\"type\", m.collaborativeFilteringTarget.Type),\n\t\tzap.Any(\"params\", m.collaborativeFilteringTarget.Params))\n\treturn nil\n}\n\nfunc (m *Master) optimizeClickThroughRatePrediction(parent context.Context, trainSet, testSet *ctr.Dataset) error {\n\tctx, span := m.tracer.Start(parent, \"Optimize Click-Through Rate Prediction Model\", m.Config.Recommend.Ranker.OptimizeTrials)\n\tdefer span.End()\n\n\tif trainSet.CountUsers() == 0 {\n\t\tspan.Fail(errors.New(\"No user found.\"))\n\t\treturn nil\n\t} else if trainSet.CountItems() == 0 {\n\t\tspan.Fail(errors.New(\"No item found.\"))\n\t\treturn nil\n\t} else if trainSet.Count() == 0 {\n\t\tspan.Fail(errors.New(\"No feedback found.\"))\n\t\treturn nil\n\t}\n\n\tsearch := ctr.NewModelSearch(map[string]ctr.ModelCreator{\n\t\t\"FM\": func() ctr.FactorizationMachines {\n\t\t\treturn ctr.NewAFM(nil)\n\t\t},\n\t}, trainSet, testSet,\n\t\tctr.NewFitConfig().\n\t\t\tSetJobs(m.Config.Master.NumJobs).\n\t\t\tSetPatience(m.Config.Recommend.Ranker.EarlyStopping.Patience)).\n\t\tWithContext(ctx).\n\t\tWithSpan(span)\n\n\tstudy, err := goptuna.CreateStudy(\"optimizeClickThroughRatePrediction\",\n\t\tgoptuna.StudyOptionDirection(goptuna.StudyDirectionMaximize),\n\t\tgoptuna.StudyOptionSampler(tpe.NewSampler()),\n\t\tgoptuna.StudyOptionLogger(log.NewOptunaLogger(log.Logger())))\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tstudy.WithContext(ctx)\n\tif err = study.Optimize(search.Objective, m.Config.Recommend.Ranker.OptimizeTrials); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tm.clickThroughRateModelMutex.Lock()\n\tm.clickThroughRateTarget = search.Result()\n\tm.clickThroughRateModelMutex.Unlock()\n\tlog.Logger().Info(\"optimize click-through rate model completed\",\n\t\tzap.Any(\"score\", m.clickThroughRateTarget.Score),\n\t\tzap.String(\"type\", m.clickThroughRateTarget.Type),\n\t\tzap.Any(\"params\", m.clickThroughRateTarget.Params))\n\treturn nil\n}\n\n// updateRecommend updates recommendations for all user in standalone mode.\nfunc (m *Master) updateRecommend(ctx context.Context) error {\n\tpipeline := &worker.Pipeline{\n\t\tConfig:                   m.Config,\n\t\tDataClient:               m.DataClient,\n\t\tCacheClient:              m.CacheClient,\n\t\tTracer:                   m.tracer,\n\t\tJobs:                     m.Config.Master.NumJobs,\n\t\tMatrixFactorizationItems: logics.NewMatrixFactorizationItems(time.Time{}),\n\t\tMatrixFactorizationUsers: logics.NewMatrixFactorizationUsers(),\n\t}\n\n\t// load matrix factorization model\n\tif m.collaborativeFilteringMeta.ID > 0 {\n\t\tr, err := m.blobStore.Open(strconv.FormatInt(m.collaborativeFilteringMeta.ID, 10))\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to load collaborative filtering model from blob store\",\n\t\t\t\tzap.Int64(\"id\", m.collaborativeFilteringMeta.ID), zap.Error(err))\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\tif err = pipeline.MatrixFactorizationItems.Unmarshal(r); err != nil {\n\t\t\tlog.Logger().Error(\"failed to unmarshal matrix factorization items\", zap.Error(err))\n\t\t} else if err = pipeline.MatrixFactorizationUsers.Unmarshal(r); err != nil {\n\t\t\tlog.Logger().Error(\"failed to unmarshal matrix factorization users\", zap.Error(err))\n\t\t}\n\t}\n\n\t// load click-through rate model when FM ranker is enabled\n\tif strings.EqualFold(m.Config.Recommend.Ranker.Type, \"fm\") && m.clickThroughRateMeta.ID > 0 {\n\t\tr, err := m.blobStore.Open(strconv.FormatInt(m.clickThroughRateMeta.ID, 10))\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to open click-through rate model\", zap.Error(err))\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\tpipeline.ClickThroughRateModel, err = ctr.UnmarshalModel(r)\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to unmarshal click-through rate model\", zap.Error(err))\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\n\t// Pull all users from database\n\tusers, err := m.pullAllUsers(ctx)\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to pull users\", zap.Error(err))\n\t\treturn errors.Trace(err)\n\t}\n\n\tpipeline.Recommend(ctx, users, func(completed, throughput int) {\n\t\tlog.Logger().Info(\"ranking recommendation\",\n\t\t\tzap.Int(\"n_complete_users\", completed),\n\t\t\tzap.Int(\"throughput\", throughput))\n\t})\n\treturn nil\n}\n\n// pullAllUsers pulls all users from the data store.\nfunc (m *Master) pullAllUsers(ctx context.Context) ([]data.User, error) {\n\tvar users []data.User\n\tuserChan, errChan := m.DataClient.GetUserStream(ctx, batchSize)\n\tfor batchUsers := range userChan {\n\t\tusers = append(users, batchUsers...)\n\t}\n\tif err := <-errChan; err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\treturn users, nil\n}\n"
  },
  {
    "path": "master/tasks_test.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage master\n\nimport (\n\t\"runtime\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/common/expression\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/samber/lo\"\n)\n\nfunc (s *MasterTestSuite) TestFindItemToItem() {\n\tctx := s.T().Context()\n\t// create config\n\ts.Config = &config.Config{}\n\ts.Config.Recommend.CacheSize = 3\n\ts.Config.Master.NumJobs = 4\n\t// collect similar\n\titems := []data.Item{\n\t\t{ItemId: \"0\", IsHidden: false, Categories: []string{\"*\"}, Timestamp: time.Now(), Labels: []string{\"a\", \"b\", \"c\", \"d\"}, Comment: \"\"},\n\t\t{ItemId: \"1\", IsHidden: false, Categories: []string{\"*\"}, Timestamp: time.Now(), Labels: []string{}, Comment: \"\"},\n\t\t{ItemId: \"2\", IsHidden: false, Categories: []string{\"*\"}, Timestamp: time.Now(), Labels: []string{\"b\", \"c\", \"d\"}, Comment: \"\"},\n\t\t{ItemId: \"3\", IsHidden: false, Categories: nil, Timestamp: time.Now(), Labels: []string{}, Comment: \"\"},\n\t\t{ItemId: \"4\", IsHidden: false, Categories: nil, Timestamp: time.Now(), Labels: []string{\"b\", \"c\"}, Comment: \"\"},\n\t\t{ItemId: \"5\", IsHidden: false, Categories: []string{\"*\"}, Timestamp: time.Now(), Labels: []string{}, Comment: \"\"},\n\t\t{ItemId: \"6\", IsHidden: false, Categories: []string{\"*\"}, Timestamp: time.Now(), Labels: []string{\"c\"}, Comment: \"\"},\n\t\t{ItemId: \"7\", IsHidden: false, Categories: []string{\"*\"}, Timestamp: time.Now(), Labels: []string{}, Comment: \"\"},\n\t\t{ItemId: \"8\", IsHidden: false, Categories: []string{\"*\"}, Timestamp: time.Now(), Labels: []string{\"a\", \"b\", \"c\", \"d\", \"e\"}, Comment: \"\"},\n\t\t{ItemId: \"9\", IsHidden: false, Categories: nil, Timestamp: time.Now(), Labels: []string{}, Comment: \"\"},\n\t}\n\tfeedbacks := make([]data.Feedback, 0)\n\tfor i := 0; i < 10; i++ {\n\t\tfor j := 0; j <= i; j++ {\n\t\t\tif i%2 == 1 {\n\t\t\t\tfeedbacks = append(feedbacks, data.Feedback{\n\t\t\t\t\tFeedbackKey: data.FeedbackKey{\n\t\t\t\t\t\tItemId:       strconv.Itoa(i),\n\t\t\t\t\t\tUserId:       strconv.Itoa(j),\n\t\t\t\t\t\tFeedbackType: \"FeedbackType\",\n\t\t\t\t\t},\n\t\t\t\t\tTimestamp: time.Now(),\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\tvar err error\n\terr = s.DataClient.BatchInsertItems(ctx, items)\n\ts.NoError(err)\n\terr = s.DataClient.BatchInsertFeedback(ctx, feedbacks, true, true, true)\n\ts.NoError(err)\n\n\t// insert hidden item\n\terr = s.DataClient.BatchInsertItems(ctx, []data.Item{{\n\t\tItemId:   \"10\",\n\t\tLabels:   []string{\"a\", \"b\", \"c\", \"d\", \"e\"},\n\t\tIsHidden: true,\n\t}})\n\ts.NoError(err)\n\tfor i := 0; i <= 10; i++ {\n\t\terr = s.DataClient.BatchInsertFeedback(ctx, []data.Feedback{{\n\t\t\tFeedbackKey: data.FeedbackKey{UserId: strconv.Itoa(i), ItemId: \"10\", FeedbackType: \"FeedbackType\"},\n\t\t}}, true, true, true)\n\t\ts.NoError(err)\n\t}\n\n\t// load mock dataset\n\t_, dataSet, err := s.LoadDataFromDatabase(s.T().Context(), s.DataClient,\n\t\t[]expression.FeedbackTypeExpression{expression.MustParseFeedbackTypeExpression(\"FeedbackType\")},\n\t\tnil, 0, 0, NewOnlineEvaluator(nil, nil), nil)\n\ts.NoError(err)\n\n\t// similar items (common users)\n\ts.Config.Recommend.ItemToItem = []config.ItemToItemConfig{{Name: \"default\", Type: \"users\"}}\n\ts.NoError(s.updateItemToItem(s.T().Context(), dataSet))\n\tsimilar, err := s.CacheClient.SearchScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"9\"), nil, 0, 100)\n\ts.NoError(err)\n\ts.Equal([]string{\"7\", \"5\", \"3\"}, cache.ConvertDocumentsToValues(similar))\n\t// similar items in category (common users)\n\tsimilar, err = s.CacheClient.SearchScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"9\"), []string{\"*\"}, 0, 100)\n\ts.NoError(err)\n\ts.Equal([]string{\"7\", \"5\"}, cache.ConvertDocumentsToValues(similar))\n\t// digest\n\tdigest, err := s.CacheClient.Get(ctx, cache.Key(cache.ItemToItemDigest, \"default\", \"9\")).String()\n\ts.NoError(err)\n\ts.Equal(s.Config.Recommend.ItemToItem[0].Hash(&s.Config.Recommend), digest)\n\n\t// similar items (common labels)\n\terr = s.CacheClient.Set(ctx, cache.Time(cache.Key(cache.LastModifyItemTime, \"8\"), time.Now()))\n\ts.NoError(err)\n\ts.Config.Recommend.ItemToItem = []config.ItemToItemConfig{{Name: \"default\", Type: \"tags\", Column: \"item.Labels\"}}\n\ts.NoError(s.updateItemToItem(s.T().Context(), dataSet))\n\tsimilar, err = s.CacheClient.SearchScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"8\"), nil, 0, 100)\n\ts.NoError(err)\n\ts.Equal([]string{\"0\", \"2\", \"4\"}, cache.ConvertDocumentsToValues(similar))\n\t// similar items in category (common labels)\n\tsimilar, err = s.CacheClient.SearchScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"8\"), []string{\"*\"}, 0, 100)\n\ts.NoError(err)\n\ts.Equal([]string{\"0\", \"2\"}, cache.ConvertDocumentsToValues(similar))\n\n\t// similar items (auto)\n\terr = s.CacheClient.Set(ctx, cache.Time(cache.Key(cache.LastModifyItemTime, \"8\"), time.Now()))\n\ts.NoError(err)\n\terr = s.CacheClient.Set(ctx, cache.Time(cache.Key(cache.LastModifyItemTime, \"9\"), time.Now()))\n\ts.NoError(err)\n\ts.Config.Recommend.ItemToItem = []config.ItemToItemConfig{{Name: \"default\", Type: \"auto\"}}\n\ts.NoError(s.updateItemToItem(s.T().Context(), dataSet))\n\tsimilar, err = s.CacheClient.SearchScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"8\"), nil, 0, 100)\n\ts.NoError(err)\n\ts.Equal([]string{\"0\", \"2\", \"4\"}, cache.ConvertDocumentsToValues(similar))\n\tsimilar, err = s.CacheClient.SearchScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"9\"), nil, 0, 100)\n\ts.NoError(err)\n\ts.Equal([]string{\"7\", \"5\", \"3\"}, cache.ConvertDocumentsToValues(similar))\n}\n\nfunc (s *MasterTestSuite) TestUserToUser() {\n\tctx := s.T().Context()\n\t// create config\n\ts.Config = &config.Config{}\n\ts.Config.Recommend.CacheSize = 3\n\ts.Config.Master.NumJobs = 4\n\t// collect similar\n\tusers := []data.User{\n\t\t{UserId: \"0\", Labels: []string{\"a\", \"b\", \"c\", \"d\"}, Comment: \"\"},\n\t\t{UserId: \"1\", Labels: []string{}, Comment: \"\"},\n\t\t{UserId: \"2\", Labels: []string{\"b\", \"c\", \"d\"}, Comment: \"\"},\n\t\t{UserId: \"3\", Labels: []string{}, Comment: \"\"},\n\t\t{UserId: \"4\", Labels: []string{\"b\", \"c\"}, Comment: \"\"},\n\t\t{UserId: \"5\", Labels: []string{}, Comment: \"\"},\n\t\t{UserId: \"6\", Labels: []string{\"c\"}, Comment: \"\"},\n\t\t{UserId: \"7\", Labels: []string{}, Comment: \"\"},\n\t\t{UserId: \"8\", Labels: []string{\"a\", \"b\", \"c\", \"d\", \"e\"}, Comment: \"\"},\n\t\t{UserId: \"9\", Labels: []string{}, Comment: \"\"},\n\t}\n\tfeedbacks := make([]data.Feedback, 0)\n\tfor i := 0; i < 10; i++ {\n\t\tfor j := 0; j <= i; j++ {\n\t\t\tif i%2 == 1 {\n\t\t\t\tfeedbacks = append(feedbacks, data.Feedback{\n\t\t\t\t\tFeedbackKey: data.FeedbackKey{\n\t\t\t\t\t\tItemId:       strconv.Itoa(j),\n\t\t\t\t\t\tUserId:       strconv.Itoa(i),\n\t\t\t\t\t\tFeedbackType: \"FeedbackType\",\n\t\t\t\t\t},\n\t\t\t\t\tTimestamp: time.Now(),\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\tvar err error\n\terr = s.DataClient.BatchInsertUsers(ctx, users)\n\ts.NoError(err)\n\terr = s.DataClient.BatchInsertFeedback(ctx, feedbacks, true, true, true)\n\ts.NoError(err)\n\t_, dataSet, err := s.LoadDataFromDatabase(s.T().Context(), s.DataClient,\n\t\t[]expression.FeedbackTypeExpression{expression.MustParseFeedbackTypeExpression(\"FeedbackType\")},\n\t\tnil, 0, 0, NewOnlineEvaluator(nil, nil), nil)\n\ts.NoError(err)\n\n\t// similar items (common users)\n\ts.Config.Recommend.UserToUser = []config.UserToUserConfig{{Name: \"default\", Type: \"items\"}}\n\ts.NoError(s.updateUserToUser(s.T().Context(), dataSet))\n\tsimilar, err := s.CacheClient.SearchScores(ctx, cache.UserToUser, cache.Key(\"default\", \"9\"), nil, 0, 100)\n\ts.NoError(err)\n\ts.Equal([]string{\"7\", \"5\", \"3\"}, cache.ConvertDocumentsToValues(similar))\n\tdigest, err := s.CacheClient.Get(ctx, cache.Key(cache.UserToUserDigest, \"default\", \"9\")).String()\n\ts.NoError(err)\n\ts.Equal(s.Config.Recommend.UserToUser[0].Hash(&s.Config.Recommend), digest)\n\n\t// similar items (common labels)\n\terr = s.CacheClient.Set(ctx, cache.Time(cache.Key(cache.LastModifyUserTime, \"8\"), time.Now()))\n\ts.NoError(err)\n\ts.Config.Recommend.UserToUser = []config.UserToUserConfig{{Name: \"default\", Type: \"tags\", Column: \"user.Labels\"}}\n\ts.NoError(s.updateUserToUser(s.T().Context(), dataSet))\n\tsimilar, err = s.CacheClient.SearchScores(ctx, cache.UserToUser, cache.Key(\"default\", \"8\"), nil, 0, 100)\n\ts.NoError(err)\n\ts.Equal([]string{\"0\", \"2\", \"4\"}, cache.ConvertDocumentsToValues(similar))\n\n\t// similar items (auto)\n\terr = s.CacheClient.Set(ctx, cache.Time(cache.Key(cache.LastModifyUserTime, \"8\"), time.Now()))\n\ts.NoError(err)\n\terr = s.CacheClient.Set(ctx, cache.Time(cache.Key(cache.LastModifyUserTime, \"9\"), time.Now()))\n\ts.NoError(err)\n\ts.Config.Recommend.UserToUser = []config.UserToUserConfig{{Name: \"default\", Type: \"auto\"}}\n\ts.NoError(s.updateUserToUser(s.T().Context(), dataSet))\n\tsimilar, err = s.CacheClient.SearchScores(ctx, cache.UserToUser, cache.Key(\"default\", \"8\"), nil, 0, 100)\n\ts.NoError(err)\n\ts.Equal([]string{\"0\", \"2\", \"4\"}, cache.ConvertDocumentsToValues(similar))\n\tsimilar, err = s.CacheClient.SearchScores(ctx, cache.UserToUser, cache.Key(\"default\", \"9\"), nil, 0, 100)\n\ts.NoError(err)\n\ts.Equal([]string{\"7\", \"5\", \"3\"}, cache.ConvertDocumentsToValues(similar))\n}\n\nfunc (s *MasterTestSuite) TestLoadDataFromDatabase() {\n\tctx := s.T().Context()\n\t// create config\n\ts.Config = &config.Config{}\n\ts.Config.Recommend.CacheSize = 3\n\ts.Config.Recommend.DataSource.PositiveFeedbackTypes = []expression.FeedbackTypeExpression{\n\t\texpression.MustParseFeedbackTypeExpression(\"positive\")}\n\ts.Config.Recommend.DataSource.ReadFeedbackTypes = []expression.FeedbackTypeExpression{\n\t\texpression.MustParseFeedbackTypeExpression(\"negative\")}\n\ts.Config.Master.NumJobs = runtime.NumCPU()\n\n\t// insert items\n\tvar items []data.Item\n\tfor i := 0; i < 9; i++ {\n\t\titems = append(items, data.Item{\n\t\t\tItemId:     strconv.Itoa(i),\n\t\t\tTimestamp:  time.Date(2000+i, 1, 1, 1, 1, 0, 0, time.UTC),\n\t\t\tLabels:     []any{strconv.Itoa(i % 3), strconv.Itoa(i*10 + 10)},\n\t\t\tCategories: []string{strconv.Itoa(i % 3)},\n\t\t})\n\t}\n\terr := s.DataClient.BatchInsertItems(ctx, items)\n\ts.NoError(err)\n\terr = s.DataClient.BatchInsertItems(ctx, []data.Item{{\n\t\tItemId:    \"9\",\n\t\tTimestamp: time.Date(2020, 1, 1, 1, 1, 0, 0, time.UTC),\n\t\tIsHidden:  true,\n\t}})\n\ts.NoError(err)\n\n\t// insert users\n\tvar users []data.User\n\tfor i := 0; i <= 10; i++ {\n\t\tusers = append(users, data.User{\n\t\t\tUserId: strconv.Itoa(i),\n\t\t\tLabels: []string{strconv.Itoa(i % 5), strconv.Itoa(i*10 + 10)},\n\t\t})\n\t}\n\terr = s.DataClient.BatchInsertUsers(ctx, users)\n\ts.NoError(err)\n\n\t// insert feedback\n\tfeedbacks := make([]data.Feedback, 0)\n\tfor i := 0; i < 10; i++ {\n\t\t// positive feedback\n\t\t// item 0: user 0\n\t\t// ...\n\t\t// item 9: user 0 ... user 9\n\t\tfor j := 0; j <= i; j++ {\n\t\t\tfeedbacks = append(feedbacks, data.Feedback{\n\t\t\t\tFeedbackKey: data.FeedbackKey{\n\t\t\t\t\tItemId:       strconv.Itoa(i),\n\t\t\t\t\tUserId:       strconv.Itoa(j),\n\t\t\t\t\tFeedbackType: \"positive\",\n\t\t\t\t},\n\t\t\t\tTimestamp: time.Now(),\n\t\t\t})\n\t\t}\n\t\t// negative feedback\n\t\t// item 0: user 1 .. user 10\n\t\t// ...\n\t\t// item 9: user 10\n\t\tfor j := i + 1; j < 11; j++ {\n\t\t\tfeedbacks = append(feedbacks, data.Feedback{\n\t\t\t\tFeedbackKey: data.FeedbackKey{\n\t\t\t\t\tItemId:       strconv.Itoa(i),\n\t\t\t\t\tUserId:       strconv.Itoa(j),\n\t\t\t\t\tFeedbackType: \"negative\",\n\t\t\t\t},\n\t\t\t\tTimestamp: time.Now(),\n\t\t\t})\n\t\t}\n\t}\n\terr = s.DataClient.BatchInsertFeedback(ctx, feedbacks, false, false, true)\n\ts.NoError(err)\n\n\t// load dataset\n\tdatasets, err := s.loadDataset(ctx)\n\ts.NoError(err)\n\ts.Equal(11, datasets.rankingTrainSet.CountUsers())\n\ts.Equal(10, datasets.rankingTrainSet.CountItems())\n\ts.Equal(11, datasets.rankingTestSet.CountUsers())\n\ts.Equal(10, datasets.rankingTestSet.CountItems())\n\ts.Equal(55, datasets.rankingTrainSet.CountFeedback()+datasets.rankingTestSet.CountFeedback())\n\ts.Equal(11, datasets.clickTrainSet.CountUsers())\n\ts.Equal(10, datasets.clickTrainSet.CountItems())\n\ts.Equal(11, datasets.clickTestSet.CountUsers())\n\ts.Equal(10, datasets.clickTestSet.CountItems())\n\ts.Equal(int32(3), datasets.clickTrainSet.Index.CountItemLabels())\n\ts.Equal(int32(5), datasets.clickTrainSet.Index.CountUserLabels())\n\ts.Equal(int32(3), datasets.clickTestSet.Index.CountItemLabels())\n\ts.Equal(int32(5), datasets.clickTestSet.Index.CountUserLabels())\n\ts.Equal(110, datasets.clickTrainSet.Count()+datasets.clickTestSet.Count())\n\ts.Equal(55, datasets.clickTrainSet.PositiveCount+datasets.clickTestSet.PositiveCount)\n\ts.Equal(55, datasets.clickTrainSet.NegativeCount+datasets.clickTestSet.NegativeCount)\n\n\t// check latest items\n\tlatest, err := s.DataClient.GetLatestItems(ctx, 3, nil)\n\ts.NoError(err)\n\ts.Equal([]data.Item{\n\t\titems[8],\n\t\titems[7],\n\t\titems[6],\n\t}, latest)\n\tlatest, err = s.DataClient.GetLatestItems(ctx, 3, []string{\"2\"})\n\ts.NoError(err)\n\ts.Equal([]data.Item{\n\t\titems[8],\n\t\titems[5],\n\t\titems[2],\n\t}, latest)\n\n\t// check categories\n\tcategoryScores, err := s.CacheClient.SearchScores(ctx, cache.ItemCategories, \"\", nil, 0, -1)\n\ts.NoError(err)\n\tcategories := make([]string, len(categoryScores))\n\tfor i, score := range categoryScores {\n\t\tcategories[i] = score.Id\n\t}\n\ts.Equal([]string{\"0\", \"1\", \"2\"}, categories)\n}\n\nfunc (s *MasterTestSuite) TestNonPersonalizedRecommend() {\n\tctx := s.T().Context()\n\t// create config\n\ts.Config = &config.Config{}\n\ts.Config.Recommend.CacheSize = 3\n\ts.Config.Recommend.DataSource.PositiveFeedbackTypes = []expression.FeedbackTypeExpression{\n\t\texpression.MustParseFeedbackTypeExpression(\"positive\")}\n\ts.Config.Recommend.DataSource.ReadFeedbackTypes = []expression.FeedbackTypeExpression{\n\t\texpression.MustParseFeedbackTypeExpression(\"negative\")}\n\ts.Config.Recommend.NonPersonalized = []config.NonPersonalizedConfig{{Name: \"latest\", Score: \"item.Timestamp.Unix()\"}}\n\ts.Config.Master.NumJobs = runtime.NumCPU()\n\n\t// insert items\n\tvar items []data.Item\n\tfor i := 0; i < 10; i++ {\n\t\titems = append(items, data.Item{\n\t\t\tItemId:    strconv.Itoa(i),\n\t\t\tTimestamp: time.Date(2000+i%2, 1, 1, i, 1, 0, 0, time.UTC),\n\t\t})\n\t}\n\terr := s.DataClient.BatchInsertItems(ctx, items)\n\ts.NoError(err)\n\n\t// insert users\n\tvar users []data.User\n\tfor i := 0; i < 10; i++ {\n\t\tusers = append(users, data.User{\n\t\t\tUserId: strconv.Itoa(i),\n\t\t})\n\t}\n\terr = s.DataClient.BatchInsertUsers(ctx, users)\n\ts.NoError(err)\n\n\t// insert feedback\n\tfeedbacks := make([]data.Feedback, 0)\n\tfor i := 0; i < 10; i++ {\n\t\t// positive feedback\n\t\t// item 0: user 0\n\t\t// ...\n\t\t// item 8: user 0 ... user 8\n\t\tif i%2 == 0 {\n\t\t\tfor j := 0; j <= i; j++ {\n\t\t\t\tfeedbacks = append(feedbacks, data.Feedback{\n\t\t\t\t\tFeedbackKey: data.FeedbackKey{\n\t\t\t\t\t\tItemId:       strconv.Itoa(i),\n\t\t\t\t\t\tUserId:       strconv.Itoa(j),\n\t\t\t\t\t\tFeedbackType: \"positive\",\n\t\t\t\t\t},\n\t\t\t\t\tTimestamp: time.Now(),\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\terr = s.DataClient.BatchInsertFeedback(ctx, feedbacks, false, false, true)\n\ts.NoError(err)\n\n\t// load dataset\n\t_, err = s.loadDataset(ctx)\n\ts.NoError(err)\n\n\t// check latest items\n\tlatest, err := s.CacheClient.SearchScores(ctx, cache.NonPersonalized, \"latest\", []string{\"\"}, 0, 3)\n\ts.NoError(err)\n\ts.Equal([]cache.Score{\n\t\t{Id: items[9].ItemId, Score: float64(items[9].Timestamp.Unix())},\n\t\t{Id: items[7].ItemId, Score: float64(items[7].Timestamp.Unix())},\n\t\t{Id: items[5].ItemId, Score: float64(items[5].Timestamp.Unix())},\n\t}, lo.Map(latest, func(document cache.Score, _ int) cache.Score {\n\t\treturn cache.Score{Id: document.Id, Score: document.Score}\n\t}))\n\n\t// check digest\n\tdigest, err := s.CacheClient.Get(ctx, cache.Key(cache.NonPersonalizedDigest, \"latest\")).String()\n\ts.NoError(err)\n\ts.Equal(s.Config.Recommend.NonPersonalized[0].Hash(), digest)\n}\n\nfunc (s *MasterTestSuite) TestNeedUpdateItemToItem() {\n\ts.Config = config.GetDefaultConfig()\n\trecommendConfig := config.ItemToItemConfig{Name: \"default\"}\n\tctx := s.T().Context()\n\n\t// empty cache\n\ts.True(s.needUpdateItemToItem(ctx, \"1\", recommendConfig))\n\terr := s.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"1\"), []cache.Score{\n\t\t{Id: \"2\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"3\", Score: 2, Categories: []string{\"\"}},\n\t\t{Id: \"4\", Score: 3, Categories: []string{\"\"}},\n\t})\n\ts.NoError(err)\n\n\t// digest mismatch\n\terr = s.CacheClient.Set(ctx, cache.String(cache.Key(cache.ItemToItemDigest, \"default\", \"1\"), \"digest\"))\n\ts.NoError(err)\n\ts.True(s.needUpdateItemToItem(ctx, \"1\", recommendConfig))\n\n\t// staled cache\n\terr = s.CacheClient.Set(ctx, cache.String(cache.Key(cache.ItemToItemDigest, \"default\", \"1\"), recommendConfig.Hash(&s.Config.Recommend)))\n\ts.NoError(err)\n\ts.True(s.needUpdateItemToItem(ctx, \"1\", recommendConfig))\n\terr = s.CacheClient.Set(ctx, cache.Time(cache.Key(cache.ItemToItemUpdateTime, \"default\", \"1\"), time.Now().Add(-s.Config.Recommend.CacheExpire)))\n\ts.NoError(err)\n\ts.True(s.needUpdateItemToItem(ctx, \"1\", recommendConfig))\n\n\t// not staled cache\n\terr = s.CacheClient.Set(ctx, cache.Time(cache.Key(cache.ItemToItemUpdateTime, \"default\", \"1\"), time.Now()))\n\ts.NoError(err)\n\ts.False(s.needUpdateItemToItem(ctx, \"1\", recommendConfig))\n}\n\nfunc (s *MasterTestSuite) TestNeedUpdateUserToUser() {\n\tctx := s.T().Context()\n\ts.Config = config.GetDefaultConfig()\n\trecommendConfig := config.UserToUserConfig{Name: \"default\"}\n\n\t// empty cache\n\ts.True(s.needUpdateUserToUser(ctx, \"1\", recommendConfig))\n\terr := s.CacheClient.AddScores(ctx, cache.UserToUser, cache.Key(\"default\", \"1\"), []cache.Score{\n\t\t{Id: \"1\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"2\", Score: 2, Categories: []string{\"\"}},\n\t\t{Id: \"3\", Score: 3, Categories: []string{\"\"}},\n\t})\n\ts.NoError(err)\n\n\t// digest mismatch\n\terr = s.CacheClient.Set(ctx, cache.String(cache.Key(cache.UserToUserDigest, \"default\", \"1\"), \"digest\"))\n\ts.NoError(err)\n\ts.True(s.needUpdateUserToUser(ctx, \"1\", recommendConfig))\n\n\t// staled cache\n\terr = s.CacheClient.Set(ctx, cache.String(cache.Key(cache.UserToUserDigest, \"default\", \"1\"), recommendConfig.Hash(&s.Config.Recommend)))\n\ts.NoError(err)\n\ts.True(s.needUpdateUserToUser(ctx, \"1\", recommendConfig))\n\terr = s.CacheClient.Set(ctx, cache.Time(cache.Key(cache.UserToUserUpdateTime, \"default\", \"1\"), time.Now().Add(-s.Config.Recommend.CacheExpire)))\n\ts.NoError(err)\n\ts.True(s.needUpdateUserToUser(ctx, \"1\", recommendConfig))\n\n\t// not staled cache\n\terr = s.CacheClient.Set(ctx, cache.Time(cache.Key(cache.UserToUserUpdateTime, \"default\", \"1\"), time.Now()))\n\ts.NoError(err)\n\ts.False(s.needUpdateUserToUser(ctx, \"1\", recommendConfig))\n}\n\nfunc (s *MasterTestSuite) TestGarbageCollection() {\n\t// create config\n\ts.Config = &config.Config{}\n\ts.Config.Master.NumJobs = 1\n\ts.Config.Recommend.NonPersonalized = []config.NonPersonalizedConfig{{Name: \"custom\", Score: \"1\"}}\n\ts.Config.Recommend.ItemToItem = []config.ItemToItemConfig{{Name: \"default\", Type: \"users\"}}\n\ts.Config.Recommend.UserToUser = []config.UserToUserConfig{{Name: \"default\", Type: \"items\"}}\n\n\t// insert items\n\tctx := s.T().Context()\n\terr := s.DataClient.BatchInsertItems(ctx, []data.Item{\n\t\t{ItemId: \"1\", Timestamp: time.Now(), Categories: []string{\"*\"}, Labels: []string{\"a\", \"b\", \"c\", \"d\"}, Comment: \"\"},\n\t\t{ItemId: \"2\", Timestamp: time.Now(), Categories: []string{\"*\"}, Labels: []string{}, Comment: \"\"},\n\t})\n\ts.NoError(err)\n\n\t// insert users\n\terr = s.DataClient.BatchInsertUsers(ctx, []data.User{\n\t\t{UserId: \"1\", Labels: []string{\"a\", \"b\", \"c\", \"d\"}, Comment: \"\"},\n\t\t{UserId: \"2\", Labels: []string{}, Comment: \"\"},\n\t})\n\ts.NoError(err)\n\n\t// insert non-personalized cache\n\ttimestamp := time.Now().Add(time.Hour)\n\terr = s.CacheClient.AddScores(ctx, cache.NonPersonalized, \"custom\", []cache.Score{\n\t\t{Id: \"1\", Score: 1, Categories: []string{\"\"}, Timestamp: timestamp},\n\t\t{Id: \"2\", Score: 2, Categories: []string{\"\"}, Timestamp: timestamp},\n\t})\n\ts.NoError(err)\n\terr = s.CacheClient.AddScores(ctx, cache.NonPersonalized, \"unknown\", []cache.Score{\n\t\t{Id: \"1\", Score: 1, Categories: []string{\"\"}, Timestamp: timestamp},\n\t\t{Id: \"2\", Score: 2, Categories: []string{\"\"}, Timestamp: timestamp},\n\t})\n\ts.NoError(err)\n\n\t// insert item-to-item cache\n\terr = s.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"1\"), []cache.Score{\n\t\t{Id: \"1\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"2\", Score: 2, Categories: []string{\"\"}},\n\t})\n\ts.NoError(err)\n\terr = s.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"3\"), []cache.Score{\n\t\t{Id: \"1\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"2\", Score: 2, Categories: []string{\"\"}},\n\t})\n\ts.NoError(err)\n\terr = s.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"unknown\", \"1\"), []cache.Score{\n\t\t{Id: \"1\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"2\", Score: 2, Categories: []string{\"\"}},\n\t})\n\ts.NoError(err)\n\n\t// insert user-to-user cache\n\terr = s.CacheClient.AddScores(ctx, cache.UserToUser, cache.Key(\"default\", \"1\"), []cache.Score{\n\t\t{Id: \"1\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"2\", Score: 2, Categories: []string{\"\"}},\n\t})\n\ts.NoError(err)\n\terr = s.CacheClient.AddScores(ctx, cache.UserToUser, cache.Key(\"default\", \"3\"), []cache.Score{\n\t\t{Id: \"1\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"2\", Score: 2, Categories: []string{\"\"}},\n\t})\n\ts.NoError(err)\n\terr = s.CacheClient.AddScores(ctx, cache.UserToUser, cache.Key(\"unknown\", \"1\"), []cache.Score{\n\t\t{Id: \"1\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"2\", Score: 2, Categories: []string{\"\"}},\n\t})\n\ts.NoError(err)\n\n\t// insert collaborative filtering cache\n\terr = s.CacheClient.AddScores(ctx, cache.CollaborativeFiltering, \"1\", []cache.Score{\n\t\t{Id: \"1\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"2\", Score: 2, Categories: []string{\"\"}},\n\t})\n\ts.NoError(err)\n\terr = s.CacheClient.AddScores(ctx, cache.CollaborativeFiltering, \"3\", []cache.Score{\n\t\t{Id: \"1\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"2\", Score: 2, Categories: []string{\"\"}},\n\t})\n\ts.NoError(err)\n\n\t// load dataset and run garbage collection\n\tdatasets, err := s.loadDataset(ctx)\n\ts.NoError(err)\n\terr = s.collectGarbage(ctx, datasets.rankingDataset)\n\ts.NoError(err)\n\n\t// check non-personalized cache\n\tnp, err := s.CacheClient.SearchScores(ctx, cache.NonPersonalized, \"custom\", nil, 0, 100)\n\ts.NoError(err)\n\ts.Equal([]string{\"2\", \"1\"}, cache.ConvertDocumentsToValues(np))\n\tnp, err = s.CacheClient.SearchScores(ctx, cache.NonPersonalized, \"unknown\", nil, 0, 100)\n\ts.NoError(err)\n\ts.Empty(np)\n\n\t// check item-to-item cache\n\tsimilar, err := s.CacheClient.SearchScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"1\"), nil, 0, 100)\n\ts.NoError(err)\n\ts.Equal([]string{\"2\", \"1\"}, cache.ConvertDocumentsToValues(similar))\n\tsimilar, err = s.CacheClient.SearchScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"3\"), nil, 0, 100)\n\ts.NoError(err)\n\ts.Empty(similar)\n\tsimilar, err = s.CacheClient.SearchScores(ctx, cache.ItemToItem, cache.Key(\"unknown\", \"1\"), nil, 0, 100)\n\ts.NoError(err)\n\ts.Empty(similar)\n\n\t// check user-to-user cache\n\tsimilar, err = s.CacheClient.SearchScores(ctx, cache.UserToUser, cache.Key(\"default\", \"1\"), nil, 0, 100)\n\ts.NoError(err)\n\ts.Equal([]string{\"2\", \"1\"}, cache.ConvertDocumentsToValues(similar))\n\tsimilar, err = s.CacheClient.SearchScores(ctx, cache.UserToUser, cache.Key(\"default\", \"3\"), nil, 0, 100)\n\ts.NoError(err)\n\ts.Empty(similar)\n\tsimilar, err = s.CacheClient.SearchScores(ctx, cache.UserToUser, cache.Key(\"unknown\", \"1\"), nil, 0, 100)\n\ts.NoError(err)\n\ts.Empty(similar)\n\n\t// check collaborative filtering cache\n\tcf, err := s.CacheClient.SearchScores(ctx, cache.CollaborativeFiltering, \"1\", nil, 0, 100)\n\ts.NoError(err)\n\ts.Equal([]string{\"2\", \"1\"}, cache.ConvertDocumentsToValues(cf))\n\tcf, err = s.CacheClient.SearchScores(ctx, cache.CollaborativeFiltering, \"3\", nil, 0, 100)\n\ts.NoError(err)\n\ts.Empty(cf)\n}\n"
  },
  {
    "path": "model/built_in.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage model\n\nimport (\n\t\"archive/zip\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/user\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"go.uber.org/zap\"\n)\n\ntype DatasetFormat int\n\nconst (\n\tFormatNCF DatasetFormat = iota\n\tFormatLibFM\n)\n\n// Built-in Data set\ntype _BuiltInDataSet struct {\n\tdownloadURL string\n\ttrainFile   string\n\ttestFile    string\n\tformat      DatasetFormat\n}\n\nvar builtInDataSets = map[string]_BuiltInDataSet{\n\t\"pinterest-20\": {\n\t\tdownloadURL: \"https://cdn.gorse.io/datasets/pinterest-20.zip\",\n\t\ttrainFile:   \"pinterest-20/train.txt\",\n\t\ttestFile:    \"pinterest-20/test.txt\",\n\t\tformat:      FormatNCF,\n\t},\n\t\"ml-100k\": {\n\t\tdownloadURL: \"https://cdn.gorse.io/datasets/ml-100k.zip\",\n\t\ttrainFile:   \"ml-100k/train.txt\",\n\t\ttestFile:    \"ml-100k/test.txt\",\n\t\tformat:      FormatNCF,\n\t},\n\t\"ml-1m\": {\n\t\tdownloadURL: \"https://cdn.gorse.io/datasets/ml-1m.zip\",\n\t\ttrainFile:   \"ml-1m/train.txt\",\n\t\ttestFile:    \"ml-1m/test.txt\",\n\t\tformat:      FormatNCF,\n\t},\n\t\"ml-tag\": {\n\t\tdownloadURL: \"https://cdn.gorse.io/datasets/ml-tag.zip\",\n\t\ttrainFile:   \"ml-tag/train.libfm\",\n\t\ttestFile:    \"ml-tag/test.libfm\",\n\t\tformat:      FormatLibFM,\n\t},\n\t\"frappe\": {\n\t\tdownloadURL: \"https://cdn.gorse.io/datasets/frappe.zip\",\n\t\ttrainFile:   \"frappe/train.libfm\",\n\t\ttestFile:    \"frappe/test.libfm\",\n\t\tformat:      FormatLibFM,\n\t},\n\t\"criteo\": {\n\t\tdownloadURL: \"https://cdn.gorse.io/datasets/criteo.zip\",\n\t\ttrainFile:   \"criteo/train.libfm\",\n\t\ttestFile:    \"criteo/test.libfm\",\n\t\tformat:      FormatLibFM,\n\t},\n}\n\n// The Data directories\nvar (\n\tGorseDir   string\n\tDataSetDir string\n\tTempDir    string\n)\n\nfunc init() {\n\tusr, err := user.Current()\n\tif err != nil {\n\t\tlog.Logger().Fatal(\"failed to get user directory\", zap.Error(err))\n\t}\n\n\tGorseDir = usr.HomeDir + \"/.gorse\"\n\tDataSetDir = GorseDir + \"/dataset\"\n\tTempDir = GorseDir + \"/temp\"\n\n\t// create all folders\n\tif err = os.MkdirAll(DataSetDir, os.ModePerm); err != nil {\n\t\tlog.Logger().Fatal(\"failed to create directory\", zap.Error(err), zap.String(\"path\", DataSetDir))\n\t}\n\tif err = os.MkdirAll(TempDir, os.ModePerm); err != nil {\n\t\tlog.Logger().Fatal(\"failed to create directory\", zap.Error(err), zap.String(\"path\", TempDir))\n\t}\n}\n\nfunc LocateBuiltInDataset(name string, format DatasetFormat) (string, string, error) {\n\t// Extract Data set information\n\tdataSet, exist := builtInDataSets[name]\n\tif !exist {\n\t\treturn \"\", \"\", fmt.Errorf(\"no such dataset %v\", name)\n\t}\n\tif dataSet.format != format {\n\t\treturn \"\", \"\", fmt.Errorf(\"format not matchs %v != %v\", format, dataSet.format)\n\t}\n\t// Download if not exists\n\ttrainFilePah := filepath.Join(DataSetDir, dataSet.trainFile)\n\ttestFilePath := filepath.Join(DataSetDir, dataSet.testFile)\n\tif _, err := os.Stat(trainFilePah); os.IsNotExist(err) {\n\t\tzipFileName, _ := downloadFromUrl(dataSet.downloadURL, TempDir)\n\t\tif _, err := unzip(zipFileName, DataSetDir); err != nil {\n\t\t\treturn \"\", \"\", err\n\t\t}\n\t}\n\treturn trainFilePah, testFilePath, nil\n}\n\n// downloadFromUrl downloads file from URL.\nfunc downloadFromUrl(src, dst string) (string, error) {\n\tlog.Logger().Info(\"Download dataset\", zap.String(\"source\", src))\n\t// Extract file name\n\ttokens := strings.Split(src, \"/\")\n\tfileName := filepath.Join(dst, tokens[len(tokens)-1])\n\t// Create file\n\tif err := os.MkdirAll(filepath.Dir(fileName), os.ModePerm); err != nil {\n\t\treturn fileName, err\n\t}\n\toutput, err := os.Create(fileName)\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to create file\", zap.Error(err), zap.String(\"filename\", fileName))\n\t\treturn fileName, err\n\t}\n\tdefer output.Close()\n\t// Download file\n\tresponse, err := http.Get(src)\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to download\", zap.Error(err), zap.String(\"source\", src))\n\t\treturn fileName, err\n\t}\n\tdefer response.Body.Close()\n\t// Save file\n\t_, err = io.Copy(output, response.Body)\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to download\", zap.Error(err), zap.String(\"source\", src))\n\t\treturn fileName, err\n\t}\n\treturn fileName, nil\n}\n\n// unzip zip file.\nfunc unzip(src, dst string) ([]string, error) {\n\tvar fileNames []string\n\t// Open zip file\n\tr, err := zip.OpenReader(src)\n\tif err != nil {\n\t\treturn fileNames, err\n\t}\n\tdefer r.Close()\n\t// Extract files\n\tfor _, f := range r.File {\n\t\t// Open file\n\t\trc, err := f.Open()\n\t\tif err != nil {\n\t\t\treturn fileNames, err\n\t\t}\n\t\t// Store filename/path for returning and using later on\n\t\tfilePath := filepath.Join(dst, f.Name)\n\t\t// Check for ZipSlip. More Info: http://bit.ly/2MsjAWE\n\t\tif !strings.HasPrefix(filePath, filepath.Clean(dst)+string(os.PathSeparator)) {\n\t\t\treturn fileNames, fmt.Errorf(\"%s: illegal file path\", filePath)\n\t\t}\n\t\t// Add filename\n\t\tfileNames = append(fileNames, filePath)\n\t\tif f.FileInfo().IsDir() {\n\t\t\t// Create folder\n\t\t\tif err = os.MkdirAll(filePath, os.ModePerm); err != nil {\n\t\t\t\treturn fileNames, err\n\t\t\t}\n\t\t} else {\n\t\t\t// Create all folders\n\t\t\tif err = os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {\n\t\t\t\treturn fileNames, err\n\t\t\t}\n\t\t\t// Create file\n\t\t\toutFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())\n\t\t\tif err != nil {\n\t\t\t\treturn fileNames, err\n\t\t\t}\n\t\t\t// Save file\n\t\t\t_, err = io.Copy(outFile, rc)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\t// Close the file without defer to close before next iteration of loop\n\t\t\terr = outFile.Close()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\t// Close file\n\t\terr = rc.Close()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn fileNames, nil\n}\n"
  },
  {
    "path": "model/built_in_test.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage model\n\nimport (\n\t\"github.com/stretchr/testify/assert\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc TestUnzip(t *testing.T) {\n\t// Download\n\tzipName, err := downloadFromUrl(\"https://cdn.gorse.io/datasets/yelp.zip\", os.TempDir())\n\tassert.Nil(t, err, \"download file failed \")\n\t// Extract files\n\tfileNames, err := unzip(zipName, DataSetDir)\n\t// Check\n\tassert.Nil(t, err, \"unzip file failed \")\n\tassert.Equal(t, 2, len(fileNames), \"Number of file doesn't match\")\n}\n\nfunc TestLocateBuiltInDataset(t *testing.T) {\n\ttrainFilePath, testFilePath, err := LocateBuiltInDataset(\"ml-1m\", FormatNCF)\n\tassert.NoError(t, err)\n\tassert.Equal(t, filepath.Join(DataSetDir, \"ml-1m\", \"train.txt\"), trainFilePath)\n\tassert.Equal(t, filepath.Join(DataSetDir, \"ml-1m\", \"test.txt\"), testFilePath)\n}\n"
  },
  {
    "path": "model/cf/evaluator.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cf\n\nimport (\n\t\"context\"\n\n\t\"github.com/chewxy/math32\"\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/gorse-io/gorse/common/floats\"\n\t\"github.com/gorse-io/gorse/common/heap\"\n\t\"github.com/gorse-io/gorse/common/parallel\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/samber/lo\"\n)\n\n/* Evaluate Item Ranking */\n\n// Metric is used by evaluators in personalized ranking tasks.\ntype Metric func(targetSet mapset.Set[int32], rankList []int32) float32\n\n// Evaluate evaluates a model in top-n tasks.\nfunc Evaluate(estimator MatrixFactorization, testSet, trainSet dataset.CFSplit, topK, numCandidates, nJobs int, scorers ...Metric) []float32 {\n\tpartSum := make([][]float32, nJobs)\n\tpartCount := make([]float32, nJobs)\n\tfor i := 0; i < nJobs; i++ {\n\t\tpartSum[i] = make([]float32, len(scorers))\n\t}\n\t//rng := NewRandomGenerator(0)\n\t// For all UserFeedback\n\tnegatives := testSet.SampleUserNegatives(trainSet, numCandidates)\n\t_ = parallel.Parallel(context.Background(), testSet.CountUsers(), nJobs, func(workerId, userIndex int) error {\n\t\t// Find top-n ItemFeedback in test set\n\t\ttargetSet := mapset.NewSet(testSet.GetUserFeedback()[userIndex]...)\n\t\tif targetSet.Cardinality() > 0 {\n\t\t\t// Sample negative samples\n\t\t\t//userTrainSet := NewSet(trainSet.UserFeedback[userIndex])\n\t\t\tnegativeSample := negatives[userIndex]\n\t\t\tcandidates := make([]int32, 0, targetSet.Cardinality()+len(negativeSample))\n\t\t\tcandidates = append(candidates, testSet.GetUserFeedback()[userIndex]...)\n\t\t\tcandidates = append(candidates, negativeSample...)\n\t\t\t// Find top-n ItemFeedback in predictions\n\t\t\trankList := Rank(estimator, int32(userIndex), candidates, topK)\n\t\t\tpartCount[workerId]++\n\t\t\tfor i, metric := range scorers {\n\t\t\t\tpartSum[workerId][i] += metric(targetSet, rankList)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\tsum := make([]float32, len(scorers))\n\tfor i := 0; i < nJobs; i++ {\n\t\tfor j := range partSum[i] {\n\t\t\tsum[j] += partSum[i][j]\n\t\t}\n\t}\n\tcount := lo.Sum(partCount)\n\tfloats.MulConst(sum, 1/count)\n\treturn sum\n}\n\n// NDCG means Normalized Discounted Cumulative Gain.\nfunc NDCG(targetSet mapset.Set[int32], rankList []int32) float32 {\n\t// IDCG = \\sum^{|REL|}_{i=1} \\frac {1} {\\log_2(i+1)}\n\tidcg := float32(0)\n\tfor i := 0; i < targetSet.Cardinality() && i < len(rankList); i++ {\n\t\tidcg += 1.0 / math32.Log2(float32(i)+2.0)\n\t}\n\t// DCG = \\sum^{N}_{i=1} \\frac {2^{rel_i}-1} {\\log_2(i+1)}\n\tdcg := float32(0)\n\tfor i, itemId := range rankList {\n\t\tif targetSet.Contains(itemId) {\n\t\t\tdcg += 1.0 / math32.Log2(float32(i)+2.0)\n\t\t}\n\t}\n\treturn dcg / idcg\n}\n\n// Precision is the fraction of relevant ItemFeedback among the recommended ItemFeedback.\n//\n//\t\\frac{|relevant documents| \\cap |retrieved documents|} {|{retrieved documents}|}\nfunc Precision(targetSet mapset.Set[int32], rankList []int32) float32 {\n\thit := float32(0)\n\tfor _, itemId := range rankList {\n\t\tif targetSet.Contains(itemId) {\n\t\t\thit++\n\t\t}\n\t}\n\treturn hit / float32(len(rankList))\n}\n\n// Recall is the fraction of relevant ItemFeedback that have been recommended over the total\n// amount of relevant ItemFeedback.\n//\n//\t\\frac{|relevant documents| \\cap |retrieved documents|} {|{relevant documents}|}\nfunc Recall(targetSet mapset.Set[int32], rankList []int32) float32 {\n\thit := 0\n\tfor _, itemId := range rankList {\n\t\tif targetSet.Contains(itemId) {\n\t\t\thit++\n\t\t}\n\t}\n\treturn float32(hit) / float32(targetSet.Cardinality())\n}\n\n// HR means Hit Ratio.\nfunc HR(targetSet mapset.Set[int32], rankList []int32) float32 {\n\tfor _, itemId := range rankList {\n\t\tif targetSet.Contains(itemId) {\n\t\t\treturn 1\n\t\t}\n\t}\n\treturn 0\n}\n\n// MAP means Mean Average Precision.\n// mAP: http://sdsawtelle.github.io/blog/output/mean-average-precision-MAP-for-recommender-systems.html\nfunc MAP(targetSet mapset.Set[int32], rankList []int32) float32 {\n\tsumPrecision := float32(0)\n\thit := 0\n\tfor i, itemId := range rankList {\n\t\tif targetSet.Contains(itemId) {\n\t\t\thit++\n\t\t\tsumPrecision += float32(hit) / float32(i+1)\n\t\t}\n\t}\n\treturn sumPrecision / float32(targetSet.Cardinality())\n}\n\n// MRR means Mean Reciprocal Rank.\n//\n// The mean reciprocal rank is a statistic measure for evaluating any process\n// that produces a list of possible responses to a sample of queries, ordered\n// by probability of correctness. The reciprocal rank of a query response is\n// the multiplicative inverse of the rank of the first correct answer: 1 for\n// first place, ​1⁄2 for second place, ​1⁄3 for third place and so on. The\n// mean reciprocal rank is the average of the reciprocal ranks of results for\n// a sample of queries Q:\n//\n//\tMRR = \\frac{1}{Q} \\sum^{|Q|}_{i=1} \\frac{1}{rank_i}\nfunc MRR(targetSet mapset.Set[int32], rankList []int32) float32 {\n\tfor i, itemId := range rankList {\n\t\tif targetSet.Contains(itemId) {\n\t\t\treturn 1 / float32(i+1)\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc Rank(model MatrixFactorization, userId int32, candidates []int32, topN int) []int32 {\n\t// Get top-n list\n\titemsHeap := heap.NewTopKFilter[int32, float32](topN)\n\tfor _, itemId := range candidates {\n\t\titemsHeap.Push(itemId, model.internalPredict(userId, itemId))\n\t}\n\treturn itemsHeap.PopAllValues()\n}\n"
  },
  {
    "path": "model/cf/evaluator_test.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage cf\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/c-bata/goptuna\"\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/model\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nconst evalEpsilon = 0.00001\n\nfunc TestNDCG(t *testing.T) {\n\ttargetSet := mapset.NewSet[int32](1, 3, 5, 7)\n\trankList := []int32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}\n\tassert.InDelta(t, 0.6766372989, NDCG(targetSet, rankList), evalEpsilon)\n}\n\nfunc TestPrecision(t *testing.T) {\n\ttargetSet := mapset.NewSet[int32](1, 3, 5, 7)\n\trankList := []int32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}\n\tassert.InDelta(t, 0.4, Precision(targetSet, rankList), evalEpsilon)\n}\n\nfunc TestRecall(t *testing.T) {\n\ttargetSet := mapset.NewSet[int32](1, 3, 15, 17, 19)\n\trankList := []int32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}\n\tassert.InDelta(t, 0.4, Recall(targetSet, rankList), evalEpsilon)\n}\n\nfunc TestAP(t *testing.T) {\n\ttargetSet := mapset.NewSet[int32](1, 3, 7, 9)\n\trankList := []int32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}\n\tassert.InDelta(t, 0.44375, MAP(targetSet, rankList), evalEpsilon)\n}\n\nfunc TestRR(t *testing.T) {\n\ttargetSet := mapset.NewSet[int32](3)\n\trankList := []int32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}\n\tassert.InDelta(t, 0.25, MRR(targetSet, rankList), evalEpsilon)\n}\n\nfunc TestHR(t *testing.T) {\n\trankList := []int32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}\n\tassert.InDelta(t, 1, HR(mapset.NewSet[int32](3), rankList), evalEpsilon)\n\tassert.InDelta(t, 0, HR(mapset.NewSet[int32](30), rankList), evalEpsilon)\n}\n\ntype mockMatrixFactorizationForEval struct {\n\tmodel.BaseModel\n\tpositive []mapset.Set[int32]\n\tnegative []mapset.Set[int32]\n}\n\nfunc (m *mockMatrixFactorizationForEval) GetUserFactor(_ int32) []float32 {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockMatrixFactorizationForEval) GetItemFactor(_ int32) []float32 {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockMatrixFactorizationForEval) IsUserPredictable(_ int32) bool {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockMatrixFactorizationForEval) IsItemPredictable(_ int32) bool {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockMatrixFactorizationForEval) Marshal(_ io.Writer) error {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockMatrixFactorizationForEval) Unmarshal(_ io.Reader) error {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockMatrixFactorizationForEval) Invalid() bool {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockMatrixFactorizationForEval) GetUserIndex() *dataset.FreqDict {\n\tpanic(\"don't call me\")\n}\n\nfunc (m *mockMatrixFactorizationForEval) GetItemIndex() *dataset.FreqDict {\n\tpanic(\"don't call me\")\n}\n\nfunc (m *mockMatrixFactorizationForEval) Fit(_ context.Context, _, _ dataset.CFSplit, _ *FitConfig) Score {\n\tpanic(\"don't call me\")\n}\n\nfunc (m *mockMatrixFactorizationForEval) Predict(_, _ string) float32 {\n\tpanic(\"don't call me\")\n}\n\nfunc (m *mockMatrixFactorizationForEval) internalPredict(userId, itemId int32) float32 {\n\tif m.positive[userId].Contains(itemId) {\n\t\treturn 1\n\t}\n\tif m.negative[userId].Contains(itemId) {\n\t\treturn -1\n\t}\n\treturn 0\n}\n\nfunc (m *mockMatrixFactorizationForEval) Clear() {\n\t// do nothing\n}\n\nfunc (m *mockMatrixFactorizationForEval) SuggestParams(trial goptuna.Trial) model.Params {\n\tpanic(\"not implemented\")\n}\n\nfunc TestEvaluate(t *testing.T) {\n\t// create dataset\n\ttrain, test := dataset.NewDataset(time.Now(), 0, 0), dataset.NewDataset(time.Now(), 0, 0)\n\t//train.UserFeedback = make([][]int32, 4)\n\tfor i := 0; i < 4; i++ {\n\t\ttrain.AddUser(data.User{UserId: strconv.Itoa(i)})\n\t\ttest.AddUser(data.User{UserId: strconv.Itoa(i / 4)})\n\t}\n\tfor i := 0; i < 16; i++ {\n\t\ttest.AddItem(data.Item{ItemId: strconv.Itoa(i)})\n\t\ttest.AddFeedback(strconv.Itoa(i/4), strconv.Itoa(i), time.Time{})\n\t}\n\tassert.Equal(t, 16, test.CountFeedback())\n\tassert.Equal(t, 4, test.CountUsers())\n\tassert.Equal(t, 16, test.CountItems())\n\t// create model\n\tm := &mockMatrixFactorizationForEval{\n\t\tpositive: []mapset.Set[int32]{\n\t\t\tmapset.NewSet[int32](0, 1, 2, 3),\n\t\t\tmapset.NewSet[int32](4, 5, 6),\n\t\t\tmapset.NewSet[int32](8, 9),\n\t\t\tmapset.NewSet[int32](12),\n\t\t},\n\t\tnegative: []mapset.Set[int32]{\n\t\t\tmapset.NewSet[int32](),\n\t\t\tmapset.NewSet[int32](7),\n\t\t\tmapset.NewSet[int32](10, 11),\n\t\t\tmapset.NewSet[int32](13, 14, 15),\n\t\t},\n\t}\n\t// evaluate model\n\ts := Evaluate(m, test, train, 4, test.CountItems(), 4, Precision)\n\tassert.Equal(t, 1, len(s))\n\tassert.Equal(t, float32(0.625), s[0])\n}\n"
  },
  {
    "path": "model/cf/model.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cf\n\nimport (\n\t\"context\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"io\"\n\t\"reflect\"\n\t\"time\"\n\n\t\"github.com/bits-and-blooms/bitset\"\n\t\"github.com/c-bata/goptuna\"\n\t\"github.com/chewxy/math32\"\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/gorse-io/gorse/common/encoding\"\n\t\"github.com/gorse-io/gorse/common/floats\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/common/monitor\"\n\t\"github.com/gorse-io/gorse/common/parallel\"\n\t\"github.com/gorse-io/gorse/common/util\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/model\"\n\t\"github.com/gorse-io/gorse/protocol\"\n\t\"github.com/juju/errors\"\n\t\"github.com/matttproud/golang_protobuf_extensions/pbutil\"\n\t\"github.com/samber/lo\"\n\t\"go.uber.org/zap\"\n)\n\ntype Score struct {\n\tNDCG      float32\n\tPrecision float32\n\tRecall    float32\n}\n\ntype FitConfig struct {\n\tJobs       int\n\tVerbose    int\n\tCandidates int\n\tTopK       int\n\tPatience   int\n}\n\nfunc NewFitConfig() *FitConfig {\n\treturn &FitConfig{\n\t\tJobs:       1,\n\t\tVerbose:    10,\n\t\tCandidates: 100,\n\t\tTopK:       10,\n\t}\n}\n\nfunc (config *FitConfig) SetVerbose(verbose int) *FitConfig {\n\tconfig.Verbose = verbose\n\treturn config\n}\n\nfunc (config *FitConfig) SetJobs(jobs int) *FitConfig {\n\tconfig.Jobs = jobs\n\treturn config\n}\n\nfunc (config *FitConfig) SetPatience(patience int) *FitConfig {\n\tconfig.Patience = patience\n\treturn config\n}\n\ntype Model interface {\n\tmodel.Model\n\t// Fit a model with a train set and parameters.\n\tFit(ctx context.Context, trainSet, validateSet dataset.CFSplit, config *FitConfig) Score\n\t// GetItemIndex returns item index.\n\tGetItemIndex() *dataset.FreqDict\n\t// Marshal model into byte stream.\n\tMarshal(w io.Writer) error\n\t// Unmarshal model from byte stream.\n\tUnmarshal(r io.Reader) error\n\t// GetUserFactor returns latent factor of a user.\n\tGetUserFactor(userIndex int32) []float32\n\t// GetItemFactor returns latent factor of an item.\n\tGetItemFactor(itemIndex int32) []float32\n}\n\ntype MatrixFactorization interface {\n\tModel\n\t// Predict the rating given by a user (userId) to a item (itemId).\n\tPredict(userId, itemId string) float32\n\t// InternalPredict predicts rating given by a user index and a item index\n\tinternalPredict(userIndex, itemIndex int32) float32\n\t// GetUserIndex returns user index.\n\tGetUserIndex() *dataset.FreqDict\n\t// GetItemIndex returns item index.\n\tGetItemIndex() *dataset.FreqDict\n\t// IsUserPredictable returns false if user has no feedback and its embedding vector never be trained.\n\tIsUserPredictable(userIndex int32) bool\n\t// IsItemPredictable returns false if item has no feedback and its embedding vector never be trained.\n\tIsItemPredictable(itemIndex int32) bool\n\t// Marshal model into byte stream.\n\tMarshal(w io.Writer) error\n\t// Unmarshal model from byte stream.\n\tUnmarshal(r io.Reader) error\n}\n\ntype BaseMatrixFactorization struct {\n\tmodel.BaseModel\n\tUserIndex       *dataset.FreqDict\n\tItemIndex       *dataset.FreqDict\n\tUserPredictable *bitset.BitSet\n\tItemPredictable *bitset.BitSet\n\t// Model parameters\n\tUserFactor [][]float32 // p_u\n\tItemFactor [][]float32 // q_i\n}\n\nfunc (baseModel *BaseMatrixFactorization) Init(trainSet dataset.CFSplit) {\n\tbaseModel.UserIndex = trainSet.GetUserDict()\n\tbaseModel.ItemIndex = trainSet.GetItemDict()\n\t// set user trained flags\n\tbaseModel.UserPredictable = bitset.New(uint(baseModel.UserIndex.Count()))\n\tfor userIndex := int32(0); userIndex < baseModel.UserIndex.Count(); userIndex++ {\n\t\tif len(trainSet.GetUserFeedback()[userIndex]) > 0 {\n\t\t\tbaseModel.UserPredictable.Set(uint(userIndex))\n\t\t}\n\t}\n\t// set item trained flags\n\tbaseModel.ItemPredictable = bitset.New(uint(baseModel.ItemIndex.Count()))\n\tfor itemIndex := int32(0); itemIndex < baseModel.ItemIndex.Count(); itemIndex++ {\n\t\tif len(trainSet.GetItemFeedback()[itemIndex]) > 0 {\n\t\t\tbaseModel.ItemPredictable.Set(uint(itemIndex))\n\t\t}\n\t}\n}\n\nfunc (baseModel *BaseMatrixFactorization) GetUserIndex() *dataset.FreqDict {\n\treturn baseModel.UserIndex\n}\n\nfunc (baseModel *BaseMatrixFactorization) GetItemIndex() *dataset.FreqDict {\n\treturn baseModel.ItemIndex\n}\n\n// IsUserPredictable returns false if user has no feedback and its embedding vector never be trained.\nfunc (baseModel *BaseMatrixFactorization) IsUserPredictable(userIndex int32) bool {\n\tif userIndex >= baseModel.UserIndex.Count() || userIndex < 0 {\n\t\treturn false\n\t}\n\treturn baseModel.UserPredictable.Test(uint(userIndex))\n}\n\n// IsItemPredictable returns false if item has no feedback and its embedding vector never be trained.\nfunc (baseModel *BaseMatrixFactorization) IsItemPredictable(itemIndex int32) bool {\n\tif itemIndex >= baseModel.ItemIndex.Count() || itemIndex < 0 {\n\t\treturn false\n\t}\n\treturn baseModel.ItemPredictable.Test(uint(itemIndex))\n}\n\n// GetUserFactor returns the latent factor of a user.\nfunc (baseModel *BaseMatrixFactorization) GetUserFactor(userIndex int32) []float32 {\n\treturn baseModel.UserFactor[userIndex]\n}\n\n// GetItemFactor returns the latent factor of an item.\nfunc (baseModel *BaseMatrixFactorization) GetItemFactor(itemIndex int32) []float32 {\n\treturn baseModel.ItemFactor[itemIndex]\n}\n\nfunc (baseModel *BaseMatrixFactorization) Predict(userId, itemId string) float32 {\n\t// Convert sparse Names to dense Names\n\tuserIndex := baseModel.UserIndex.Id(userId)\n\titemIndex := baseModel.ItemIndex.Id(itemId)\n\tif userIndex < 0 {\n\t\tlog.Logger().Warn(\"unknown user\", zap.String(\"user_id\", userId))\n\t}\n\tif itemIndex < 0 {\n\t\tlog.Logger().Warn(\"unknown item\", zap.String(\"item_id\", itemId))\n\t}\n\treturn baseModel.internalPredict(userIndex, itemIndex)\n}\n\nfunc (baseModel *BaseMatrixFactorization) internalPredict(userIndex, itemIndex int32) float32 {\n\tret := float32(0.0)\n\tif itemIndex >= 0 && userIndex >= 0 {\n\t\tret = floats.Dot(baseModel.UserFactor[userIndex], baseModel.ItemFactor[itemIndex])\n\t} else {\n\t\tlog.Logger().Warn(\"unknown user or item\")\n\t}\n\treturn ret\n}\n\n// Marshal model into byte stream.\nfunc (baseModel *BaseMatrixFactorization) Marshal(w io.Writer) error {\n\t// write params\n\terr := encoding.WriteGob(w, baseModel.Params)\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t// write predictable user count\n\tif err := binary.Write(w, binary.LittleEndian, int64(baseModel.UserPredictable.Count())); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t// write user latent factors\n\tfor userIndex := int32(0); userIndex < baseModel.UserIndex.Count(); userIndex++ {\n\t\tif baseModel.UserPredictable.Test(uint(userIndex)) {\n\t\t\tuserId, _ := baseModel.UserIndex.String(userIndex)\n\t\t\tlatentFactor := &protocol.LatentFactor{\n\t\t\t\tId:   userId,\n\t\t\t\tData: baseModel.UserFactor[userIndex],\n\t\t\t}\n\t\t\tif _, err := pbutil.WriteDelimited(w, latentFactor); err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t}\n\t}\n\t// write predictable item count\n\tif err := binary.Write(w, binary.LittleEndian, int64(baseModel.ItemPredictable.Count())); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t// write item latent factors\n\tfor itemIndex := int32(0); itemIndex < baseModel.ItemIndex.Count(); itemIndex++ {\n\t\tif baseModel.ItemPredictable.Test(uint(itemIndex)) {\n\t\t\titemId, _ := baseModel.ItemIndex.String(itemIndex)\n\t\t\tlatentFactor := &protocol.LatentFactor{\n\t\t\t\tId:   itemId,\n\t\t\t\tData: baseModel.ItemFactor[itemIndex],\n\t\t\t}\n\t\t\tif _, err := pbutil.WriteDelimited(w, latentFactor); err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// Unmarshal model from byte stream.\nfunc (baseModel *BaseMatrixFactorization) Unmarshal(r io.Reader) error {\n\t// read params\n\tif err := encoding.ReadGob(r, &baseModel.Params); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t// read predictable user count\n\tvar userPredictableCount int64\n\tif err := binary.Read(r, binary.LittleEndian, &userPredictableCount); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t// read user latent factors\n\tbaseModel.UserIndex = dataset.NewFreqDict()\n\tbaseModel.UserPredictable = bitset.New(uint(userPredictableCount))\n\tbaseModel.UserFactor = make([][]float32, userPredictableCount)\n\tfor i := 0; i < int(userPredictableCount); i++ {\n\t\tlatentFactor := new(protocol.LatentFactor)\n\t\tif _, err := pbutil.ReadDelimited(r, latentFactor); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\tuserIndex := baseModel.UserIndex.Add(latentFactor.Id)\n\t\tbaseModel.UserPredictable.Set(uint(userIndex))\n\t\tbaseModel.UserFactor[userIndex] = latentFactor.Data\n\t}\n\t// read predictable item count\n\tvar itemPredictableCount int64\n\tif err := binary.Read(r, binary.LittleEndian, &itemPredictableCount); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t// read item latent factors\n\tbaseModel.ItemIndex = dataset.NewFreqDict()\n\tbaseModel.ItemPredictable = bitset.New(uint(itemPredictableCount))\n\tbaseModel.ItemFactor = make([][]float32, itemPredictableCount)\n\tfor i := 0; i < int(itemPredictableCount); i++ {\n\t\tlatentFactor := new(protocol.LatentFactor)\n\t\tif _, err := pbutil.ReadDelimited(r, latentFactor); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\titemIndex := baseModel.ItemIndex.Add(latentFactor.Id)\n\t\tbaseModel.ItemPredictable.Set(uint(itemIndex))\n\t\tbaseModel.ItemFactor[itemIndex] = latentFactor.Data\n\t}\n\treturn nil\n}\n\nfunc (baseModel *BaseMatrixFactorization) Clear() {\n\tbaseModel.UserIndex = nil\n\tbaseModel.ItemIndex = nil\n\tbaseModel.ItemFactor = nil\n\tbaseModel.UserFactor = nil\n}\n\nfunc (baseModel *BaseMatrixFactorization) Invalid() bool {\n\treturn baseModel == nil ||\n\t\tbaseModel.UserIndex == nil ||\n\t\tbaseModel.ItemIndex == nil ||\n\t\tbaseModel.ItemFactor == nil ||\n\t\tbaseModel.UserFactor == nil\n}\n\nfunc GetModelName(m Model) string {\n\tswitch m.(type) {\n\tcase *BPR:\n\t\treturn \"bpr\"\n\tcase *ALS:\n\t\treturn \"als\"\n\tdefault:\n\t\treturn reflect.TypeOf(m).String()\n\t}\n}\n\nfunc MarshalModel(w io.Writer, m Model) error {\n\tif err := encoding.WriteString(w, GetModelName(m)); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif err := m.Marshal(w); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\treturn nil\n}\n\nfunc UnmarshalModel(r io.Reader) (MatrixFactorization, error) {\n\tname, err := encoding.ReadString(r)\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\tswitch name {\n\tcase \"bpr\":\n\t\tvar bpr BPR\n\t\tif err := bpr.Unmarshal(r); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\treturn &bpr, nil\n\tcase \"als\":\n\t\tvar als ALS\n\t\tif err := als.Unmarshal(r); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\treturn &als, nil\n\t}\n\treturn nil, fmt.Errorf(\"unknown model %v\", name)\n}\n\n// BPR means Bayesian Personal Ranking, is a pairwise learning algorithm for matrix factorization\n// model with implicit feedback. The pairwise ranking between item i and j for user u is estimated\n// by:\n//\n//\tp(i >_u j) = \\sigma( p_u^T (q_i - q_j) )\n//\n// Hyper-parameters:\n//\n//\t Reg \t\t- The regularization parameter of the cost function that is\n//\t\t\t\t  optimized. Default is 0.01.\n//\t Lr \t\t- The learning rate of SGD. Default is 0.05.\n//\t nFactors\t- The number of latent factors. Default is 10.\n//\t NEpochs\t- The number of iteration of the SGD procedure. Default is 100.\n//\t InitMean\t- The mean of initial random latent factors. Default is 0.\n//\t InitStdDev\t- The standard deviation of initial random latent factors. Default is 0.001.\ntype BPR struct {\n\tBaseMatrixFactorization\n\t// Hyper parameters\n\tnFactors   int\n\tnEpochs    int\n\tlr         float32\n\treg        float32\n\tinitMean   float32\n\tinitStdDev float32\n}\n\n// NewBPR creates a BPR model.\nfunc NewBPR(params model.Params) *BPR {\n\tbpr := new(BPR)\n\tbpr.SetParams(params)\n\treturn bpr\n}\n\n// SetParams sets hyper-parameters of the BPR model.\nfunc (bpr *BPR) SetParams(params model.Params) {\n\tbpr.BaseMatrixFactorization.SetParams(params)\n\t// Setup hyper-parameters\n\tbpr.nFactors = bpr.Params.GetInt(model.NFactors, 16)\n\tbpr.nEpochs = bpr.Params.GetInt(model.NEpochs, 100)\n\tbpr.lr = bpr.Params.GetFloat32(model.Lr, 0.05)\n\tbpr.reg = bpr.Params.GetFloat32(model.Reg, 0.01)\n\tbpr.initMean = bpr.Params.GetFloat32(model.InitMean, 0)\n\tbpr.initStdDev = bpr.Params.GetFloat32(model.InitStdDev, 0.001)\n}\n\nfunc (bpr *BPR) SuggestParams(trial goptuna.Trial) model.Params {\n\treturn model.Params{\n\t\tmodel.NFactors:   16,\n\t\tmodel.Lr:         lo.Must(trial.SuggestLogFloat(string(model.Lr), 0.001, 0.1)),\n\t\tmodel.Reg:        lo.Must(trial.SuggestLogFloat(string(model.Reg), 0.001, 0.1)),\n\t\tmodel.InitMean:   0,\n\t\tmodel.InitStdDev: lo.Must(trial.SuggestLogFloat(string(model.InitStdDev), 0.001, 0.1)),\n\t}\n}\n\n// Fit the BPR model. Its task complexity is O(bpr.nEpochs).\nfunc (bpr *BPR) Fit(ctx context.Context, trainSet, valSet dataset.CFSplit, config *FitConfig) Score {\n\tlog.Logger().Info(\"fit bpr\",\n\t\tzap.Int(\"train_set_size\", trainSet.CountFeedback()),\n\t\tzap.Int(\"test_set_size\", valSet.CountFeedback()),\n\t\tzap.Any(\"params\", bpr.GetParams()),\n\t\tzap.Any(\"config\", config))\n\tbpr.Init(trainSet)\n\t// Create buffers\n\ttemp := util.NewMatrix32(config.Jobs, bpr.nFactors)\n\tuserFactor := util.NewMatrix32(config.Jobs, bpr.nFactors)\n\tpositiveItemFactor := util.NewMatrix32(config.Jobs, bpr.nFactors)\n\tnegativeItemFactor := util.NewMatrix32(config.Jobs, bpr.nFactors)\n\trng := make([]util.RandomGenerator, config.Jobs)\n\tfor i := 0; i < config.Jobs; i++ {\n\t\trng[i] = util.NewRandomGenerator(bpr.GetRandomGenerator().Int63())\n\t}\n\t// Convert array to hashmap\n\tuserFeedback := make([]mapset.Set[int32], trainSet.CountUsers())\n\tfor u := range userFeedback {\n\t\tuserFeedback[u] = mapset.NewSet[int32]()\n\t\tfor _, i := range trainSet.GetUserFeedback()[u] {\n\t\t\tuserFeedback[u].Add(i)\n\t\t}\n\t}\n\tevalStart := time.Now()\n\tscore := Evaluate(bpr, valSet, trainSet, config.TopK, config.Candidates, config.Jobs, NDCG, Precision, Recall)\n\tscores := []lo.Tuple2[int, float32]{{A: 0, B: score[0]}}\n\tevalTime := time.Since(evalStart)\n\tlog.Logger().Debug(fmt.Sprintf(\"fit bpr %v/%v\", 0, bpr.nEpochs),\n\t\tzap.String(\"eval_time\", evalTime.String()),\n\t\tzap.Float32(fmt.Sprintf(\"NDCG@%v\", config.TopK), score[0]),\n\t\tzap.Float32(fmt.Sprintf(\"Precision@%v\", config.TopK), score[1]),\n\t\tzap.Float32(fmt.Sprintf(\"Recall@%v\", config.TopK), score[2]))\n\t// Training\n\t_, span := monitor.Start(ctx, \"BPR.Fit\", bpr.nEpochs)\n\tdefer span.End()\n\tfor epoch := 1; epoch <= bpr.nEpochs; epoch++ {\n\t\tfitStart := time.Now()\n\t\t// Training epoch\n\t\tcost := make([]float32, config.Jobs)\n\t\tif err := parallel.Parallel(ctx, trainSet.CountFeedback(), config.Jobs, func(workerId, _ int) error {\n\t\t\t// Select a user\n\t\t\tvar userIndex int32\n\t\t\tvar ratingCount int\n\t\t\tfor {\n\t\t\t\tuserIndex = rng[workerId].Int31n(int32(trainSet.CountUsers()))\n\t\t\t\tratingCount = len(trainSet.GetUserFeedback()[userIndex])\n\t\t\t\tif ratingCount > 0 {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tposIndex := trainSet.GetUserFeedback()[userIndex][rng[workerId].Intn(ratingCount)]\n\t\t\t// Select a negative sample\n\t\t\tnegIndex := int32(-1)\n\t\t\tfor {\n\t\t\t\ttemp := rng[workerId].Int31n(int32(trainSet.CountItems()))\n\t\t\t\tif !userFeedback[userIndex].Contains(temp) {\n\t\t\t\t\tnegIndex = temp\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tdiff := bpr.internalPredict(userIndex, posIndex) - bpr.internalPredict(userIndex, negIndex)\n\t\t\tcost[workerId] += math32.Log1p(math32.Exp(-diff))\n\t\t\tgrad := math32.Exp(-diff) / (1.0 + math32.Exp(-diff))\n\t\t\t// Pairwise update\n\t\t\tcopy(userFactor[workerId], bpr.UserFactor[userIndex])\n\t\t\tcopy(positiveItemFactor[workerId], bpr.ItemFactor[posIndex])\n\t\t\tcopy(negativeItemFactor[workerId], bpr.ItemFactor[negIndex])\n\t\t\t// Update positive item latent factor: +w_u\n\t\t\tfloats.MulConstTo(userFactor[workerId], grad, temp[workerId])\n\t\t\tfloats.MulConstAdd(positiveItemFactor[workerId], -bpr.reg, temp[workerId])\n\t\t\tfloats.MulConstAdd(temp[workerId], bpr.lr, bpr.ItemFactor[posIndex])\n\t\t\t// Update negative item latent factor: -w_u\n\t\t\tfloats.MulConstTo(userFactor[workerId], -grad, temp[workerId])\n\t\t\tfloats.MulConstAdd(negativeItemFactor[workerId], -bpr.reg, temp[workerId])\n\t\t\tfloats.MulConstAdd(temp[workerId], bpr.lr, bpr.ItemFactor[negIndex])\n\t\t\t// Update user latent factor: h_i-h_j\n\t\t\tfloats.SubTo(positiveItemFactor[workerId], negativeItemFactor[workerId], temp[workerId])\n\t\t\tfloats.MulConst(temp[workerId], grad)\n\t\t\tfloats.MulConstAdd(userFactor[workerId], -bpr.reg, temp[workerId])\n\t\t\tfloats.MulConstAdd(temp[workerId], bpr.lr, bpr.UserFactor[userIndex])\n\t\t\treturn nil\n\t\t}); err != nil {\n\t\t\tlog.Logger().Info(\"fit bpr canceled\", zap.Int(\"epoch\", epoch), zap.Error(err))\n\t\t\treturn Score{}\n\t\t}\n\t\tfitTime := time.Since(fitStart)\n\t\t// Cross validation\n\t\tif epoch%config.Verbose == 0 || epoch == bpr.nEpochs {\n\t\t\tevalStart = time.Now()\n\t\t\tscore = Evaluate(bpr, valSet, trainSet, config.TopK, config.Candidates, config.Jobs, NDCG, Precision, Recall)\n\t\t\tscores = append(scores, lo.Tuple2[int, float32]{A: epoch, B: score[0]})\n\t\t\tevalTime = time.Since(evalStart)\n\t\t\tlog.Logger().Info(fmt.Sprintf(\"fit bpr %v/%v\", epoch, bpr.nEpochs),\n\t\t\t\tzap.String(\"fit_time\", fitTime.String()),\n\t\t\t\tzap.String(\"eval_time\", evalTime.String()),\n\t\t\t\tzap.Float32(fmt.Sprintf(\"NDCG@%v\", config.TopK), score[0]),\n\t\t\t\tzap.Float32(fmt.Sprintf(\"Precision@%v\", config.TopK), score[1]),\n\t\t\t\tzap.Float32(fmt.Sprintf(\"Recall@%v\", config.TopK), score[2]))\n\t\t\t// early stopping if no improvement in last `patience` epochs\n\t\t\tif config.Patience > 0 && epoch > config.Patience {\n\t\t\t\tepochScore := lo.MaxBy(scores, func(a, b lo.Tuple2[int, float32]) bool { return a.B > b.B })\n\t\t\t\tif epochScore.A <= epoch-config.Patience {\n\t\t\t\t\tlog.Logger().Info(\"early stopping\",\n\t\t\t\t\t\tzap.Int(\"best_epoch\", epochScore.A),\n\t\t\t\t\t\tzap.Float32(\"best_NDCG\", epochScore.B),\n\t\t\t\t\t\tzap.Int(\"patience\", config.Patience))\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tspan.Add(1)\n\t}\n\tlog.Logger().Info(\"fit bpr complete\",\n\t\tzap.Float32(fmt.Sprintf(\"NDCG@%v\", config.TopK), score[0]),\n\t\tzap.Float32(fmt.Sprintf(\"Precision@%v\", config.TopK), score[1]),\n\t\tzap.Float32(fmt.Sprintf(\"Recall@%v\", config.TopK), score[2]))\n\treturn Score{\n\t\tNDCG:      score[0],\n\t\tPrecision: score[1],\n\t\tRecall:    score[2],\n\t}\n}\n\nfunc (bpr *BPR) Init(trainSet dataset.CFSplit) {\n\t// Initialize parameters\n\tnewUserFactor := bpr.GetRandomGenerator().NormalMatrix(trainSet.CountUsers(), bpr.nFactors, bpr.initMean, bpr.initStdDev)\n\tnewItemFactor := bpr.GetRandomGenerator().NormalMatrix(trainSet.CountItems(), bpr.nFactors, bpr.initMean, bpr.initStdDev)\n\t// Initialize base\n\tbpr.UserFactor = newUserFactor\n\tbpr.ItemFactor = newItemFactor\n\tbpr.BaseMatrixFactorization.Init(trainSet)\n}\n\n// Marshal model into byte stream.\nfunc (bpr *BPR) Marshal(w io.Writer) error {\n\tif err := bpr.BaseMatrixFactorization.Marshal(w); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\treturn nil\n}\n\n// Unmarshal model from byte stream.\nfunc (bpr *BPR) Unmarshal(r io.Reader) error {\n\tif err := bpr.BaseMatrixFactorization.Unmarshal(r); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tbpr.SetParams(bpr.Params)\n\treturn nil\n}\n\ntype ALS struct {\n\tBaseMatrixFactorization\n\t// Hyper parameters\n\tnFactors   int\n\tnEpochs    int\n\treg        float32\n\tinitMean   float32\n\tinitStdDev float32\n\tweight     float32\n}\n\n// NewALS creates a eALS model.\nfunc NewALS(params model.Params) *ALS {\n\tfast := new(ALS)\n\tfast.SetParams(params)\n\treturn fast\n}\n\n// SetParams sets hyper-parameters for the ALS model.\nfunc (als *ALS) SetParams(params model.Params) {\n\tals.BaseMatrixFactorization.SetParams(params)\n\tals.nFactors = als.Params.GetInt(model.NFactors, 16)\n\tals.nEpochs = als.Params.GetInt(model.NEpochs, 50)\n\tals.initMean = als.Params.GetFloat32(model.InitMean, 0)\n\tals.initStdDev = als.Params.GetFloat32(model.InitStdDev, 0.1)\n\tals.reg = als.Params.GetFloat32(model.Reg, 0.06)\n\tals.weight = als.Params.GetFloat32(model.Alpha, 0.001)\n}\n\nfunc (als *ALS) SuggestParams(trial goptuna.Trial) model.Params {\n\treturn model.Params{\n\t\tmodel.NFactors:   16,\n\t\tmodel.InitMean:   0,\n\t\tmodel.InitStdDev: lo.Must(trial.SuggestLogFloat(string(model.InitStdDev), 0.001, 0.1)),\n\t\tmodel.Reg:        lo.Must(trial.SuggestLogFloat(string(model.Reg), 0.001, 0.1)),\n\t\tmodel.Alpha:      lo.Must(trial.SuggestLogFloat(string(model.Alpha), 0.001, 0.1)),\n\t}\n}\n\nfunc (als *ALS) Init(trainSet dataset.CFSplit) {\n\t// Initialize\n\tnewUserFactor := als.GetRandomGenerator().NormalMatrix(trainSet.CountUsers(), als.nFactors, als.initMean, als.initStdDev)\n\tnewItemFactor := als.GetRandomGenerator().NormalMatrix(trainSet.CountItems(), als.nFactors, als.initMean, als.initStdDev)\n\t// Initialize base\n\tals.UserFactor = newUserFactor\n\tals.ItemFactor = newItemFactor\n\tals.BaseMatrixFactorization.Init(trainSet)\n}\n\n// Fit the ALS model. Its task complexity is O(ccd.nEpochs).\nfunc (als *ALS) Fit(ctx context.Context, trainSet, valSet dataset.CFSplit, config *FitConfig) Score {\n\tlog.Logger().Info(\"fit als\",\n\t\tzap.Int(\"train_set_size\", trainSet.CountFeedback()),\n\t\tzap.Int(\"test_set_size\", valSet.CountFeedback()),\n\t\tzap.Any(\"params\", als.GetParams()),\n\t\tzap.Any(\"config\", config))\n\tals.Init(trainSet)\n\t// Create temporary matrix\n\ts := util.NewMatrix32(als.nFactors, als.nFactors)\n\tuserPredictions := make([][]float32, config.Jobs)\n\titemPredictions := make([][]float32, config.Jobs)\n\tuserRes := make([][]float32, config.Jobs)\n\titemRes := make([][]float32, config.Jobs)\n\tfor i := 0; i < config.Jobs; i++ {\n\t\tuserPredictions[i] = make([]float32, trainSet.CountItems())\n\t\titemPredictions[i] = make([]float32, trainSet.CountUsers())\n\t\tuserRes[i] = make([]float32, trainSet.CountItems())\n\t\titemRes[i] = make([]float32, trainSet.CountUsers())\n\t}\n\t// evaluate initial model\n\tevalStart := time.Now()\n\tscore := Evaluate(als, valSet, trainSet, config.TopK, config.Candidates, config.Jobs, NDCG, Precision, Recall)\n\tscores := []lo.Tuple2[int, float32]{{A: 0, B: score[0]}}\n\tevalTime := time.Since(evalStart)\n\tlog.Logger().Debug(fmt.Sprintf(\"fit als %v/%v\", 0, als.nEpochs),\n\t\tzap.String(\"eval_time\", evalTime.String()),\n\t\tzap.Float32(fmt.Sprintf(\"NDCG@%v\", config.TopK), score[0]),\n\t\tzap.Float32(fmt.Sprintf(\"Precision@%v\", config.TopK), score[1]),\n\t\tzap.Float32(fmt.Sprintf(\"Recall@%v\", config.TopK), score[2]))\n\n\t_, span := monitor.Start(ctx, \"ALS.Fit\", als.nEpochs)\n\tdefer span.End()\n\tfor ep := 1; ep <= als.nEpochs; ep++ {\n\t\tfitStart := time.Now()\n\t\t// Update user factors\n\t\t// S^q <- \\sum^N_{itemIndex=1} c_i q_i q_i^T\n\t\tfloats.MatZero(s)\n\t\tfor itemIndex := 0; itemIndex < trainSet.CountItems(); itemIndex++ {\n\t\t\tif err := ctx.Err(); err != nil {\n\t\t\t\tlog.Logger().Info(\"fit als canceled\", zap.Int(\"epoch\", ep), zap.Error(err))\n\t\t\t\treturn Score{}\n\t\t\t}\n\t\t\tif len(trainSet.GetItemFeedback()[itemIndex]) > 0 {\n\t\t\t\tfor i := 0; i < als.nFactors; i++ {\n\t\t\t\t\tfor j := 0; j < als.nFactors; j++ {\n\t\t\t\t\t\ts[i][j] += als.ItemFactor[itemIndex][i] * als.ItemFactor[itemIndex][j]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif err := parallel.Parallel(ctx, trainSet.CountUsers(), config.Jobs, func(workerId, userIndex int) error {\n\t\t\tuserFeedback := trainSet.GetUserFeedback()[userIndex]\n\t\t\tfor _, i := range userFeedback {\n\t\t\t\tuserPredictions[workerId][i] = als.internalPredict(int32(userIndex), i)\n\t\t\t}\n\t\t\tfor f := 0; f < als.nFactors; f++ {\n\t\t\t\t// for itemIndex \\in R_u do   \\hat_{r}^f_{ui} <- \\hat_{r}_{ui} - p_{uf]q_{if}\n\t\t\t\tfor _, i := range userFeedback {\n\t\t\t\t\tuserRes[workerId][i] = userPredictions[workerId][i] - als.UserFactor[userIndex][f]*als.ItemFactor[i][f]\n\t\t\t\t}\n\t\t\t\t// p_{uf} <-\n\t\t\t\ta, b, c := float32(0), float32(0), float32(0)\n\t\t\t\tfor _, i := range userFeedback {\n\t\t\t\t\ta += (1 - (1-als.weight)*userRes[workerId][i]) * als.ItemFactor[i][f]\n\t\t\t\t\tc += (1 - als.weight) * als.ItemFactor[i][f] * als.ItemFactor[i][f]\n\t\t\t\t}\n\t\t\t\tfor k := 0; k < als.nFactors; k++ {\n\t\t\t\t\tif k != f {\n\t\t\t\t\t\tb += als.weight * als.UserFactor[userIndex][k] * s[k][f]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tals.UserFactor[userIndex][f] = (a - b) / (c + als.weight*s[f][f] + als.reg)\n\t\t\t\t// for itemIndex \\in R_u do   \\hat_{r}_{ui} <- \\hat_{r}^f_{ui} - p_{uf]q_{if}\n\t\t\t\tfor _, i := range userFeedback {\n\t\t\t\t\tuserPredictions[workerId][i] = userRes[workerId][i] + als.UserFactor[userIndex][f]*als.ItemFactor[i][f]\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t}); err != nil {\n\t\t\tlog.Logger().Info(\"fit als canceled\", zap.Int(\"epoch\", ep), zap.Error(err))\n\t\t\treturn Score{}\n\t\t}\n\t\t// Update item factors\n\t\t// S^p <- P^T P\n\t\tfloats.MatZero(s)\n\t\tfor userIndex := 0; userIndex < trainSet.CountUsers(); userIndex++ {\n\t\t\tif ctx.Err() != nil {\n\t\t\t\tlog.Logger().Info(\"fit als canceled\", zap.Int(\"epoch\", ep), zap.Error(ctx.Err()))\n\t\t\t\treturn Score{}\n\t\t\t}\n\t\t\tif len(trainSet.GetUserFeedback()[userIndex]) > 0 {\n\t\t\t\tfor i := 0; i < als.nFactors; i++ {\n\t\t\t\t\tfor j := 0; j < als.nFactors; j++ {\n\t\t\t\t\t\ts[i][j] += als.UserFactor[userIndex][i] * als.UserFactor[userIndex][j]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif err := parallel.Parallel(ctx, trainSet.CountItems(), config.Jobs, func(workerId, itemIndex int) error {\n\t\t\titemFeedback := trainSet.GetItemFeedback()[itemIndex]\n\t\t\tfor _, u := range itemFeedback {\n\t\t\t\titemPredictions[workerId][u] = als.internalPredict(u, int32(itemIndex))\n\t\t\t}\n\t\t\tfor f := 0; f < als.nFactors; f++ {\n\t\t\t\t// for itemIndex \\in R_u do   \\hat_{r}^f_{ui} <- \\hat_{r}_{ui} - p_{uf]q_{if}\n\t\t\t\tfor _, u := range itemFeedback {\n\t\t\t\t\titemRes[workerId][u] = itemPredictions[workerId][u] - als.UserFactor[u][f]*als.ItemFactor[itemIndex][f]\n\t\t\t\t}\n\t\t\t\t// q_{if} <-\n\t\t\t\ta, b, c := float32(0), float32(0), float32(0)\n\t\t\t\tfor _, u := range itemFeedback {\n\t\t\t\t\ta += (1 - (1-als.weight)*itemRes[workerId][u]) * als.UserFactor[u][f]\n\t\t\t\t\tc += (1 - als.weight) * als.UserFactor[u][f] * als.UserFactor[u][f]\n\t\t\t\t}\n\t\t\t\tfor k := 0; k < als.nFactors; k++ {\n\t\t\t\t\tif k != f {\n\t\t\t\t\t\tb += als.weight * als.ItemFactor[itemIndex][k] * s[k][f]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tals.ItemFactor[itemIndex][f] = (a - b) / (c + als.weight*s[f][f] + als.reg)\n\t\t\t\t// for itemIndex \\in R_u do   \\hat_{r}_{ui} <- \\hat_{r}^f_{ui} - p_{uf]q_{if}\n\t\t\t\tfor _, u := range itemFeedback {\n\t\t\t\t\titemPredictions[workerId][u] = itemRes[workerId][u] + als.UserFactor[u][f]*als.ItemFactor[itemIndex][f]\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t}); err != nil {\n\t\t\tlog.Logger().Info(\"fit als canceled\", zap.Int(\"epoch\", ep), zap.Error(ctx.Err()))\n\t\t\treturn Score{}\n\t\t}\n\t\tfitTime := time.Since(fitStart)\n\t\t// Cross validation\n\t\tif ep%config.Verbose == 0 || ep == als.nEpochs {\n\t\t\tevalStart = time.Now()\n\t\t\tscore = Evaluate(als, valSet, trainSet, config.TopK, config.Candidates, config.Jobs, NDCG, Precision, Recall)\n\t\t\tscores = append(scores, lo.Tuple2[int, float32]{A: ep, B: score[0]})\n\t\t\tevalTime = time.Since(evalStart)\n\t\t\tlog.Logger().Debug(fmt.Sprintf(\"fit als %v/%v\", ep, als.nEpochs),\n\t\t\t\tzap.String(\"fit_time\", fitTime.String()),\n\t\t\t\tzap.String(\"eval_time\", evalTime.String()),\n\t\t\t\tzap.Float32(fmt.Sprintf(\"NDCG@%v\", config.TopK), score[0]),\n\t\t\t\tzap.Float32(fmt.Sprintf(\"Precision@%v\", config.TopK), score[1]),\n\t\t\t\tzap.Float32(fmt.Sprintf(\"Recall@%v\", config.TopK), score[2]))\n\t\t\t// early stopping if no improvement in last `patience` epochs\n\t\t\tif config.Patience > 0 && ep > config.Patience {\n\t\t\t\tepochScore := lo.MaxBy(scores, func(a, b lo.Tuple2[int, float32]) bool { return a.B > b.B })\n\t\t\t\tif epochScore.A <= ep-config.Patience {\n\t\t\t\t\tlog.Logger().Info(\"early stopping\",\n\t\t\t\t\t\tzap.Int(\"best_epoch\", epochScore.A),\n\t\t\t\t\t\tzap.Float32(\"best_NDCG\", epochScore.B),\n\t\t\t\t\t\tzap.Int(\"patience\", config.Patience))\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tspan.Add(1)\n\t}\n\tlog.Logger().Info(\"fit als complete\",\n\t\tzap.Float32(fmt.Sprintf(\"NDCG@%v\", config.TopK), score[0]),\n\t\tzap.Float32(fmt.Sprintf(\"Precision@%v\", config.TopK), score[1]),\n\t\tzap.Float32(fmt.Sprintf(\"Recall@%v\", config.TopK), score[2]))\n\treturn Score{\n\t\tNDCG:      score[0],\n\t\tPrecision: score[1],\n\t\tRecall:    score[2],\n\t}\n}\n\n// Marshal model into byte stream.\nfunc (als *ALS) Marshal(w io.Writer) error {\n\tif err := als.BaseMatrixFactorization.Marshal(w); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\treturn nil\n}\n\n// Unmarshal model from byte stream.\nfunc (als *ALS) Unmarshal(r io.Reader) error {\n\tif err := als.BaseMatrixFactorization.Unmarshal(r); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tals.SetParams(als.Params)\n\treturn nil\n}\n"
  },
  {
    "path": "model/cf/model_test.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage cf\n\nimport (\n\t\"bytes\"\n\t\"math\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/gorse-io/gorse/common/floats\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/model\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nconst benchDelta = 0.01\n\nfunc newFitConfig(_ int) *FitConfig {\n\tcfg := NewFitConfig().SetVerbose(1).SetJobs(runtime.NumCPU())\n\treturn cfg\n}\n\nfunc TestBPR_MovieLens(t *testing.T) {\n\ttrainSet, testSet, err := dataset.LoadDataFromBuiltIn(\"ml-1m\")\n\tassert.NoError(t, err)\n\tm := NewBPR(model.Params{\n\t\tmodel.NFactors:   8,\n\t\tmodel.Reg:        0.01,\n\t\tmodel.Lr:         0.05,\n\t\tmodel.NEpochs:    30,\n\t\tmodel.InitMean:   0,\n\t\tmodel.InitStdDev: 0.001,\n\t})\n\tfitConfig := newFitConfig(30)\n\tscore := m.Fit(t.Context(), trainSet, testSet, fitConfig)\n\tassert.InDelta(t, 0.36, score.NDCG, benchDelta)\n\tassert.Equal(t, trainSet.GetUserDict(), m.GetUserIndex())\n\tassert.Equal(t, testSet.GetItemDict(), m.GetItemIndex())\n\n\t// test predict\n\tassert.Equal(t, m.Predict(\"1\", \"1\"), m.internalPredict(1, 1))\n\tassert.Equal(t, m.internalPredict(1, 1), floats.Dot(m.GetUserFactor(1), m.GetItemFactor(1)))\n\tassert.True(t, m.IsUserPredictable(1))\n\tassert.True(t, m.IsItemPredictable(1))\n\tassert.False(t, m.IsUserPredictable(math.MaxInt32))\n\tassert.False(t, m.IsItemPredictable(math.MaxInt32))\n\n\t// test encode/decode model and increment training\n\tbuf := bytes.NewBuffer(nil)\n\terr = MarshalModel(buf, m)\n\tassert.NoError(t, err)\n\ttmp, err := UnmarshalModel(buf)\n\tassert.NoError(t, err)\n\tassert.Equal(t, m.Params, tmp.GetParams())\n\tassert.Equal(t, m.Predict(\"1\", \"1\"), tmp.Predict(\"1\", \"1\"))\n\tassert.True(t, m.IsUserPredictable(1))\n\tassert.True(t, m.IsItemPredictable(1))\n\tassert.False(t, m.IsUserPredictable(math.MaxInt32))\n\tassert.False(t, m.IsItemPredictable(math.MaxInt32))\n\n\t// test clear\n\tm.Clear()\n\tassert.True(t, m.Invalid())\n}\n\n//func TestBPR_Pinterest(t *testing.T) {\n//\ttrainSet, testSet, err := LoadDataFromBuiltIn(\"pinterest-20\")\n//\tassert.NoError(t, err)\n//\tm := NewBPR(model.Params{\n//\t\tmodel.NFactors:   8,\n//\t\tmodel.Reg:        0.005,\n//\t\tmodel.Lr:         0.05,\n//\t\tmodel.NEpochs:    50,\n//\t\tmodel.InitMean:   0,\n//\t\tmodel.InitStdDev: 0.001,\n//\t})\n//\tscore := m.Fit(trainSet, testSet, fitConfig)\n//\tassertEpsilon(t, 0.53, score.NDCG, benchDelta)\n//}\n\nfunc TestCCD_MovieLens(t *testing.T) {\n\ttrainSet, testSet, err := dataset.LoadDataFromBuiltIn(\"ml-1m\")\n\tassert.NoError(t, err)\n\tm := NewALS(model.Params{\n\t\tmodel.NFactors: 8,\n\t\tmodel.Reg:      0.015,\n\t\tmodel.NEpochs:  30,\n\t\tmodel.Alpha:    0.05,\n\t})\n\tfitConfig := newFitConfig(30)\n\tscore := m.Fit(t.Context(), trainSet, testSet, fitConfig)\n\tassert.InDelta(t, 0.36, score.NDCG, benchDelta)\n\n\t// test predict\n\tassert.Equal(t, m.Predict(\"1\", \"1\"), m.internalPredict(1, 1))\n\tassert.Equal(t, m.internalPredict(1, 1), floats.Dot(m.GetUserFactor(1), m.GetItemFactor(1)))\n\n\t// test encode/decode model and increment training\n\tbuf := bytes.NewBuffer(nil)\n\terr = MarshalModel(buf, m)\n\tassert.NoError(t, err)\n\ttmp, err := UnmarshalModel(buf)\n\tassert.NoError(t, err)\n\tassert.Equal(t, m.Params, tmp.GetParams())\n\tassert.Equal(t, m.Predict(\"1\", \"1\"), tmp.Predict(\"1\", \"1\"))\n\n\t// test clear\n\tm.Clear()\n\tassert.True(t, m.Invalid())\n}\n\n//func TestCCD_Pinterest(t *testing.T) {\n//\ttrainSet, testSet, err := LoadDataFromBuiltIn(\"pinterest-20\")\n//\tassert.NoError(t, err)\n//\tm := NewALS(model.Params{\n//\t\tmodel.NFactors:   8,\n//\t\tmodel.Reg:        0.01,\n//\t\tmodel.NEpochs:    20,\n//\t\tmodel.InitStdDev: 0.01,\n//\t\tmodel.Alpha:      0.001,\n//\t})\n//\tscore := m.Fit(trainSet, testSet, fitConfig)\n//\tassertEpsilon(t, 0.52, score.NDCG, benchDelta)\n//}\n"
  },
  {
    "path": "model/cf/optimize.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/c-bata/goptuna\"\n\t\"github.com/gorse-io/gorse/common/monitor\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/storage/meta\"\n\t\"golang.org/x/exp/maps\"\n)\n\ntype ModelCreator func() MatrixFactorization\n\ntype ModelSearch struct {\n\tmodelCreators map[string]ModelCreator\n\tmodelTypes    []string\n\ttrainSet      dataset.CFSplit\n\tvalSet        dataset.CFSplit\n\tconfig        *FitConfig\n\tctx           context.Context\n\tspan          *monitor.Span\n\tresult        meta.Model[Score]\n}\n\nfunc NewModelSearch(models map[string]ModelCreator, trainSet, valSet dataset.CFSplit, config *FitConfig) *ModelSearch {\n\treturn &ModelSearch{\n\t\tmodelCreators: models,\n\t\tmodelTypes:    maps.Keys(models),\n\t\ttrainSet:      trainSet,\n\t\tvalSet:        valSet,\n\t\tconfig:        config,\n\t}\n}\n\nfunc (ms *ModelSearch) WithContext(ctx context.Context) *ModelSearch {\n\tms.ctx = ctx\n\treturn ms\n}\n\nfunc (ms *ModelSearch) WithSpan(span *monitor.Span) *ModelSearch {\n\tms.span = span\n\treturn ms\n}\n\nfunc (ms *ModelSearch) Objective(trial goptuna.Trial) (float64, error) {\n\tif len(ms.modelCreators) == 0 {\n\t\treturn 0, errors.New(\"no model to search\")\n\t}\n\tmodelType, err := trial.SuggestCategorical(\"Model\", ms.modelTypes)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tm := ms.modelCreators[modelType]()\n\tm.SetParams(m.SuggestParams(trial))\n\tscore := m.Fit(ms.ctx, ms.trainSet, ms.valSet, ms.config)\n\tif score.NDCG > ms.result.Score.NDCG {\n\t\tms.result.Type = modelType\n\t\tms.result.Params = m.GetParams()\n\t\tms.result.Score = score\n\t}\n\tif ms.span != nil {\n\t\tms.span.Add(1)\n\t}\n\treturn float64(score.NDCG), nil\n}\n\nfunc (ms *ModelSearch) Result() meta.Model[Score] {\n\treturn ms.result\n}\n"
  },
  {
    "path": "model/cf/optimize_test.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage cf\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/c-bata/goptuna\"\n\t\"github.com/c-bata/goptuna/tpe\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/model\"\n\t\"github.com/samber/lo\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype mockMatrixFactorizationForSearch struct {\n\tmodel.BaseModel\n}\n\nfunc newMockMatrixFactorizationForSearch(numEpoch int) *mockMatrixFactorizationForSearch {\n\treturn &mockMatrixFactorizationForSearch{model.BaseModel{Params: model.Params{model.NEpochs: numEpoch}}}\n}\n\nfunc (m *mockMatrixFactorizationForSearch) GetUserFactor(_ int32) []float32 {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockMatrixFactorizationForSearch) GetItemFactor(_ int32) []float32 {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockMatrixFactorizationForSearch) IsUserPredictable(_ int32) bool {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockMatrixFactorizationForSearch) IsItemPredictable(_ int32) bool {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockMatrixFactorizationForSearch) Marshal(_ io.Writer) error {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockMatrixFactorizationForSearch) Unmarshal(_ io.Reader) error {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockMatrixFactorizationForSearch) Invalid() bool {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockMatrixFactorizationForSearch) GetUserIndex() *dataset.FreqDict {\n\tpanic(\"don't call me\")\n}\n\nfunc (m *mockMatrixFactorizationForSearch) GetItemIndex() *dataset.FreqDict {\n\tpanic(\"don't call me\")\n}\n\nfunc (m *mockMatrixFactorizationForSearch) Fit(_ context.Context, _, _ dataset.CFSplit, _ *FitConfig) Score {\n\tscore := float32(0)\n\tscore += m.Params.GetFloat32(model.NFactors, 0.0)\n\tscore += m.Params.GetFloat32(model.InitMean, 0.0)\n\tscore += m.Params.GetFloat32(model.InitStdDev, 0.0)\n\treturn Score{NDCG: score}\n}\n\nfunc (m *mockMatrixFactorizationForSearch) Predict(_, _ string) float32 {\n\tpanic(\"don't call me\")\n}\n\nfunc (m *mockMatrixFactorizationForSearch) internalPredict(_, _ int32) float32 {\n\tpanic(\"don't call me\")\n}\n\nfunc (m *mockMatrixFactorizationForSearch) Clear() {\n\t// do nothing\n}\n\nfunc (m *mockMatrixFactorizationForSearch) SuggestParams(trial goptuna.Trial) model.Params {\n\treturn model.Params{\n\t\tmodel.NFactors:   lo.Must(trial.SuggestDiscreteFloat(string(model.NFactors), 1, 4, 1)),\n\t\tmodel.InitMean:   lo.Must(trial.SuggestDiscreteFloat(string(model.InitMean), 1, 4, 1)),\n\t\tmodel.InitStdDev: lo.Must(trial.SuggestDiscreteFloat(string(model.InitStdDev), 4, 4, 1)),\n\t}\n}\n\nfunc TestTPE(t *testing.T) {\n\tsearch := NewModelSearch(map[string]ModelCreator{\n\t\t\"mock\": func() MatrixFactorization {\n\t\t\treturn newMockMatrixFactorizationForSearch(10)\n\t\t},\n\t}, nil, nil, nil)\n\tstudy, err := goptuna.CreateStudy(\"TestTPE\",\n\t\tgoptuna.StudyOptionDirection(goptuna.StudyDirectionMaximize),\n\t\tgoptuna.StudyOptionSampler(tpe.NewSampler()))\n\tassert.NoError(t, err)\n\terr = study.Optimize(search.Objective, 10)\n\tassert.NoError(t, err)\n\tv, _ := study.GetBestValue()\n\tassert.Equal(t, float64(12), v)\n\tresult := search.Result()\n\tassert.Equal(t, \"mock\", result.Type)\n\tassert.Equal(t, model.Params{\n\t\tmodel.NFactors:   float64(4),\n\t\tmodel.InitMean:   float64(4),\n\t\tmodel.InitStdDev: float64(4),\n\t}, result.Params)\n\tassert.Equal(t, Score{NDCG: 12}, result.Score)\n}\n"
  },
  {
    "path": "model/ctr/data.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage ctr\n\nimport (\n\t\"bufio\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/gorse-io/gorse/common/jsonutil\"\n\t\"github.com/gorse-io/gorse/common/util\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/model\"\n\t\"github.com/juju/errors\"\n\t\"github.com/samber/lo\"\n\t\"modernc.org/mathutil\"\n)\n\ntype Label struct {\n\tName  string\n\tValue float32\n}\n\nfunc ConvertLabels(o any) []Label {\n\tfeatures := make([]Label, 0)\n\treturn convertLabels(features, \"\", o)\n}\n\nfunc convertLabels(result []Label, prefix string, o any) []Label {\n\tif o == nil {\n\t\treturn nil\n\t}\n\tswitch labels := o.(type) {\n\tcase []any:\n\t\tif len(labels) == 0 {\n\t\t\treturn nil\n\t\t}\n\t\tswitch labels[0].(type) {\n\t\tcase string:\n\t\t\tfor _, val := range labels {\n\t\t\t\tif s, ok := val.(string); ok {\n\t\t\t\t\tresult = append(result, Label{\n\t\t\t\t\t\tName:  prefix + s,\n\t\t\t\t\t\tValue: 1,\n\t\t\t\t\t})\n\t\t\t\t} else {\n\t\t\t\t\tpanic(\"unsupported labels: \" + jsonutil.MustMarshal(labels))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\tcase map[string]any:\n\t\tfor key, val := range labels {\n\t\t\tresult = convertLabels(result, prefix+key+\".\", val)\n\t\t}\n\tcase string:\n\t\tresult = append(result, Label{\n\t\t\tName:  prefix + labels,\n\t\t\tValue: 1,\n\t\t})\n\tcase json.Number:\n\t\tvalue, _ := labels.Float64()\n\t\tresult = append(result, Label{\n\t\t\tName:  prefix,\n\t\t\tValue: float32(value),\n\t\t})\n\t}\n\treturn result\n}\n\ntype Embedding struct {\n\tName  string\n\tValue []float32\n}\n\nfunc ConvertEmbeddings(o any) []Embedding {\n\tembeddings := make([]Embedding, 0)\n\treturn convertEmbeddings(embeddings, \"\", o)\n}\n\nfunc convertEmbeddings(result []Embedding, prefix string, o any) []Embedding {\n\tif o == nil {\n\t\treturn nil\n\t}\n\tswitch embeddings := o.(type) {\n\tcase []any:\n\t\tif len(embeddings) == 0 {\n\t\t\treturn nil\n\t\t}\n\t\tvar value []float32\n\t\tfor _, val := range embeddings {\n\t\t\tswitch v := val.(type) {\n\t\t\tcase float64:\n\t\t\t\tvalue = append(value, float32(v))\n\t\t\tcase float32:\n\t\t\t\tvalue = append(value, v)\n\t\t\tdefault:\n\t\t\t\treturn result\n\t\t\t}\n\t\t}\n\t\tresult = append(result, Embedding{\n\t\t\tName:  prefix,\n\t\t\tValue: value,\n\t\t})\n\tcase []float64:\n\t\tresult = append(result, Embedding{\n\t\t\tName:  prefix,\n\t\t\tValue: lo.Map(embeddings, func(f float64, _ int) float32 { return float32(f) }),\n\t\t})\n\tcase []float32:\n\t\tresult = append(result, Embedding{\n\t\t\tName:  prefix,\n\t\t\tValue: embeddings,\n\t\t})\n\tcase map[string]any:\n\t\tfor key, val := range embeddings {\n\t\t\tif prefix == \"\" {\n\t\t\t\tresult = convertEmbeddings(result, key, val)\n\t\t\t} else {\n\t\t\t\tresult = convertEmbeddings(result, prefix+\".\"+key, val)\n\t\t\t}\n\t\t}\n\t}\n\treturn result\n}\n\n// Dataset for click-through-rate models.\ntype Dataset struct {\n\tIndex                  dataset.UnifiedIndex\n\tUserLabels             [][]lo.Tuple2[int32, float32]\n\tItemLabels             [][]lo.Tuple2[int32, float32]\n\tContextLabels          [][]lo.Tuple2[int32, float32]\n\tUsers                  []int32\n\tItems                  []int32\n\tTarget                 []float32\n\tItemEmbeddings         [][][]float32 // Index by row id, embedding id, embedding dimension\n\tItemEmbeddingDimension []int\n\tItemEmbeddingIndex     *dataset.Index\n\tPositiveCount          int\n\tNegativeCount          int\n}\n\n// CountUsers returns the number of users.\nfunc (dataset *Dataset) CountUsers() int {\n\treturn int(dataset.Index.CountUsers())\n}\n\n// CountItems returns the number of items.\nfunc (dataset *Dataset) CountItems() int {\n\treturn int(dataset.Index.CountItems())\n}\n\nfunc (dataset *Dataset) CountUserLabels() int {\n\treturn int(dataset.Index.CountUserLabels())\n}\n\nfunc (dataset *Dataset) CountItemLabels() int {\n\treturn int(dataset.Index.CountItemLabels())\n}\n\nfunc (dataset *Dataset) CountContextLabels() int {\n\treturn int(dataset.Index.CountContextLabels())\n}\n\nfunc (dataset *Dataset) CountPositive() int {\n\treturn dataset.PositiveCount\n}\n\nfunc (dataset *Dataset) CountNegative() int {\n\treturn dataset.NegativeCount\n}\n\nfunc (dataset *Dataset) GetIndex() dataset.UnifiedIndex {\n\treturn dataset.Index\n}\n\n// Count returns the number of samples.\nfunc (dataset *Dataset) Count() int {\n\tif len(dataset.Users) != len(dataset.Items) {\n\t\tpanic(\"len(dataset.Users) != len(dataset.Items)\")\n\t}\n\tif len(dataset.Users) > 0 && len(dataset.Users) != len(dataset.Target) {\n\t\tpanic(\"len(dataset.Users) != len(dataset.Target)\")\n\t}\n\tif dataset.ContextLabels != nil && len(dataset.ContextLabels) != len(dataset.Target) {\n\t\tpanic(\"len(dataset.ContextLabels) != len(dataset.Target)\")\n\t}\n\treturn len(dataset.Target)\n}\n\nfunc (dataset *Dataset) GetTarget(i int) float32 {\n\treturn dataset.Target[i]\n}\n\n// Get returns the i-th sample.\nfunc (dataset *Dataset) Get(i int) ([]int32, []float32, [][]float32, float32) {\n\tvar (\n\t\tindices   []int32\n\t\tvalues    []float32\n\t\tembedding [][]float32\n\t\tposition  int32\n\t)\n\t// append user id\n\tif len(dataset.Users) > 0 {\n\t\tindices = append(indices, dataset.Users[i])\n\t\tvalues = append(values, 1)\n\t\tposition += int32(dataset.CountUsers())\n\t}\n\t// append item id\n\tif len(dataset.Items) > 0 {\n\t\tindices = append(indices, position+dataset.Items[i])\n\t\tvalues = append(values, 1)\n\t\tposition += int32(dataset.CountItems())\n\t\tif len(dataset.ItemEmbeddings) > 0 {\n\t\t\tembedding = dataset.ItemEmbeddings[dataset.Items[i]]\n\t\t}\n\t}\n\t// append user indices\n\tif len(dataset.Users) > 0 {\n\t\tuserFeatures := dataset.UserLabels[dataset.Users[i]]\n\t\tfor _, feature := range userFeatures {\n\t\t\tindices = append(indices, position+feature.A)\n\t\t\tvalues = append(values, feature.B)\n\t\t}\n\t\tposition += dataset.Index.CountUserLabels()\n\t}\n\t// append item indices\n\tif len(dataset.Items) > 0 {\n\t\titemFeatures := dataset.ItemLabels[dataset.Items[i]]\n\t\tfor _, feature := range itemFeatures {\n\t\t\tindices = append(indices, position+feature.A)\n\t\t\tvalues = append(values, feature.B)\n\t\t}\n\t}\n\t// append context indices\n\tif dataset.ContextLabels != nil {\n\t\tcontextIndices, contextValues := lo.Unzip2(dataset.ContextLabels[i])\n\t\tindices = append(indices, contextIndices...)\n\t\tvalues = append(values, contextValues...)\n\t}\n\treturn indices, values, embedding, dataset.Target[i]\n}\n\n// LoadLibFMFile loads libFM format file.\nfunc LoadLibFMFile(path string) (features [][]lo.Tuple2[int32, float32], targets []float32, maxLabel int32, err error) {\n\t// open file\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn nil, []float32{}, 0, errors.Trace(err)\n\t}\n\tdefer file.Close()\n\t// read lines\n\tscanner := bufio.NewScanner(file)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tfields := strings.Split(line, \" \")\n\t\t// fetch target\n\t\ttarget, err := strconv.ParseFloat(fields[0], 32)\n\t\tif err != nil {\n\t\t\treturn nil, []float32{}, 0, errors.Trace(err)\n\t\t}\n\t\ttargets = append(targets, float32(target))\n\t\t// fetch features\n\t\tlineFeatures := make([]lo.Tuple2[int32, float32], 0, len(fields[1:]))\n\t\tfor _, field := range fields[1:] {\n\t\t\tif len(strings.TrimSpace(field)) > 0 {\n\t\t\t\tkv := strings.Split(field, \":\")\n\t\t\t\tk, v := kv[0], kv[1]\n\t\t\t\t// append feature\n\t\t\t\tfeature, err := strconv.Atoi(k)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, []float32{}, 0, errors.Trace(err)\n\t\t\t\t}\n\t\t\t\tvalue, err := strconv.ParseFloat(v, 32)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, []float32{}, 0, errors.Trace(err)\n\t\t\t\t}\n\t\t\t\tlineFeatures = append(lineFeatures, lo.Tuple2[int32, float32]{\n\t\t\t\t\tA: int32(feature),\n\t\t\t\t\tB: float32(value),\n\t\t\t\t})\n\t\t\t\tmaxLabel = mathutil.MaxInt32Val(maxLabel, int32(feature))\n\t\t\t}\n\t\t}\n\t\tfeatures = append(features, lineFeatures)\n\t}\n\t// check error\n\tif err = scanner.Err(); err != nil {\n\t\treturn nil, []float32{}, 0, errors.Trace(err)\n\t}\n\treturn\n}\n\n// LoadDataFromBuiltIn loads built-in dataset.\nfunc LoadDataFromBuiltIn(name string) (train, test *Dataset, err error) {\n\ttrainFilePath, testFilePath, err := model.LocateBuiltInDataset(name, model.FormatLibFM)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\ttrain, test = &Dataset{}, &Dataset{}\n\tvar trainMaxLabel, testMaxLabel int32\n\tif train.ContextLabels, train.Target, trainMaxLabel, err = LoadLibFMFile(trainFilePath); err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif test.ContextLabels, test.Target, testMaxLabel, err = LoadLibFMFile(testFilePath); err != nil {\n\t\treturn nil, nil, err\n\t}\n\tunifiedIndex := dataset.NewUnifiedDirectIndex(mathutil.MaxInt32(trainMaxLabel, testMaxLabel) + 1)\n\ttrain.Index = unifiedIndex\n\ttest.Index = unifiedIndex\n\treturn\n}\n\n// Split a dataset to training set and test set.\nfunc (dataset *Dataset) Split(ratio float32, seed int64) (*Dataset, *Dataset) {\n\t// create train/test dataset\n\ttrainSet := &Dataset{\n\t\tIndex:                  dataset.Index,\n\t\tUserLabels:             dataset.UserLabels,\n\t\tItemLabels:             dataset.ItemLabels,\n\t\tItemEmbeddings:         dataset.ItemEmbeddings,\n\t\tItemEmbeddingIndex:     dataset.ItemEmbeddingIndex,\n\t\tItemEmbeddingDimension: dataset.ItemEmbeddingDimension,\n\t}\n\ttestSet := &Dataset{\n\t\tIndex:                  dataset.Index,\n\t\tUserLabels:             dataset.UserLabels,\n\t\tItemLabels:             dataset.ItemLabels,\n\t\tItemEmbeddings:         dataset.ItemEmbeddings,\n\t\tItemEmbeddingIndex:     dataset.ItemEmbeddingIndex,\n\t\tItemEmbeddingDimension: dataset.ItemEmbeddingDimension,\n\t}\n\t// split by random\n\tnumTestSize := int(float32(dataset.Count()) * ratio)\n\trng := util.NewRandomGenerator(seed)\n\tsampledIndex := mapset.NewSet(rng.Sample(0, dataset.Count(), numTestSize)...)\n\tfor i := 0; i < len(dataset.Target); i++ {\n\t\tif sampledIndex.Contains(i) {\n\t\t\t// add samples into test set\n\t\t\ttestSet.Users = append(testSet.Users, dataset.Users[i])\n\t\t\ttestSet.Items = append(testSet.Items, dataset.Items[i])\n\t\t\tif dataset.ContextLabels != nil {\n\t\t\t\ttestSet.ContextLabels = append(testSet.ContextLabels, dataset.ContextLabels[i])\n\t\t\t}\n\t\t\ttestSet.Target = append(testSet.Target, dataset.Target[i])\n\t\t\tif dataset.Target[i] > 0 {\n\t\t\t\ttestSet.PositiveCount++\n\t\t\t} else {\n\t\t\t\ttestSet.NegativeCount++\n\t\t\t}\n\t\t} else {\n\t\t\t// add samples into train set\n\t\t\ttrainSet.Users = append(trainSet.Users, dataset.Users[i])\n\t\t\ttrainSet.Items = append(trainSet.Items, dataset.Items[i])\n\t\t\tif dataset.ContextLabels != nil {\n\t\t\t\ttrainSet.ContextLabels = append(trainSet.ContextLabels, dataset.ContextLabels[i])\n\t\t\t}\n\t\t\ttrainSet.Target = append(trainSet.Target, dataset.Target[i])\n\t\t\tif dataset.Target[i] > 0 {\n\t\t\t\ttrainSet.PositiveCount++\n\t\t\t} else {\n\t\t\t\ttrainSet.NegativeCount++\n\t\t\t}\n\t\t}\n\t}\n\treturn trainSet, testSet\n}\n\nfunc (dataset *Dataset) GetItemEmbeddingDim() []int {\n\treturn dataset.ItemEmbeddingDimension\n}\n\nfunc (dataset *Dataset) GetItemEmbeddingIndex() *dataset.Index {\n\treturn dataset.ItemEmbeddingIndex\n}\n"
  },
  {
    "path": "model/ctr/data_test.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage ctr\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/samber/lo\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestConvertLabels(t *testing.T) {\n\tfeatures := ConvertLabels(nil)\n\tassert.Nil(t, features)\n\n\t// categorical features\n\tfeatures = ConvertLabels(\"label\")\n\tassert.ElementsMatch(t, []Label{{Name: \"label\", Value: 1}}, features)\n\tfeatures = ConvertLabels([]any{\"1\", \"2\", \"3\"})\n\tassert.ElementsMatch(t, []Label{\n\t\t{Name: \"1\", Value: 1},\n\t\t{Name: \"2\", Value: 1},\n\t\t{Name: \"3\", Value: 1},\n\t}, features)\n\tfeatures = ConvertLabels(map[string]any{\"city\": \"wenzhou\", \"tags\": []any{\"1\", \"2\", \"3\"}})\n\tassert.ElementsMatch(t, []Label{\n\t\t{Name: \"city.wenzhou\", Value: 1},\n\t\t{Name: \"tags.1\", Value: 1},\n\t\t{Name: \"tags.2\", Value: 1},\n\t\t{Name: \"tags.3\", Value: 1},\n\t}, features)\n\tfeatures = ConvertLabels(map[string]any{\"address\": map[string]any{\"province\": \"zhejiang\", \"city\": \"wenzhou\"}})\n\tassert.ElementsMatch(t, []Label{\n\t\t{Name: \"address.province.zhejiang\", Value: 1},\n\t\t{Name: \"address.city.wenzhou\", Value: 1},\n\t}, features)\n\n\t// numerical features\n\tfeatures = ConvertLabels(json.Number(\"1\"))\n\tassert.Equal(t, []Label{{Name: \"\", Value: 1}}, features)\n\tfeatures = ConvertLabels(map[string]any{\"city\": \"wenzhou\", \"tags\": json.Number(\"0.5\")})\n\tassert.ElementsMatch(t, []Label{\n\t\t{Name: \"city.wenzhou\", Value: 1},\n\t\t{Name: \"tags.\", Value: 0.5},\n\t}, features)\n\n\t// not supported\n\tfeatures = ConvertLabels([]any{float64(1), float64(2), float64(3)})\n\tassert.Empty(t, features)\n\tfeatures = ConvertLabels(map[string]any{\"city\": \"wenzhou\", \"tags\": []any{float64(1), float64(2), float64(3)}})\n\tassert.ElementsMatch(t, []Label{{Name: \"city.wenzhou\", Value: 1}}, features)\n}\n\nfunc TestConvertEmbeddings(t *testing.T) {\n\tembeddings := ConvertEmbeddings(nil)\n\tassert.Nil(t, embeddings)\n\n\tembeddings = ConvertEmbeddings([]float32{1, 2, 3})\n\tif assert.Len(t, embeddings, 1) {\n\t\tassert.Equal(t, \"\", embeddings[0].Name)\n\t\tassert.Equal(t, []float32{1, 2, 3}, embeddings[0].Value)\n\t}\n\n\tembeddings = ConvertEmbeddings([]float64{1, 2, 3})\n\tif assert.Len(t, embeddings, 1) {\n\t\tassert.Equal(t, \"\", embeddings[0].Name)\n\t\tassert.Equal(t, []float32{1, 2, 3}, embeddings[0].Value)\n\t}\n\n\tembeddings = ConvertEmbeddings([]any{float64(1), float32(2), float64(3)})\n\tif assert.Len(t, embeddings, 1) {\n\t\tassert.Equal(t, \"\", embeddings[0].Name)\n\t\tassert.Equal(t, []float32{1, 2, 3}, embeddings[0].Value)\n\t}\n\n\tembeddings = ConvertEmbeddings(map[string]any{\n\t\t\"embedding1\": []float32{1, 2, 3},\n\t\t\"a\": map[string]any{\n\t\t\t\"embedding2\": []float64{4, 5, 6},\n\t\t},\n\t\t\"no_embedding\": \"test\",\n\t})\n\tif assert.Len(t, embeddings, 2) {\n\t\tassert.ElementsMatch(t, []Embedding{\n\t\t\t{Name: \"embedding1\", Value: []float32{1, 2, 3}},\n\t\t\t{Name: \"a.embedding2\", Value: []float32{4, 5, 6}},\n\t\t}, embeddings)\n\t}\n}\n\nfunc TestLoadDataFromBuiltIn(t *testing.T) {\n\ttrain, test, err := LoadDataFromBuiltIn(\"frappe\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, 202027, train.Count())\n\tassert.Equal(t, 28860, test.Count())\n}\n\nfunc TestDataset_Split(t *testing.T) {\n\t// create dataset\n\tunifiedIndex := dataset.NewUnifiedMapIndexBuilder()\n\tdataSet := NewMapIndexDataset()\n\tnumUsers, numItems := 5, 6\n\tfor i := 0; i < numUsers; i++ {\n\t\tunifiedIndex.AddUser(fmt.Sprintf(\"user%v\", i))\n\t\tunifiedIndex.AddUserLabel(fmt.Sprintf(\"user_label%v\", 2*i))\n\t\tunifiedIndex.AddUserLabel(fmt.Sprintf(\"user_label%v\", 2*i+1))\n\t\tdataSet.UserLabels = append(dataSet.UserLabels, []lo.Tuple2[int32, float32]{\n\t\t\t{A: int32(2 * i), B: 1},\n\t\t\t{A: int32(2*i + 1), B: 1},\n\t\t})\n\t}\n\tfor i := 0; i < numItems; i++ {\n\t\tunifiedIndex.AddItem(fmt.Sprintf(\"item%v\", i))\n\t\tunifiedIndex.AddItemLabel(fmt.Sprintf(\"item_label%v\", 3*i))\n\t\tunifiedIndex.AddItemLabel(fmt.Sprintf(\"item_label%v\", 3*i+1))\n\t\tunifiedIndex.AddItemLabel(fmt.Sprintf(\"item_label%v\", 3*i+2))\n\t\tdataSet.ItemLabels = append(dataSet.ItemLabels, []lo.Tuple2[int32, float32]{\n\t\t\t{A: int32(3 * i), B: 1},\n\t\t\t{A: int32(3*i + 1), B: 1},\n\t\t\t{A: int32(3*i + 2), B: 1},\n\t\t})\n\t\tdataSet.ItemEmbeddings = append(dataSet.ItemEmbeddings, [][]float32{\n\t\t\t{float32(i), float32(i) + 0.1, float32(i) + 0.2},\n\t\t})\n\t}\n\tfor i := 0; i < numUsers; i++ {\n\t\tfor j := 0; j < numItems; j++ {\n\t\t\tif i+j > 4 {\n\t\t\t\tdataSet.Users = append(dataSet.Users, int32(i))\n\t\t\t\tdataSet.Items = append(dataSet.Items, int32(j))\n\t\t\t\tdataSet.ContextLabels = append(dataSet.ContextLabels, []lo.Tuple2[int32, float32]{{A: int32(i * j), B: 0.5}})\n\t\t\t\tdataSet.Target = append(dataSet.Target, 1)\n\t\t\t\tdataSet.PositiveCount++\n\t\t\t} else {\n\t\t\t\tdataSet.Users = append(dataSet.Users, int32(i))\n\t\t\t\tdataSet.Items = append(dataSet.Items, int32(j))\n\t\t\t\tdataSet.ContextLabels = append(dataSet.ContextLabels, []lo.Tuple2[int32, float32]{{A: int32(i * j), B: 0.5}})\n\t\t\t\tdataSet.Target = append(dataSet.Target, -1)\n\t\t\t\tdataSet.NegativeCount++\n\t\t\t}\n\t\t}\n\t}\n\tdataSet.Index = unifiedIndex.Build()\n\n\tassert.Equal(t, numUsers*numItems, dataSet.Count())\n\tassert.Equal(t, numUsers, dataSet.CountUsers())\n\tassert.Equal(t, numItems, dataSet.CountItems())\n\tassert.Equal(t, numUsers*numItems/2, dataSet.PositiveCount)\n\tassert.Equal(t, numUsers*numItems/2, dataSet.NegativeCount)\n\n\tfeatures, values, embeddings, target := dataSet.Get(2)\n\tassert.Equal(t, []int32{\n\t\t0,\n\t\tdataSet.Index.CountUsers() + 2,\n\t\tdataSet.Index.CountUsers() + dataSet.Index.CountItems() + 0,\n\t\tdataSet.Index.CountUsers() + dataSet.Index.CountItems() + 1,\n\t\tdataSet.Index.CountUsers() + dataSet.Index.CountItems() + dataSet.Index.CountUserLabels() + 6,\n\t\tdataSet.Index.CountUsers() + dataSet.Index.CountItems() + dataSet.Index.CountUserLabels() + 7,\n\t\tdataSet.Index.CountUsers() + dataSet.Index.CountItems() + dataSet.Index.CountUserLabels() + 8,\n\t\t0,\n\t}, features)\n\tassert.Equal(t, [][]float32{{2, 2.1, 2.2}}, embeddings)\n\tassert.Equal(t, []float32{1, 1, 1, 1, 1, 1, 1, 0.5}, values)\n\tassert.Equal(t, float32(-1), target)\n\n\t// split\n\ttrain, test := dataSet.Split(0.2, 0)\n\tassert.Equal(t, numUsers, train.CountUsers())\n\tassert.Equal(t, numItems, train.CountItems())\n\tassert.Equal(t, 24, train.Count())\n\tassert.Equal(t, 12, train.PositiveCount)\n\tassert.Equal(t, 12, train.NegativeCount)\n\tassert.Equal(t, numUsers, test.CountUsers())\n\tassert.Equal(t, numItems, test.CountItems())\n\tassert.Equal(t, 6, test.Count())\n\tassert.Equal(t, 3, test.PositiveCount)\n\tassert.Equal(t, 3, test.NegativeCount)\n}\n"
  },
  {
    "path": "model/ctr/evaluator.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage ctr\n\nimport (\n\t\"sort\"\n\n\t\"github.com/chewxy/math32\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/samber/lo\"\n\t\"modernc.org/sortutil\"\n)\n\n// EvaluateRegression evaluates factorization machines in regression task.\nfunc EvaluateRegression(estimator FactorizationMachines, testSet *Dataset) Score {\n\tsum := float32(0)\n\t// For all UserFeedback\n\tfor i := 0; i < testSet.Count(); i++ {\n\t\tfeatures, values, _, target := testSet.Get(i)\n\t\tprediction := estimator.InternalPredict(features, values)\n\t\tsum += (target - prediction) * (target - prediction)\n\t}\n\tif testSet.Count() == 0 {\n\t\treturn Score{\n\t\t\tRMSE: 0,\n\t\t}\n\t}\n\treturn Score{\n\t\tRMSE: math32.Sqrt(sum / float32(testSet.Count())),\n\t}\n}\n\n// EvaluateClassification evaluates factorization machines in classification task.\nfunc EvaluateClassification(estimator FactorizationMachines, testSet dataset.CTRSplit, jobs int) Score {\n\t// For all UserFeedback\n\tvar posFeatures, negFeatures []lo.Tuple2[[]int32, []float32]\n\tvar posEmbeddings, negEmbeddings [][][]float32\n\tfor i := 0; i < testSet.Count(); i++ {\n\t\tindices, values, embeddings, target := testSet.Get(i)\n\t\tif target > 0 {\n\t\t\tposFeatures = append(posFeatures, lo.Tuple2[[]int32, []float32]{A: indices, B: values})\n\t\t\tposEmbeddings = append(posEmbeddings, embeddings)\n\t\t} else {\n\t\t\tnegFeatures = append(negFeatures, lo.Tuple2[[]int32, []float32]{A: indices, B: values})\n\t\t\tnegEmbeddings = append(negEmbeddings, embeddings)\n\t\t}\n\t}\n\tvar posPrediction, negPrediction []float32\n\tif batchInference, ok := estimator.(BatchInference); ok {\n\t\tposPrediction = batchInference.BatchInternalPredict(posFeatures, posEmbeddings, jobs)\n\t\tnegPrediction = batchInference.BatchInternalPredict(negFeatures, negEmbeddings, jobs)\n\t} else {\n\t\tfor _, features := range posFeatures {\n\t\t\tposPrediction = append(posPrediction, estimator.InternalPredict(features.A, features.B))\n\t\t}\n\t\tfor _, features := range negFeatures {\n\t\t\tnegPrediction = append(negPrediction, estimator.InternalPredict(features.A, features.B))\n\t\t}\n\t}\n\tif testSet.Count() == 0 {\n\t\treturn Score{\n\t\t\tPrecision: 0,\n\t\t}\n\t}\n\treturn Score{\n\t\tPrecision: Precision(posPrediction, negPrediction),\n\t\tRecall:    Recall(posPrediction, negPrediction),\n\t\tAccuracy:  Accuracy(posPrediction, negPrediction),\n\t\tAUC:       AUC(posPrediction, negPrediction),\n\t}\n}\n\nfunc Precision(posPrediction, negPrediction []float32) float32 {\n\tvar tp, fp float32\n\tfor _, p := range posPrediction {\n\t\tif p > 0 { // true positive\n\t\t\ttp++\n\t\t}\n\t}\n\tfor _, p := range negPrediction {\n\t\tif p > 0 { // false positive\n\t\t\tfp++\n\t\t}\n\t}\n\tif tp+fp == 0 {\n\t\treturn 0\n\t}\n\treturn tp / (tp + fp)\n}\n\nfunc Recall(posPrediction, _ []float32) float32 {\n\tvar tp, fn float32\n\tfor _, p := range posPrediction {\n\t\tif p > 0 { // true positive\n\t\t\ttp++\n\t\t} else { // false negative\n\t\t\tfn++\n\t\t}\n\t}\n\tif tp+fn == 0 {\n\t\treturn 0\n\t}\n\treturn tp / (tp + fn)\n}\n\nfunc Accuracy(posPrediction, negPrediction []float32) float32 {\n\tvar correct float32\n\tfor _, p := range posPrediction {\n\t\tif p > 0 {\n\t\t\tcorrect++\n\t\t}\n\t}\n\tfor _, p := range negPrediction {\n\t\tif p < 0 {\n\t\t\tcorrect++\n\t\t}\n\t}\n\tif len(posPrediction)+len(negPrediction) == 0 {\n\t\treturn 0\n\t}\n\treturn correct / float32(len(posPrediction)+len(negPrediction))\n}\n\nfunc AUC(posPrediction, negPrediction []float32) float32 {\n\tsort.Sort(sortutil.Float32Slice(posPrediction))\n\tsort.Sort(sortutil.Float32Slice(negPrediction))\n\tvar sum float32\n\tvar nPos int\n\tfor pPos := range posPrediction {\n\t\t// find the negative sample with the greatest prediction less than current positive sample\n\t\tfor nPos < len(negPrediction) && negPrediction[nPos] < posPrediction[pPos] {\n\t\t\tnPos++\n\t\t}\n\t\t// add the number of negative samples have less prediction than current positive sample\n\t\tsum += float32(nPos)\n\t}\n\tif len(posPrediction)*len(negPrediction) == 0 {\n\t\treturn 0\n\t}\n\treturn sum / float32(len(posPrediction)*len(negPrediction))\n}\n"
  },
  {
    "path": "model/ctr/evaluator_test.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage ctr\n\nimport (\n\t\"github.com/stretchr/testify/assert\"\n\t\"testing\"\n)\n\nfunc TestPrecision(t *testing.T) {\n\tposPrediction := []float32{1, 1, 1}\n\tnegPrediction := []float32{1}\n\tprecision := Precision(posPrediction, negPrediction)\n\tassert.Equal(t, float32(0.75), precision)\n\tprecision = Precision(nil, nil)\n\tassert.Zero(t, precision)\n}\n\nfunc TestRecall(t *testing.T) {\n\tposPrediction := []float32{1, -1, -1, -1}\n\trecall := Recall(posPrediction, nil)\n\tassert.Equal(t, float32(0.25), recall)\n\trecall = Recall(nil, nil)\n\tassert.Zero(t, recall)\n}\n\nfunc TestAccuracy(t *testing.T) {\n\tposPrediction := []float32{1, 1, -1, -1}\n\tnegPrediction := []float32{1, 1, -1, -1}\n\taccuracy := Accuracy(posPrediction, negPrediction)\n\tassert.Equal(t, float32(0.5), accuracy)\n\taccuracy = Accuracy(nil, nil)\n\tassert.Zero(t, accuracy)\n}\n"
  },
  {
    "path": "model/ctr/fm.go",
    "content": "//go:build !cgo || !xla\n\n// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage ctr\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/c-bata/goptuna\"\n\t\"github.com/chewxy/math32\"\n\t\"github.com/gorse-io/gorse/common/encoding\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/common/monitor\"\n\t\"github.com/gorse-io/gorse/common/nn\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/model\"\n\t\"github.com/juju/errors\"\n\t\"github.com/samber/lo\"\n\t\"go.uber.org/zap\"\n\t\"modernc.org/mathutil\"\n)\n\nconst headerAFM = \"AFM\"\n\ntype AFM struct {\n\tBaseFactorizationMachines\n\tmu sync.RWMutex\n\t// parameters\n\tB *nn.Tensor\n\tW nn.Layer\n\tV nn.Layer\n\tA []nn.Layer\n\tE []nn.Layer\n\t// hyper parameters\n\tbatchSize  int\n\tnFactors   int\n\tnEpochs    int\n\tlr         float32\n\treg        float32\n\tinitMean   float32\n\tinitStdDev float32\n\toptimizer  string\n\t// dataset stats\n\tnumFeatures    int\n\tnumDimension   int\n\tembeddingDim   []int\n\tembeddingIndex *dataset.Index\n}\n\nfunc NewAFM(params model.Params) *AFM {\n\tfm := new(AFM)\n\tfm.SetParams(params)\n\treturn fm\n}\n\nfunc (fm *AFM) SuggestParams(trial goptuna.Trial) model.Params {\n\treturn model.Params{\n\t\tmodel.NFactors:   16,\n\t\tmodel.Lr:         lo.Must(trial.SuggestLogFloat(string(model.Lr), 0.001, 0.1)),\n\t\tmodel.Reg:        lo.Must(trial.SuggestLogFloat(string(model.Reg), 0.001, 0.1)),\n\t\tmodel.InitMean:   0,\n\t\tmodel.InitStdDev: lo.Must(trial.SuggestLogFloat(string(model.InitStdDev), 0.001, 0.1)),\n\t}\n}\n\nfunc (fm *AFM) SetParams(params model.Params) {\n\tfm.BaseFactorizationMachines.SetParams(params)\n\tfm.batchSize = fm.Params.GetInt(model.BatchSize, 1024)\n\tfm.nFactors = fm.Params.GetInt(model.NFactors, 16)\n\tfm.nEpochs = fm.Params.GetInt(model.NEpochs, 50)\n\tfm.lr = fm.Params.GetFloat32(model.Lr, 0.001)\n\tfm.reg = fm.Params.GetFloat32(model.Reg, 0.0002)\n\tfm.initMean = fm.Params.GetFloat32(model.InitMean, 0)\n\tfm.initStdDev = fm.Params.GetFloat32(model.InitStdDev, 0.01)\n\tfm.optimizer = fm.Params.GetString(model.Optimizer, model.Adam)\n}\n\nfunc (fm *AFM) Clear() {\n\tfm.Index = nil\n}\n\nfunc (fm *AFM) Invalid() bool {\n\treturn fm == nil || fm.Index == nil\n}\n\nfunc (fm *AFM) Forward(indices, values *nn.Tensor, embeddings []*nn.Tensor, jobs int) *nn.Tensor {\n\tbatchSize := indices.Shape()[0]\n\tv := fm.V.Forward(indices)\n\tx := nn.Reshape(values, batchSize, fm.numDimension, 1)\n\tvx := nn.BMM(v, x, true, false, jobs)\n\tsumSquare := nn.Square(vx)\n\te2 := nn.Square(v)\n\tx2 := nn.Square(x)\n\tsquareSum := nn.BMM(e2, x2, true, false, jobs)\n\tsum := nn.Sub(sumSquare, squareSum)\n\tsum = nn.Sum(sum, 1)\n\tsum = nn.Mul(sum, nn.NewScalar(0.5))\n\tw := fm.W.Forward(indices)\n\tlinear := nn.BMM(w, x, true, false, jobs)\n\tfmOutput := nn.Add(nn.Reshape(linear, batchSize), nn.Reshape(sum, batchSize), fm.B)\n\t// Encode the embedding\n\tfor i, embedding := range embeddings {\n\t\tencodedNorm := fm.E[i].Forward(fm.A[i].Forward(embedding))\n\t\tencodedNorm = nn.Reshape(encodedNorm, batchSize, fm.nFactors, 1)\n\t\tfmOutput = nn.Add(fmOutput, nn.Reshape(nn.BMM(vx, encodedNorm, true, false, jobs), batchSize))\n\t}\n\treturn nn.Flatten(fmOutput)\n}\n\nfunc (fm *AFM) Parameters() []*nn.Tensor {\n\tvar params []*nn.Tensor\n\tparams = append(params, fm.B)\n\tparams = append(params, fm.V.Parameters()...)\n\tparams = append(params, fm.W.Parameters()...)\n\tfor i := range fm.embeddingDim {\n\t\tparams = append(params, fm.A[i].Parameters()...)\n\t\tparams = append(params, fm.E[i].Parameters()...)\n\t}\n\treturn params\n}\n\nfunc (fm *AFM) Predict(_, _ string, _, _ []Label) float32 {\n\tpanic(\"Predict is unsupported for deep learning models\")\n}\n\nfunc (fm *AFM) InternalPredict(_ []int32, _ []float32) float32 {\n\tpanic(\"InternalPredict is unsupported for deep learning models\")\n}\n\nfunc (fm *AFM) BatchInternalPredict(x []lo.Tuple2[[]int32, []float32], e [][][]float32, jobs int) []float32 {\n\tfm.mu.RLock()\n\tdefer fm.mu.RUnlock()\n\tindicesTensor, valuesTensor, embeddingTensor, _ := fm.convertToTensors(x, e, nil)\n\tpredictions := make([]float32, 0, len(x))\n\tfor i := 0; i < len(x); i += fm.batchSize {\n\t\tj := mathutil.Min(i+fm.batchSize, len(x))\n\t\tembeddingTensorSlice := make([]*nn.Tensor, len(fm.embeddingDim))\n\t\tfor k := range fm.embeddingDim {\n\t\t\tembeddingTensorSlice[k] = embeddingTensor[k].Slice(i, j)\n\t\t}\n\t\toutput := fm.Forward(indicesTensor.Slice(i, j), valuesTensor.Slice(i, j), embeddingTensorSlice, jobs)\n\t\tpredictions = append(predictions, output.Data()...)\n\t}\n\treturn predictions[:len(x)]\n}\n\nfunc (fm *AFM) BatchPredict(inputs []lo.Tuple4[string, string, []Label, []Label], embeddings [][]Embedding, jobs int) []float32 {\n\tx := make([]lo.Tuple2[[]int32, []float32], len(inputs))\n\tfor i, input := range inputs {\n\t\t// encode user\n\t\tif userIndex := fm.Index.EncodeUser(input.A); userIndex != dataset.NotId {\n\t\t\tx[i].A = append(x[i].A, userIndex)\n\t\t\tx[i].B = append(x[i].B, 1)\n\t\t}\n\t\t// encode item\n\t\tif itemIndex := fm.Index.EncodeItem(input.B); itemIndex != dataset.NotId {\n\t\t\tx[i].A = append(x[i].A, itemIndex)\n\t\t\tx[i].B = append(x[i].B, 1)\n\t\t}\n\t\t// encode user labels\n\t\tfor _, userFeature := range input.C {\n\t\t\tif userFeatureIndex := fm.Index.EncodeUserLabel(userFeature.Name); userFeatureIndex != dataset.NotId {\n\t\t\t\tx[i].A = append(x[i].A, userFeatureIndex)\n\t\t\t\tx[i].B = append(x[i].B, userFeature.Value)\n\t\t\t}\n\t\t}\n\t\t// encode item labels\n\t\tfor _, itemFeature := range input.D {\n\t\t\tif itemFeatureIndex := fm.Index.EncodeItemLabel(itemFeature.Name); itemFeatureIndex != dataset.NotId {\n\t\t\t\tx[i].A = append(x[i].A, itemFeatureIndex)\n\t\t\t\tx[i].B = append(x[i].B, itemFeature.Value)\n\t\t\t}\n\t\t}\n\t}\n\te := make([][][]float32, len(inputs))\n\tfor i := range inputs {\n\t\te[i] = make([][]float32, len(fm.embeddingDim))\n\t\tfor _, embedding := range embeddings[i] {\n\t\t\titemIndex := fm.embeddingIndex.ToNumber(embedding.Name)\n\t\t\tif itemIndex == dataset.NotId {\n\t\t\t\t// unknown embedding\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tindex := int(itemIndex)\n\t\t\tif len(embedding.Value) != fm.embeddingDim[index] {\n\t\t\t\t// dimension mismatch\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\te[i][index] = embedding.Value\n\t\t}\n\t}\n\treturn fm.BatchInternalPredict(x, e, jobs)\n}\n\nfunc (fm *AFM) Init(trainSet dataset.CTRSplit) {\n\tfm.numFeatures = int(trainSet.GetIndex().Len())\n\tfm.numDimension = 0\n\tfor i := 0; i < trainSet.Count(); i++ {\n\t\t_, x, _, _ := trainSet.Get(i)\n\t\tfm.numDimension = mathutil.MaxVal(fm.numDimension, len(x))\n\t}\n\tfm.B = nn.Zeros()\n\tfm.W = nn.NewEmbedding(int(trainSet.GetIndex().Len()), 1)\n\tfm.V = nn.NewEmbedding(int(trainSet.GetIndex().Len()), fm.nFactors)\n\tfm.embeddingDim = trainSet.GetItemEmbeddingDim()\n\tfm.embeddingIndex = trainSet.GetItemEmbeddingIndex()\n\tfm.A = make([]nn.Layer, len(fm.embeddingDim))\n\tfm.E = make([]nn.Layer, len(fm.embeddingDim))\n\tfor i, dim := range fm.embeddingDim {\n\t\tfm.A[i] = nn.NewAttention(dim, fm.nFactors)\n\t\tfm.E[i] = nn.NewLinear(dim, fm.nFactors)\n\t}\n\tfm.BaseFactorizationMachines.Init(trainSet)\n}\n\nfunc (fm *AFM) Fit(ctx context.Context, trainSet, testSet dataset.CTRSplit, config *FitConfig) Score {\n\tlog.Logger().Info(\"fit AFM\",\n\t\tzap.Int(\"train_set_size\", trainSet.Count()),\n\t\tzap.Int(\"test_set_size\", testSet.Count()),\n\t\tzap.Any(\"params\", fm.GetParams()),\n\t\tzap.Any(\"config\", config))\n\tfm.Init(trainSet)\n\tfm.W.SetJobs(config.Jobs)\n\tfm.V.SetJobs(config.Jobs)\n\tfor i := range fm.embeddingDim {\n\t\tfm.A[i].SetJobs(config.Jobs)\n\t\tfm.E[i].SetJobs(config.Jobs)\n\t}\n\n\tevalStart := time.Now()\n\tscore := EvaluateClassification(fm, testSet, config.Jobs)\n\tscores := []lo.Tuple2[int, float32]{{A: 0, B: score.AUC}}\n\tevalTime := time.Since(evalStart)\n\tfields := append([]zap.Field{zap.String(\"eval_time\", evalTime.String())}, score.ZapFields()...)\n\tlog.Logger().Info(fmt.Sprintf(\"fit AFM %v/%v\", 0, fm.nEpochs), fields...)\n\n\tvar x []lo.Tuple2[[]int32, []float32]\n\tvar e [][][]float32\n\tvar y []float32\n\tfor i := 0; i < trainSet.Count(); i++ {\n\t\tindices, values, embeddings, target := trainSet.Get(i)\n\t\tx = append(x, lo.Tuple2[[]int32, []float32]{A: indices, B: values})\n\t\te = append(e, embeddings)\n\t\ty = append(y, target)\n\t}\n\tindices, values, embeddings, target := fm.convertToTensors(x, e, y)\n\n\tvar optimizer nn.Optimizer\n\tswitch fm.optimizer {\n\tcase model.SGD:\n\t\toptimizer = nn.NewSGD(fm.Parameters(), fm.lr)\n\tcase model.Adam:\n\t\toptimizer = nn.NewAdam(fm.Parameters(), fm.lr)\n\tdefault:\n\t\tpanic(\"unknown optimizer\")\n\t}\n\toptimizer.SetWeightDecay(fm.reg)\n\toptimizer.SetJobs(config.Jobs)\n\t_, span := monitor.Start(ctx, \"FM.Fit\", fm.nEpochs)\n\tdefer span.End()\n\tfor epoch := 1; epoch <= fm.nEpochs; epoch++ {\n\t\tfitStart := time.Now()\n\t\tcost := float32(0)\n\t\tfor i := 0; i < trainSet.Count(); i += fm.batchSize {\n\t\t\tif ctx.Err() != nil {\n\t\t\t\tlog.Logger().Info(\"fit AFM canceled\", zap.Error(ctx.Err()))\n\t\t\t\treturn Score{}\n\t\t\t}\n\t\t\tj := mathutil.Min(i+fm.batchSize, trainSet.Count())\n\t\t\tbatchIndices := indices.Slice(i, j)\n\t\t\tbatchValues := values.Slice(i, j)\n\t\t\tbatchEmbedding := make([]*nn.Tensor, len(fm.embeddingDim))\n\t\t\tfor k := range fm.embeddingDim {\n\t\t\t\tbatchEmbedding[k] = embeddings[k].Slice(i, j)\n\t\t\t}\n\t\t\tbatchTarget := target.Slice(i, j)\n\t\t\tbatchOutput := fm.Forward(batchIndices, batchValues, batchEmbedding, config.Jobs)\n\t\t\tbatchLoss := nn.BCEWithLogits(batchTarget, batchOutput, nil)\n\t\t\tcost += batchLoss.Data()[0]\n\t\t\toptimizer.ZeroGrad()\n\t\t\tbatchLoss.Backward()\n\t\t\toptimizer.Step()\n\t\t}\n\n\t\tfitTime := time.Since(fitStart)\n\t\t// Cross validation\n\t\tif epoch%config.Verbose == 0 || epoch == fm.nEpochs {\n\t\t\tevalStart = time.Now()\n\t\t\tscore = EvaluateClassification(fm, testSet, config.Jobs)\n\t\t\tscores = append(scores, lo.Tuple2[int, float32]{A: epoch, B: score.AUC})\n\t\t\tevalTime = time.Since(evalStart)\n\t\t\tfields = append([]zap.Field{\n\t\t\t\tzap.String(\"fit_time\", fitTime.String()),\n\t\t\t\tzap.String(\"eval_time\", evalTime.String()),\n\t\t\t\tzap.Float32(\"loss\", cost),\n\t\t\t}, score.ZapFields()...)\n\t\t\tlog.Logger().Info(fmt.Sprintf(\"fit AFM %v/%v\", epoch, fm.nEpochs), fields...)\n\t\t\t// check NaN\n\t\t\tif math32.IsNaN(cost) || math32.IsNaN(score.GetValue()) {\n\t\t\t\tlog.Logger().Warn(\"model diverged\", zap.Float32(\"lr\", fm.lr))\n\t\t\t\tbreak\n\t\t\t}\n\t\t\t// early stopping if no improvement in last `patience` epochs\n\t\t\tif config.Patience > 0 && epoch > config.Patience {\n\t\t\t\tepochScore := lo.MaxBy(scores, func(a, b lo.Tuple2[int, float32]) bool { return a.B > b.B })\n\t\t\t\tif epochScore.A <= epoch-config.Patience {\n\t\t\t\t\tlog.Logger().Info(\"early stopping\",\n\t\t\t\t\t\tzap.Int(\"best_epoch\", epochScore.A),\n\t\t\t\t\t\tzap.Float32(\"best_auc\", epochScore.B),\n\t\t\t\t\t\tzap.Int(\"patience\", config.Patience))\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tspan.Add(1)\n\t}\n\treturn score\n}\n\nfunc (fm *AFM) Marshal(w io.Writer) error {\n\t// write params\n\tif err := encoding.WriteGob(w, fm.Params); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t// write index\n\tif err := dataset.MarshalUnifiedIndex(w, fm.Index); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t// write dataset stats\n\tif err := encoding.WriteGob(w, fm.numFeatures); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif err := encoding.WriteGob(w, fm.numDimension); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif err := encoding.WriteGob(w, fm.embeddingDim); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif len(fm.embeddingDim) > 0 {\n\t\tif err := dataset.MarshalIndex(w, fm.embeddingIndex); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\t// write parameters\n\tif err := nn.Save(fm.Parameters(), w); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\treturn nil\n}\n\nfunc (fm *AFM) Unmarshal(r io.Reader) error {\n\t// read params\n\terr := encoding.ReadGob(r, &fm.Params)\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tfm.SetParams(fm.Params)\n\t// read index\n\tfm.Index, err = dataset.UnmarshalUnifiedIndex(r)\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t// read dataset stats\n\tif err = encoding.ReadGob(r, &fm.numFeatures); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif err = encoding.ReadGob(r, &fm.numDimension); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif err = encoding.ReadGob(r, &fm.embeddingDim); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif len(fm.embeddingDim) > 0 {\n\t\tfm.embeddingIndex, err = dataset.UnmarshalIndex(r)\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\t// read parameters\n\tfm.B = nn.Zeros()\n\tfm.W = nn.NewEmbedding(fm.numFeatures, 1)\n\tfm.V = nn.NewEmbedding(fm.numFeatures, fm.nFactors)\n\tfm.A = make([]nn.Layer, len(fm.embeddingDim))\n\tfm.E = make([]nn.Layer, len(fm.embeddingDim))\n\tfor i, dim := range fm.embeddingDim {\n\t\tfm.A[i] = nn.NewAttention(dim, fm.nFactors)\n\t\tfm.E[i] = nn.NewLinear(dim, fm.nFactors)\n\t}\n\tif err = nn.Load(fm.Parameters(), r); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\treturn nil\n}\n\nfunc (fm *AFM) convertToTensors(x []lo.Tuple2[[]int32, []float32], e [][][]float32, y []float32) (\n\tindicesTensor, valuesTensor *nn.Tensor,\n\tembeddingTensor []*nn.Tensor,\n\ttargetTensor *nn.Tensor,\n) {\n\tif y != nil && len(x) != len(y) {\n\t\tpanic(\"length of x and y must be equal\")\n\t}\n\n\talignedIndices := make([]float32, len(x)*fm.numDimension)\n\talignedValues := make([]float32, len(x)*fm.numDimension)\n\talignedEmbeddings := make([][]float32, len(fm.embeddingDim))\n\tfor i := range fm.embeddingDim {\n\t\talignedEmbeddings[i] = make([]float32, 0, len(x)*fm.embeddingDim[i])\n\t}\n\talignedTarget := make([]float32, len(x))\n\tfor i := range x {\n\t\tif len(x[i].A) != len(x[i].B) {\n\t\t\tpanic(\"length of indices and values must be equal\")\n\t\t}\n\t\tfor j := range x[i].A {\n\t\t\talignedIndices[i*fm.numDimension+j] = float32(x[i].A[j])\n\t\t\talignedValues[i*fm.numDimension+j] = x[i].B[j]\n\t\t}\n\t\tfor j := range fm.embeddingDim {\n\t\t\tif len(e[i]) > j && len(e[i][j]) == fm.embeddingDim[j] {\n\t\t\t\talignedEmbeddings[j] = append(alignedEmbeddings[j], e[i][j]...)\n\t\t\t} else {\n\t\t\t\talignedEmbeddings[j] = append(alignedEmbeddings[j], make([]float32, fm.embeddingDim[j])...)\n\t\t\t}\n\t\t}\n\t\tif y != nil {\n\t\t\talignedTarget[i] = y[i]\n\t\t}\n\t}\n\n\tindicesTensor = nn.NewTensor(alignedIndices, len(x), fm.numDimension)\n\tvaluesTensor = nn.NewTensor(alignedValues, len(x), fm.numDimension)\n\tembeddingTensor = make([]*nn.Tensor, len(fm.embeddingDim))\n\tfor i := range fm.embeddingDim {\n\t\tembeddingTensor[i] = nn.NewTensor(alignedEmbeddings[i], len(x), fm.embeddingDim[i])\n\t}\n\tif y != nil {\n\t\ttargetTensor = nn.NewTensor(alignedTarget, len(x))\n\t}\n\treturn\n}\n"
  },
  {
    "path": "model/ctr/fm_xla.go",
    "content": "//go:build cgo && xla\n\n// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage ctr\n\nimport (\n\tstd_context \"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/c-bata/goptuna\"\n\t\"github.com/gomlx/gomlx/backends\"\n\t_ \"github.com/gomlx/gomlx/backends/xla\"\n\t\"github.com/gomlx/gomlx/pkg/core/dtypes\"\n\t\"github.com/gomlx/gomlx/pkg/core/graph\"\n\t\"github.com/gomlx/gomlx/pkg/core/shapes\"\n\t\"github.com/gomlx/gomlx/pkg/core/tensors\"\n\tmlx_context \"github.com/gomlx/gomlx/pkg/ml/context\"\n\t\"github.com/gomlx/gomlx/pkg/ml/context/initializers\"\n\t\"github.com/gomlx/gomlx/pkg/ml/layers\"\n\t\"github.com/gomlx/gomlx/pkg/ml/layers/activations\"\n\t\"github.com/gomlx/gomlx/pkg/ml/train\"\n\t\"github.com/gomlx/gomlx/pkg/ml/train/losses\"\n\t\"github.com/gomlx/gomlx/pkg/ml/train/optimizers\"\n\t\"github.com/gorse-io/gorse/common/encoding\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/common/monitor\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/model\"\n\t\"github.com/juju/errors\"\n\t\"github.com/samber/lo\"\n\t\"go.uber.org/zap\"\n\t\"modernc.org/mathutil\"\n)\n\nconst headerAFM = \"AFM\"\n\ntype AFM struct {\n\tBaseFactorizationMachines\n\tmu      sync.RWMutex\n\tctx     *mlx_context.Context\n\tbackend backends.Backend\n\t// hyper parameters\n\tbatchSize  int\n\tnFactors   int\n\tnEpochs    int\n\tlr         float32\n\treg        float32\n\tinitMean   float32\n\tinitStdDev float32\n\toptimizer  string\n\t// dataset stats\n\tnumFeatures    int\n\tnumDimension   int\n\tembeddingDim   []int\n\tembeddingIndex *dataset.Index\n\n\t// compiled executors\n\tpredictExecutor *mlx_context.Exec\n}\n\nfunc NewAFM(params model.Params) *AFM {\n\tfm := new(AFM)\n\tfm.SetParams(params)\n\treturn fm\n}\n\nfunc (fm *AFM) SuggestParams(trial goptuna.Trial) model.Params {\n\treturn model.Params{\n\t\tmodel.NFactors:   16,\n\t\tmodel.Lr:         lo.Must(trial.SuggestLogFloat(string(model.Lr), 0.001, 0.1)),\n\t\tmodel.Reg:        lo.Must(trial.SuggestLogFloat(string(model.Reg), 0.001, 0.1)),\n\t\tmodel.InitMean:   0,\n\t\tmodel.InitStdDev: lo.Must(trial.SuggestLogFloat(string(model.InitStdDev), 0.001, 0.1)),\n\t}\n}\n\nfunc (fm *AFM) SetParams(params model.Params) {\n\tfm.BaseFactorizationMachines.SetParams(params)\n\tfm.batchSize = fm.Params.GetInt(model.BatchSize, 1024)\n\tfm.nFactors = fm.Params.GetInt(model.NFactors, 16)\n\tfm.nEpochs = fm.Params.GetInt(model.NEpochs, 50)\n\tfm.lr = fm.Params.GetFloat32(model.Lr, 0.001)\n\tfm.reg = fm.Params.GetFloat32(model.Reg, 0.0002)\n\tfm.initMean = fm.Params.GetFloat32(model.InitMean, 0)\n\tfm.initStdDev = fm.Params.GetFloat32(model.InitStdDev, 0.01)\n\tfm.optimizer = fm.Params.GetString(model.Optimizer, model.Adam)\n}\n\nfunc (fm *AFM) Clear() {\n\tfm.Index = nil\n}\n\nfunc (fm *AFM) Invalid() bool {\n\treturn fm == nil || fm.Index == nil\n}\n\nfunc (fm *AFM) Predict(_, _ string, _, _ []Label) float32 {\n\tpanic(\"Predict is unsupported for deep learning models\")\n}\n\nfunc (fm *AFM) InternalPredict(_ []int32, _ []float32) float32 {\n\tpanic(\"InternalPredict is unsupported for deep learning models\")\n}\n\nfunc (fm *AFM) attentionForward(ctx *mlx_context.Context, x *graph.Node, dimensions, k int) *graph.Node {\n\tg := x.Graph()\n\t// W: Linear(dimensions -> k)\n\twCtx := ctx.In(\"attention_w\")\n\tw := layers.Dense(wCtx, x, true, k)\n\tw = activations.Relu(w)\n\n\t// H: [k, dimensions]\n\thCtx := ctx.In(\"attention_h\")\n\thVar := hCtx.VariableWithShape(\"H\", shapes.Make(dtypes.F32, k, dimensions))\n\th := hVar.ValueGraph(g)\n\n\t// Softmax(W * H, 1)\n\t// w: [batchSize, k]\n\t// h: [k, dimensions]\n\t// score: [batchSize, dimensions]\n\tscore := graph.Dot(w, h)\n\tscore = graph.Softmax(score, 1)\n\n\t// score * x\n\treturn graph.Mul(score, x)\n}\n\nfunc (fm *AFM) forwardGraph(ctx *mlx_context.Context, indices, values *graph.Node, additionalEmbeddings []*graph.Node) *graph.Node {\n\t// Disable variable checks to allow reuse\n\tctx = ctx.Checked(false)\n\tg := indices.Graph()\n\tbatchSize := indices.Shape().Dimensions[0]\n\n\t// V: Embedding(numFeatures, nFactors)\n\tvCtx := ctx.In(\"V\")\n\tv := layers.Embedding(vCtx, indices, dtypes.F32, fm.numFeatures, fm.nFactors) // [batchSize, numDimension, nFactors]\n\n\t// x: values [batchSize, numDimension, 1]\n\tx := graph.Reshape(values, batchSize, fm.numDimension, 1)\n\n\t// vx: BMM(v, x, true, false) -> [batchSize, nFactors, 1]\n\t// contracting axes: [1] (numDimension), batch axes: [0]\n\tvx := graph.DotGeneral(v, []int{1}, []int{0}, x, []int{1}, []int{0})\n\n\t// Interaction part: 0.5 * sum(vx^2 - sum(v^2 * x^2))\n\tsumSquare := graph.Square(vx)\n\te2 := graph.Square(v)\n\tx2 := graph.Square(x)\n\tsquareSum := graph.DotGeneral(e2, []int{1}, []int{0}, x2, []int{1}, []int{0})\n\tinteraction := graph.Sub(sumSquare, squareSum)\n\tinteraction = graph.ReduceSum(interaction, 1) // [batchSize, 1]\n\tinteraction = graph.Mul(interaction, graph.Scalar(g, dtypes.F32, 0.5))\n\n\t// Linear part: sum(W[indices] * values)\n\twCtx := ctx.In(\"W\")\n\tw := layers.Embedding(wCtx, indices, dtypes.F32, fm.numFeatures, 1) // [batchSize, numDimension, 1]\n\tlinear := graph.DotGeneral(w, []int{1}, []int{0}, x, []int{1}, []int{0})\n\tlinear = graph.Reshape(linear, batchSize, 1)\n\n\t// Bias\n\tbCtx := ctx.In(\"B\")\n\tbVar := bCtx.VariableWithShape(\"bias\", shapes.Make(dtypes.F32, 1))\n\tbias := bVar.ValueGraph(g)\n\tbias = graph.Reshape(bias, 1, 1) // Reshape to [1, 1] for broadcasting with [batchSize, 1]\n\n\tfmOutput := graph.Add(graph.Add(linear, interaction), bias) // [batchSize, 1]\n\n\t// Additional embeddings with attention\n\tfor i, embedding := range additionalEmbeddings {\n\t\t// A: Attention\n\t\taCtx := ctx.In(fmt.Sprintf(\"A_%d\", i))\n\t\tattended := fm.attentionForward(aCtx, embedding, fm.embeddingDim[i], fm.nFactors)\n\n\t\t// E: Linear(dim -> nFactors)\n\t\teCtx := ctx.In(fmt.Sprintf(\"E_%d\", i))\n\t\tencoded := layers.Dense(eCtx, attended, true, fm.nFactors)\n\t\tencoded = graph.Reshape(encoded, batchSize, fm.nFactors, 1)\n\n\t\t// Output: vx^T * encoded -> [batchSize, 1, 1]\n\t\t// vx: [batch, nFactors, 1], encoded: [batch, nFactors, 1]\n\t\t// contracting axes: [1] (nFactors), batch axes: [0]\n\t\tterm := graph.DotGeneral(vx, []int{1}, []int{0}, encoded, []int{1}, []int{0})\n\t\tfmOutput = graph.Add(fmOutput, graph.Reshape(term, batchSize, 1))\n\t}\n\n\treturn graph.Reshape(fmOutput, batchSize)\n}\n\nfunc (fm *AFM) BatchInternalPredict(x []lo.Tuple2[[]int32, []float32], e [][][]float32, jobs int) []float32 {\n\tfm.mu.RLock()\n\tdefer fm.mu.RUnlock()\n\n\tif fm.predictExecutor == nil {\n\t\tvar err error\n\t\tfm.predictExecutor, err = mlx_context.NewExec(fm.backend, fm.ctx, func(ctx *mlx_context.Context, nodes []*graph.Node) *graph.Node {\n\t\t\tres := fm.forwardGraph(ctx, nodes[0], nodes[1], nodes[2:])\n\t\t\treturn res\n\t\t})\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\t// Prepare data\n\tnumBatches := (len(x) + fm.batchSize - 1) / fm.batchSize\n\tpredictions := make([]float32, 0, len(x))\n\n\tfor b := 0; b < numBatches; b++ {\n\t\tstart := b * fm.batchSize\n\t\tend := mathutil.Min(start+fm.batchSize, len(x))\n\t\tbatchSize := end - start\n\n\t\tindicesData := make([]int32, batchSize*fm.numDimension)\n\t\tvaluesData := make([]float32, batchSize*fm.numDimension)\n\t\tadditionalData := make([][]float32, len(fm.embeddingDim))\n\t\tfor i := range additionalData {\n\t\t\tadditionalData[i] = make([]float32, batchSize*fm.embeddingDim[i])\n\t\t}\n\n\t\tfor i := 0; i < batchSize; i++ {\n\t\t\trow := x[start+i]\n\t\t\tfor j := 0; j < len(row.A); j++ {\n\t\t\t\tindicesData[i*fm.numDimension+j] = row.A[j]\n\t\t\t\tvaluesData[i*fm.numDimension+j] = row.B[j]\n\t\t\t}\n\t\t\tfor j := range fm.embeddingDim {\n\t\t\t\tif len(e[start+i]) > j && len(e[start+i][j]) == fm.embeddingDim[j] {\n\t\t\t\t\tcopy(additionalData[j][i*fm.embeddingDim[j]:], e[start+i][j])\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tinputs := []any{\n\t\t\ttensors.FromFlatDataAndDimensions(indicesData, batchSize, fm.numDimension),\n\t\t\ttensors.FromFlatDataAndDimensions(valuesData, batchSize, fm.numDimension),\n\t\t}\n\t\tfor i := range additionalData {\n\t\t\tinputs = append(inputs, tensors.FromFlatDataAndDimensions(additionalData[i], batchSize, fm.embeddingDim[i]))\n\t\t}\n\n\t\toutputs := fm.predictExecutor.MustExec(inputs...)\n\t\tbatchPreds := outputs[0].Value().([]float32)\n\t\tpredictions = append(predictions, batchPreds...)\n\t}\n\n\treturn predictions[:len(x)]\n}\n\nfunc (fm *AFM) BatchPredict(inputs []lo.Tuple4[string, string, []Label, []Label], embeddings [][]Embedding, jobs int) []float32 {\n\tx := make([]lo.Tuple2[[]int32, []float32], len(inputs))\n\tfor i, input := range inputs {\n\t\t// encode user\n\t\tif userIndex := fm.Index.EncodeUser(input.A); userIndex != dataset.NotId {\n\t\t\tx[i].A = append(x[i].A, userIndex)\n\t\t\tx[i].B = append(x[i].B, 1)\n\t\t}\n\t\t// encode item\n\t\tif itemIndex := fm.Index.EncodeItem(input.B); itemIndex != dataset.NotId {\n\t\t\tx[i].A = append(x[i].A, itemIndex)\n\t\t\tx[i].B = append(x[i].B, 1)\n\t\t}\n\t\t// encode user labels\n\t\tfor _, userFeature := range input.C {\n\t\t\tif userFeatureIndex := fm.Index.EncodeUserLabel(userFeature.Name); userFeatureIndex != dataset.NotId {\n\t\t\t\tx[i].A = append(x[i].A, userFeatureIndex)\n\t\t\t\tx[i].B = append(x[i].B, userFeature.Value)\n\t\t\t}\n\t\t}\n\t\t// encode item labels\n\t\tfor _, itemFeature := range input.D {\n\t\t\tif itemFeatureIndex := fm.Index.EncodeItemLabel(itemFeature.Name); itemFeatureIndex != dataset.NotId {\n\t\t\t\tx[i].A = append(x[i].A, itemFeatureIndex)\n\t\t\t\tx[i].B = append(x[i].B, itemFeature.Value)\n\t\t\t}\n\t\t}\n\t}\n\te := make([][][]float32, len(inputs))\n\tfor i := range inputs {\n\t\te[i] = make([][]float32, len(fm.embeddingDim))\n\t\tfor _, embedding := range embeddings[i] {\n\t\t\titemIndex := fm.embeddingIndex.ToNumber(embedding.Name)\n\t\t\tif itemIndex == dataset.NotId {\n\t\t\t\t// unknown embedding\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tindex := int(itemIndex)\n\t\t\tif len(embedding.Value) != fm.embeddingDim[index] {\n\t\t\t\t// dimension mismatch\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\te[i][index] = embedding.Value\n\t\t}\n\t}\n\treturn fm.BatchInternalPredict(x, e, jobs)\n}\n\nfunc (fm *AFM) Init(trainSet dataset.CTRSplit) {\n\tfm.numFeatures = int(trainSet.GetIndex().Len())\n\tfm.numDimension = 0\n\tfor i := 0; i < trainSet.Count(); i++ {\n\t\t_, x, _, _ := trainSet.Get(i)\n\t\tfm.numDimension = mathutil.MaxVal(fm.numDimension, len(x))\n\t}\n\tfm.embeddingDim = trainSet.GetItemEmbeddingDim()\n\tfm.embeddingIndex = trainSet.GetItemEmbeddingIndex()\n\n\tif fm.ctx == nil {\n\t\tfm.ctx = mlx_context.New()\n\t\tfm.ctx.SetParam(\"initializers_seed\", int64(42))\n\t\t// Set default initializer to Normal(0, 0.01) to match nn package\n\t\tfm.ctx = fm.ctx.WithInitializer(initializers.RandomNormalFn(fm.ctx, float64(fm.initStdDev)))\n\t}\n\tif fm.backend == nil {\n\t\tvar err error\n\t\tfm.backend, err = backends.New()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\tfm.BaseFactorizationMachines.Init(trainSet)\n}\n\ntype ctrDataset struct {\n\ttrainSet      dataset.CTRSplit\n\tnumFeatures   int\n\tnumDimension  int\n\tembeddingDim  []int\n\tbatchSize     int\n\tcurrentOffset int\n}\n\nfunc (d *ctrDataset) Name() string { return \"CTRDataset\" }\nfunc (d *ctrDataset) Reset()       { d.currentOffset = 0 }\nfunc (d *ctrDataset) Yield() (spec any, inputs []*tensors.Tensor, labels []*tensors.Tensor, err error) {\n\tif d.currentOffset >= d.trainSet.Count() {\n\t\treturn nil, nil, nil, io.EOF\n\t}\n\n\tbatchSize := mathutil.Min(d.batchSize, d.trainSet.Count()-d.currentOffset)\n\tindicesData := make([]int32, batchSize*d.numDimension)\n\tvaluesData := make([]float32, batchSize*d.numDimension)\n\tadditionalData := make([][]float32, len(d.embeddingDim))\n\tfor i := range additionalData {\n\t\tadditionalData[i] = make([]float32, batchSize*d.embeddingDim[i])\n\t}\n\tlabelsData := make([]float32, batchSize)\n\n\tfor i := 0; i < batchSize; i++ {\n\t\tindices, values, embeddings, target := d.trainSet.Get(d.currentOffset + i)\n\t\tfor j := 0; j < len(indices); j++ {\n\t\t\tindicesData[i*d.numDimension+j] = indices[j]\n\t\t\tvaluesData[i*d.numDimension+j] = values[j]\n\t\t}\n\t\tfor j := range d.embeddingDim {\n\t\t\tif len(embeddings) > j && len(embeddings[j]) == d.embeddingDim[j] {\n\t\t\t\tcopy(additionalData[j][i*d.embeddingDim[j]:], embeddings[j])\n\t\t\t}\n\t\t}\n\t\t// Convert target from {-1, 1} to {0, 1} for GoMLX BinaryCrossentropy\n\t\tlabelsData[i] = (target + 1) / 2\n\t}\n\n\td.currentOffset += batchSize\n\n\tinputs = []*tensors.Tensor{\n\t\ttensors.FromFlatDataAndDimensions(indicesData, batchSize, d.numDimension),\n\t\ttensors.FromFlatDataAndDimensions(valuesData, batchSize, d.numDimension),\n\t}\n\tfor i := range additionalData {\n\t\tinputs = append(inputs, tensors.FromFlatDataAndDimensions(additionalData[i], batchSize, d.embeddingDim[i]))\n\t}\n\tlabels = []*tensors.Tensor{tensors.FromFlatDataAndDimensions(labelsData, batchSize)}\n\treturn nil, inputs, labels, nil\n}\n\nfunc (fm *AFM) Fit(ctx std_context.Context, trainSet, testSet dataset.CTRSplit, config *FitConfig) Score {\n\tlog.Logger().Info(\"fit AFM (mlx)\",\n\t\tzap.Int(\"train_set_size\", trainSet.Count()),\n\t\tzap.Int(\"test_set_size\", testSet.Count()),\n\t\tzap.Any(\"params\", fm.GetParams()),\n\t\tzap.Any(\"config\", config))\n\tfm.Init(trainSet)\n\n\tevalStart := time.Now()\n\tscore := EvaluateClassification(fm, testSet, config.Jobs)\n\tscores := []lo.Tuple2[int, float32]{{A: 0, B: score.AUC}}\n\tevalTime := time.Since(evalStart)\n\tfields := append([]zap.Field{zap.String(\"eval_time\", evalTime.String())}, score.ZapFields()...)\n\tlog.Logger().Info(fmt.Sprintf(\"fit AFM %v/%v\", 0, fm.nEpochs), fields...)\n\n\tmodelFn := func(ctx *mlx_context.Context, spec any, inputs []*graph.Node) []*graph.Node {\n\t\treturn []*graph.Node{fm.forwardGraph(ctx, inputs[0], inputs[1], inputs[2:])}\n\t}\n\tlossFn := func(labels, predictions []*graph.Node) *graph.Node {\n\t\treturn graph.ReduceAllMean(losses.BinaryCrossentropyLogits(labels, predictions))\n\t}\n\n\toptimizer := optimizers.Adam().LearningRate(float64(fm.lr)).Done()\n\ttrainer := train.NewTrainer(fm.backend, fm.ctx, modelFn, lossFn, optimizer, nil, nil)\n\tloop := train.NewLoop(trainer)\n\n\tds := &ctrDataset{\n\t\ttrainSet:     trainSet,\n\t\tnumFeatures:  fm.numFeatures,\n\t\tnumDimension: fm.numDimension,\n\t\tembeddingDim: fm.embeddingDim,\n\t\tbatchSize:    fm.batchSize,\n\t}\n\n\t_, span := monitor.Start(ctx, \"FM.Fit\", fm.nEpochs)\n\tdefer span.End()\n\n\tfor epoch := 1; epoch <= fm.nEpochs; epoch++ {\n\t\tfitStart := time.Now()\n\t\tds.Reset()\n\t\t_, err := loop.RunSteps(ds, (trainSet.Count()+fm.batchSize-1)/fm.batchSize)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tfitTime := time.Since(fitStart)\n\n\t\tif epoch%config.Verbose == 0 || epoch == fm.nEpochs {\n\t\t\tevalStart = time.Now()\n\t\t\tscore = EvaluateClassification(fm, testSet, config.Jobs)\n\t\t\tscores = append(scores, lo.Tuple2[int, float32]{A: epoch, B: score.AUC})\n\t\t\tevalTime = time.Since(evalStart)\n\t\t\tfields := append([]zap.Field{\n\t\t\t\tzap.String(\"fit_time\", fitTime.String()),\n\t\t\t\tzap.String(\"eval_time\", evalTime.String()),\n\t\t\t}, score.ZapFields()...)\n\t\t\tlog.Logger().Info(fmt.Sprintf(\"fit AFM %v/%v\", epoch, fm.nEpochs), fields...)\n\n\t\t\tif config.Patience > 0 && epoch > config.Patience {\n\t\t\t\tepochScore := lo.MaxBy(scores, func(a, b lo.Tuple2[int, float32]) bool { return a.B > b.B })\n\t\t\t\tif epochScore.A <= epoch-config.Patience {\n\t\t\t\t\tlog.Logger().Info(\"early stopping\",\n\t\t\t\t\t\tzap.Int(\"best_epoch\", epochScore.A),\n\t\t\t\t\t\tzap.Float32(\"best_auc\", epochScore.B),\n\t\t\t\t\t\tzap.Int(\"patience\", config.Patience))\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tspan.Add(1)\n\t}\n\n\treturn score\n}\n\ntype savedVariable struct {\n\tDimensions []int\n\tData       any\n\tScope      string\n\tName       string\n}\n\nfunc (fm *AFM) Marshal(w io.Writer) error {\n\t// write params\n\tif err := encoding.WriteGob(w, fm.Params); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t// write index\n\tif err := dataset.MarshalUnifiedIndex(w, fm.Index); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t// write dataset stats\n\tif err := encoding.WriteGob(w, fm.numFeatures); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif err := encoding.WriteGob(w, fm.numDimension); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif err := encoding.WriteGob(w, fm.embeddingDim); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif len(fm.embeddingDim) > 0 {\n\t\tif err := dataset.MarshalIndex(w, fm.embeddingIndex); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\t// write parameters (GoMLX variables)\n\tvariables := make(map[string]savedVariable)\n\tfm.ctx.EnumerateVariables(func(v *mlx_context.Variable) {\n\t\tval, err := v.Value()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tvar flatData any\n\t\tval.MustConstFlatData(func(flat any) {\n\t\t\tflatData = flat\n\t\t})\n\t\tvariables[v.ScopeAndName()] = savedVariable{\n\t\t\tDimensions: val.Shape().Dimensions,\n\t\t\tData:       flatData,\n\t\t\tScope:      v.Scope(),\n\t\t\tName:       v.Name(),\n\t\t}\n\t})\n\tif err := encoding.WriteGob(w, variables); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\treturn nil\n}\n\nfunc (fm *AFM) Unmarshal(r io.Reader) error {\n\t// read params\n\terr := encoding.ReadGob(r, &fm.Params)\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tfm.SetParams(fm.Params)\n\t// read index\n\tfm.Index, err = dataset.UnmarshalUnifiedIndex(r)\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t// read dataset stats\n\tif err = encoding.ReadGob(r, &fm.numFeatures); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif err = encoding.ReadGob(r, &fm.numDimension); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif err = encoding.ReadGob(r, &fm.embeddingDim); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif len(fm.embeddingDim) > 0 {\n\t\tfm.embeddingIndex, err = dataset.UnmarshalIndex(r)\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\t// read parameters\n\tvar variables map[string]savedVariable\n\tif err = encoding.ReadGob(r, &variables); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif fm.ctx == nil {\n\t\tfm.ctx = mlx_context.New()\n\t}\n\tif fm.backend == nil {\n\t\tvar err error\n\t\tfm.backend, err = backends.New()\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\tfor _, data := range variables {\n\t\t// Use a type switch to handle different data types in tensors\n\t\tvar t *tensors.Tensor\n\t\tswitch d := data.Data.(type) {\n\t\tcase []float32:\n\t\t\tt = tensors.FromFlatDataAndDimensions(d, data.Dimensions...)\n\t\tcase []float64:\n\t\t\tt = tensors.FromFlatDataAndDimensions(d, data.Dimensions...)\n\t\tcase []int32:\n\t\t\tt = tensors.FromFlatDataAndDimensions(d, data.Dimensions...)\n\t\tcase []int64:\n\t\t\tt = tensors.FromFlatDataAndDimensions(d, data.Dimensions...)\n\t\tcase []uint64:\n\t\t\tt = tensors.FromFlatDataAndDimensions(d, data.Dimensions...)\n\t\tdefault:\n\t\t\tlog.Logger().Warn(\"unknown variable type\", zap.String(\"scope\", data.Scope), zap.String(\"name\", data.Name), zap.Any(\"type\", fmt.Sprintf(\"%T\", d)))\n\t\t\tcontinue\n\t\t}\n\t\tfm.ctx.InAbsPath(data.Scope).VariableWithValue(data.Name, t)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "model/ctr/model.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage ctr\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"reflect\"\n\n\t\"github.com/gorse-io/gorse/common/encoding\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/model\"\n\t\"github.com/juju/errors\"\n\t\"github.com/samber/lo\"\n\t\"go.uber.org/zap\"\n)\n\ntype Score struct {\n\tRMSE      float32\n\tPrecision float32\n\tRecall    float32\n\tAccuracy  float32\n\tAUC       float32\n}\n\nfunc (score Score) ZapFields() []zap.Field {\n\treturn []zap.Field{\n\t\tzap.Float32(\"Accuracy\", score.Accuracy),\n\t\tzap.Float32(\"Precision\", score.Precision),\n\t\tzap.Float32(\"Recall\", score.Recall),\n\t\tzap.Float32(\"AUC\", score.AUC),\n\t}\n}\n\nfunc (score Score) GetValue() float32 {\n\treturn score.Precision\n}\n\nfunc (score Score) BetterThan(s Score) bool {\n\treturn score.AUC > s.AUC\n}\n\ntype FitConfig struct {\n\tJobs     int\n\tVerbose  int\n\tPatience int\n}\n\nfunc NewFitConfig() *FitConfig {\n\treturn &FitConfig{\n\t\tJobs:     1,\n\t\tVerbose:  10,\n\t\tPatience: 10,\n\t}\n}\n\nfunc (config *FitConfig) SetVerbose(verbose int) *FitConfig {\n\tconfig.Verbose = verbose\n\treturn config\n}\n\nfunc (config *FitConfig) SetJobs(jobs int) *FitConfig {\n\tconfig.Jobs = jobs\n\treturn config\n}\n\nfunc (config *FitConfig) SetPatience(patience int) *FitConfig {\n\tconfig.Patience = patience\n\treturn config\n}\n\nfunc (config *FitConfig) LoadDefaultIfNil() *FitConfig {\n\tif config == nil {\n\t\treturn NewFitConfig()\n\t}\n\treturn config\n}\n\ntype FactorizationMachines interface {\n\tmodel.Model\n\tPredict(userId, itemId string, userFeatures, itemFeatures []Label) float32\n\tInternalPredict(x []int32, values []float32) float32\n\tFit(ctx context.Context, trainSet, testSet dataset.CTRSplit, config *FitConfig) Score\n\tMarshal(w io.Writer) error\n}\n\ntype BatchInference interface {\n\tBatchPredict(inputs []lo.Tuple4[string, string, []Label, []Label], e [][]Embedding, jobs int) []float32\n\tBatchInternalPredict(x []lo.Tuple2[[]int32, []float32], e [][][]float32, jobs int) []float32\n}\n\ntype BaseFactorizationMachines struct {\n\tmodel.BaseModel\n\tIndex dataset.UnifiedIndex\n}\n\nfunc (b *BaseFactorizationMachines) Init(trainSet dataset.CTRSplit) {\n\tb.Index = trainSet.GetIndex()\n}\n\nfunc MarshalModel(w io.Writer, m FactorizationMachines) error {\n\t// write header\n\tvar err error\n\tswitch m.(type) {\n\tcase *AFM:\n\t\terr = encoding.WriteString(w, headerAFM)\n\tdefault:\n\t\treturn fmt.Errorf(\"unknown model: %v\", reflect.TypeOf(m))\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn m.Marshal(w)\n}\n\nfunc UnmarshalModel(r io.Reader) (FactorizationMachines, error) {\n\t// read header\n\theader, err := encoding.ReadString(r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tswitch header {\n\tcase headerAFM:\n\t\tvar fm AFM\n\t\tif err := fm.Unmarshal(r); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\treturn &fm, nil\n\t}\n\treturn nil, fmt.Errorf(\"unknown model: %v\", header)\n}\n\n"
  },
  {
    "path": "model/ctr/model.py",
    "content": "import os\nfrom pathlib import Path\nfrom typing import Dict, List, Tuple\n\nimport click\nimport torch\nfrom tqdm import tqdm\n\n\nclass Dataset:\n\n    def __init__(self):\n        self.indices = []\n        self.values = []\n        self.targets = []\n        self.num_fields = 0\n        self.num_features = 0\n\n    def __len__(self):\n        assert len(self.indices) == len(self.targets)\n        return len(self.indices)\n\n    def aligned(self) -> Tuple[List[List[int]], List[List[float]]]:\n        aligned_indices = []\n        aligned_values = []\n        for i in range(len(self)):\n            aligned_indices_row = self.indices[i]\n            aligned_values_row = self.values[i]\n            if len(aligned_indices_row) < self.num_fields:\n                aligned_indices_row += [0] * (\n                        self.num_fields - len(aligned_indices_row)\n                )\n                aligned_values_row += [0] * (self.num_fields - len(aligned_values_row))\n            aligned_indices.append(aligned_indices_row)\n            aligned_values.append(aligned_values_row)\n        return aligned_indices, aligned_values\n\n\ndef load_libfm(path: str) -> Dataset:\n    dataset = Dataset()\n    with open(path, 'r') as f:\n        for line in f.readlines():\n            splits = line.strip().split(' ')\n            indices = [int(v.split(':')[0]) for v in splits[1:]]\n            values = [float(v.split(':')[1]) for v in splits[1:]]\n            target = 1 if float(splits[0]) == 1 else 0\n            dataset.indices.append(indices)\n            dataset.values.append(values)\n            dataset.targets.append(target)\n            dataset.num_fields = max(dataset.num_fields, len(indices))\n            dataset.num_features = max(dataset.num_features, max(indices) + 1)\n    return dataset\n\n\ndef load_dataset(name: str) -> Tuple[Dataset, Dataset]:\n    dataset_dir = os.path.join(Path.home(), '.gorse', 'dataset', name)\n    return load_libfm(os.path.join(dataset_dir, \"train.libfm\")), load_libfm(os.path.join(dataset_dir, \"test.libfm\"))\n\n\ndef accuracy(positive_predictions: List[float], negative_predictions: List[float]) -> float:\n    num_pos = len(positive_predictions)\n    num_neg = len(negative_predictions)\n    num_correct = 0\n    for pos in positive_predictions:\n        if pos > 0:\n            num_correct += 1\n    for neg in negative_predictions:\n        if neg <= 0:\n            num_correct += 1\n    return num_correct / (num_pos + num_neg)\n\n\ndef auc(positive_predictions: List[float], negative_predictions: List[float]) -> float:\n    sorted_positive_predictions = sorted(positive_predictions)\n    sorted_negative_predictions = sorted(negative_predictions)\n    sum = 0.0\n    num_pos = 0\n    for pos in sorted_positive_predictions:\n        while (\n                num_pos < len(sorted_negative_predictions)\n                and sorted_negative_predictions[num_pos] < pos\n        ):\n            num_pos += 1\n        sum += num_pos\n    return sum / (len(positive_predictions) * len(negative_predictions))\n\n\nclass Evaluator:\n\n    def __init__(self, test: Dataset, device: str = \"cpu\") -> None:\n        align_indices, align_values = test.aligned()\n        self.indices = torch.tensor(align_indices, dtype=torch.long).to(device)\n        self.values = torch.tensor(align_values, dtype=torch.float).to(device)\n        self.positive_samples = []\n        self.negative_samples = []\n        for i in range(len(test)):\n            if test.targets[i] > 0:\n                self.positive_samples.append(i)\n            else:\n                self.negative_samples.append(i)\n\n    def evaluate(self, model) -> Dict[str, float]:\n        positive_predictions = (\n            model.predict(\n                self.indices[self.positive_samples],\n                self.values[self.positive_samples],\n            )\n            .cpu()\n            .tolist()\n        )\n        negative_predictions = (\n            model.predict(\n                self.indices[self.negative_samples],\n                self.values[self.negative_samples],\n            )\n            .cpu()\n            .tolist()\n        )\n        return {\n            \"Accuracy\": accuracy(positive_predictions, negative_predictions),\n            \"AUC\": auc(positive_predictions, negative_predictions),\n        }\n\n\nclass FactorizationMachine(torch.nn.Module):\n\n    def __init__(\n            self, num_fields: int, num_features: int, k: int = 8, init_stddev=0.01\n    ) -> None:\n        super().__init__()\n        self.b = torch.nn.Parameter(torch.zeros(1))\n        self.w = torch.nn.Embedding(num_features, 1)\n        self.v = torch.nn.Embedding(num_features, k)\n        torch.nn.init.normal_(self.w.weight, mean=0.0, std=init_stddev)\n        torch.nn.init.normal_(self.v.weight, mean=0.0, std=init_stddev)\n\n    def forward(self, indices, values):\n        linear = self.b + torch.sum(self.w(indices) * values.unsqueeze(-1), dim=1)\n        x = self.v(indices) * values.unsqueeze(-1)\n        square_of_sum = torch.sum(x, dim=1) ** 2\n        sum_of_square = torch.sum(x ** 2, dim=1)\n        interaction = 0.5 * torch.sum(\n            square_of_sum - sum_of_square, dim=1, keepdim=True\n        )\n        fm = linear + interaction\n        return fm.squeeze()\n\n    def predict(self, indices, values):\n        return self.forward(indices, values)\n\n    def fit(\n            self,\n            train: Dataset,\n            test: Dataset,\n            epochs: int = 20,\n            lr: float = 0.01,\n            reg: float = 0.0001,\n            batch_size: int = 1024,\n            device: str = \"cpu\",\n            verbose: bool = True,\n    ) -> dict[str, float]:\n        self.to(device)\n        aligned_indices, aligned_values = train.aligned()\n        indices = torch.tensor(aligned_indices, dtype=torch.long).to(device)\n        values = torch.tensor(aligned_values, dtype=torch.float).to(device)\n        targets = torch.tensor(train.targets, dtype=torch.float).to(device)\n\n        optimizer = torch.optim.Adam(self.parameters(), lr=lr, weight_decay=reg)\n        criterion = torch.nn.BCEWithLogitsLoss()\n        evaluator = Evaluator(test, device)\n\n        score = evaluator.evaluate(self)\n\n        for epoch in range(epochs):\n            for i in tqdm(\n                    range(0, len(indices), batch_size),\n                    disable=not verbose,\n                    desc=f\"Epoch {epoch + 1}/{epochs}\",\n                    postfix=score,\n            ):\n                batch_indices = indices[i: i + batch_size]\n                batch_values = values[i: i + batch_size]\n                batch_targets = targets[i: i + batch_size]\n                optimizer.zero_grad()\n                output = self.forward(batch_indices, batch_values)\n                loss = criterion(output, batch_targets)\n                loss.backward()\n                optimizer.step()\n            score = evaluator.evaluate(self)\n        return score\n\n\n@click.command()\n@click.argument(\"dataset\")\n@click.option('-dim', default=8, help=\"Number of factors.\")\n@click.option('-iter', default=20, help=\"Number of epochs.\")\n@click.option('-learn_rate', default=0.01, help=\"Learning rate.\")\n@click.option('-regular', default=0.0001, help=\"Regularization.\")\n@click.option('-device', default=\"cpu\", help=\"Device to use.\")\ndef main(dataset: str, dim: int, iter: int, learn_rate: float, regular: float, device: str):\n    train, test = load_dataset(dataset)\n    model = FactorizationMachine(train.num_fields, train.num_features, dim)\n    model.fit(train, test, iter, learn_rate, regular, device=device)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "model/ctr/model_test.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage ctr\n\nimport (\n\t\"bytes\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/model\"\n\t\"github.com/samber/lo\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nconst classificationDelta = 0.01\n\nfunc newFitConfigWithTestTracker() *FitConfig {\n\tcfg := NewFitConfig().SetVerbose(1).SetJobs(runtime.NumCPU())\n\treturn cfg\n}\n\nfunc TestFactorizationMachines_Classification_Frappe(t *testing.T) {\n\t// python .\\model.py frappe -dim 8 -iter 10 -learn_rate 0.01 -regular 0.0001\n\ttrain, test, err := LoadDataFromBuiltIn(\"frappe\")\n\tassert.NoError(t, err)\n\tm := NewAFM(model.Params{\n\t\tmodel.NFactors:  8,\n\t\tmodel.NEpochs:   10,\n\t\tmodel.Lr:        0.01,\n\t\tmodel.Reg:       0.0001,\n\t\tmodel.BatchSize: 1024,\n\t})\n\tfitConfig := newFitConfigWithTestTracker()\n\tscore := m.Fit(t.Context(), train, test, fitConfig)\n\tassert.InDelta(t, 0.919, score.Accuracy, classificationDelta)\n}\n\nfunc TestFactorizationMachines_Classification_MovieLens(t *testing.T) {\n\tt.Skip(\"Skip time-consuming test\")\n\t// python .\\model.py ml-tag -dim 8 -iter 10 -learn_rate 0.01 -regular 0.0001\n\ttrain, test, err := LoadDataFromBuiltIn(\"ml-tag\")\n\tassert.NoError(t, err)\n\tm := NewAFM(model.Params{\n\t\tmodel.InitStdDev: 0.01,\n\t\tmodel.NFactors:   8,\n\t\tmodel.NEpochs:    10,\n\t\tmodel.Lr:         0.001,\n\t\tmodel.Reg:        0.0001,\n\t\tmodel.BatchSize:  1024,\n\t})\n\tfitConfig := newFitConfigWithTestTracker()\n\tscore := m.Fit(t.Context(), train, test, fitConfig)\n\tassert.InDelta(t, 0.815, score.Accuracy, classificationDelta)\n}\n\nfunc TestFactorizationMachines_Classification_Criteo(t *testing.T) {\n\t// python .\\model.py criteo -dim 8 -iter 10 -learn_rate 0.01 -regular 0.0001\n\ttrain, test, err := LoadDataFromBuiltIn(\"criteo\")\n\tassert.NoError(t, err)\n\tm := NewAFM(model.Params{\n\t\tmodel.NFactors:  8,\n\t\tmodel.NEpochs:   10,\n\t\tmodel.Lr:        0.01,\n\t\tmodel.Reg:       0.0001,\n\t\tmodel.BatchSize: 1024,\n\t})\n\tfitConfig := newFitConfigWithTestTracker()\n\tscore := m.Fit(t.Context(), train, test, fitConfig)\n\tassert.InDelta(t, 0.77, score.Accuracy, 0.025)\n\n\t// test prediction\n\tassert.Equal(t,\n\t\tm.BatchInternalPredict(\n\t\t\t[]lo.Tuple2[[]int32, []float32]{{A: []int32{1, 2, 3, 4, 5, 6}, B: []float32{1, 1, 0.3, 0.4, 0.5, 0.6}}},\n\t\t\tmake([][][]float32, 2), fitConfig.Jobs),\n\t\tm.BatchPredict([]lo.Tuple4[string, string, []Label, []Label]{{\n\t\t\tA: \"1\",\n\t\t\tB: \"2\",\n\t\t\tC: []Label{\n\t\t\t\t{Name: \"3\", Value: 0.3},\n\t\t\t\t{Name: \"4\", Value: 0.4},\n\t\t\t},\n\t\t\tD: []Label{\n\t\t\t\t{Name: \"5\", Value: 0.5},\n\t\t\t\t{Name: \"6\", Value: 0.6},\n\t\t\t}}}, make([][]Embedding, 2), fitConfig.Jobs))\n\n\t// test marshal and unmarshal\n\tbuf := bytes.NewBuffer(nil)\n\terr = MarshalModel(buf, m)\n\tassert.NoError(t, err)\n\ttmp, err := UnmarshalModel(buf)\n\tassert.NoError(t, err)\n\tscoreClone := EvaluateClassification(tmp, test, fitConfig.Jobs)\n\tassert.InDelta(t, 0.77, scoreClone.Accuracy, 0.02)\n\n\t// test clear\n\tassert.False(t, m.Invalid())\n\tm.Clear()\n\tassert.True(t, m.Invalid())\n}\n\nfunc newSynthesisDataset() *Dataset {\n\tbuilder := dataset.NewUnifiedMapIndexBuilder()\n\tbuilder.AddUser(\"u0\")\n\tbuilder.AddUser(\"u1\")\n\tbuilder.AddUserLabel(\"ul0\")\n\tbuilder.AddUserLabel(\"ul1\")\n\tbuilder.AddUserLabel(\"ul2\")\n\tbuilder.AddItem(\"i0\")\n\tbuilder.AddItem(\"i1\")\n\tbuilder.AddItemLabel(\"il0\")\n\tbuilder.AddItemLabel(\"il1\")\n\tbuilder.AddItemLabel(\"il2\")\n\n\tdataSet := NewMapIndexDataset()\n\tdataSet.Index = builder.Build()\n\tdataSet.UserLabels = [][]lo.Tuple2[int32, float32]{\n\t\t{{A: 0, B: 1.0}, {A: 1, B: 0.5}, {A: 2, B: -1.0}},\n\t\t{{A: 0, B: -1.0}, {A: 1, B: -0.5}, {A: 2, B: 1.0}},\n\t}\n\tdataSet.ItemLabels = [][]lo.Tuple2[int32, float32]{\n\t\t{{A: 0, B: 1.0}, {A: 1, B: 0.5}, {A: 2, B: -1.0}},\n\t\t{{A: 0, B: -1.0}, {A: 1, B: -0.5}, {A: 2, B: 1.0}},\n\t}\n\tdataSet.ItemEmbeddingIndex = dataset.NewMapIndex()\n\tdataSet.ItemEmbeddingIndex.Add(\"e1\")\n\tdataSet.ItemEmbeddingIndex.Add(\"e2\")\n\tdataSet.ItemEmbeddingDimension = []int{3, 4}\n\tdataSet.ItemEmbeddings = [][][]float32{\n\t\t{{0.8, 0.8, 0.8}, {0.1, 0.1, 0.1, 0.1}},\n\t\t{{-0.8, -0.8, -0.8}, {-0.1, -0.1, -0.1, -0.1}},\n\t}\n\n\tdataSet.Users = []int32{0, 0, 1, 1}\n\tdataSet.Items = []int32{0, 1, 0, 1}\n\tdataSet.Target = []float32{1, -1, -1, 1}\n\tdataSet.PositiveCount = 2\n\tdataSet.NegativeCount = 2\n\treturn dataSet\n}\n\nfunc TestFactorizationMachines_Classification_Synthesis(t *testing.T) {\n\tdataSet := newSynthesisDataset()\n\tfitConfig := newFitConfigWithTestTracker()\n\tm := NewAFM(nil)\n\tscore := m.Fit(t.Context(), dataSet, dataSet, fitConfig)\n\tassert.GreaterOrEqual(t, score.Accuracy, float32(0.5))\n\n\tbuf := bytes.NewBuffer(nil)\n\terr := MarshalModel(buf, m)\n\tassert.NoError(t, err)\n\tclone, err := UnmarshalModel(buf)\n\tassert.NoError(t, err)\n\tcloneScore := EvaluateClassification(clone, dataSet, fitConfig.Jobs)\n\tassert.InDelta(t, score.Accuracy, cloneScore.Accuracy, 0.05)\n\n\tindicesPos, valuesPos, embeddingsPos, _ := dataSet.Get(0)\n\tindicesNeg, valuesNeg, embeddingsNeg, _ := dataSet.Get(1)\n\tassert.Equal(t,\n\t\tm.BatchInternalPredict(\n\t\t\t[]lo.Tuple2[[]int32, []float32]{\n\t\t\t\t{A: indicesPos, B: valuesPos},\n\t\t\t\t{A: indicesNeg, B: valuesNeg},\n\t\t\t},\n\t\t\t[][][]float32{embeddingsPos, embeddingsNeg},\n\t\t\tfitConfig.Jobs,\n\t\t),\n\t\tm.BatchPredict(\n\t\t\t[]lo.Tuple4[string, string, []Label, []Label]{\n\t\t\t\t{\n\t\t\t\t\tA: \"u0\",\n\t\t\t\t\tB: \"i0\",\n\t\t\t\t\tC: []Label{{Name: \"ul0\", Value: 1.0}, {Name: \"ul1\", Value: 0.5}, {Name: \"ul2\", Value: -1.0}},\n\t\t\t\t\tD: []Label{{Name: \"il0\", Value: 1.0}, {Name: \"il1\", Value: 0.5}, {Name: \"il2\", Value: -1.0}},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tA: \"u0\",\n\t\t\t\t\tB: \"i1\",\n\t\t\t\t\tC: []Label{{Name: \"ul0\", Value: 1.0}, {Name: \"ul1\", Value: 0.5}, {Name: \"ul2\", Value: -1.0}},\n\t\t\t\t\tD: []Label{{Name: \"il0\", Value: -1.0}, {Name: \"il1\", Value: -0.5}, {Name: \"il2\", Value: 1.0}},\n\t\t\t\t},\n\t\t\t},\n\t\t\t[][]Embedding{\n\t\t\t\t{{Name: \"e1\", Value: embeddingsPos[0]}, {Name: \"e2\", Value: embeddingsPos[1]}},\n\t\t\t\t{{Name: \"e1\", Value: embeddingsNeg[0]}, {Name: \"e2\", Value: embeddingsNeg[1]}},\n\t\t\t},\n\t\t\tfitConfig.Jobs,\n\t\t))\n\n\tassert.Len(t, m.BatchPredict(\n\t\t[]lo.Tuple4[string, string, []Label, []Label]{\n\t\t\t{A: \"u0\", B: \"i0\"},\n\t\t\t{A: \"u0\", B: \"i1\"},\n\t\t\t{\n\t\t\t\tA: \"u0\",\n\t\t\t\tB: \"i0\",\n\t\t\t\tC: []Label{{Name: \"ul_unknown\", Value: 1}},\n\t\t\t\tD: []Label{{Name: \"il_unknown\", Value: 1}},\n\t\t\t},\n\t\t\t{\n\t\t\t\tA: \"u0\",\n\t\t\t\tB: \"i1\",\n\t\t\t\tC: []Label{{Name: \"ul_unknown\", Value: 1}},\n\t\t\t\tD: []Label{{Name: \"il_unknown\", Value: 1}},\n\t\t\t},\n\t\t},\n\t\t[][]Embedding{\n\t\t\t{},\n\t\t\t{},\n\t\t\t{{Name: \"unknown_embedding\", Value: make([]float32, 3)}},\n\t\t\t{{Name: \"unknown_embedding\", Value: make([]float32, 3)}},\n\t\t},\n\t\tfitConfig.Jobs,\n\t), 4)\n}\n"
  },
  {
    "path": "model/ctr/optimize.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage ctr\n\nimport (\n\t\"context\"\n\n\t\"github.com/c-bata/goptuna\"\n\t\"github.com/gorse-io/gorse/common/monitor\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/storage/meta\"\n\t\"github.com/juju/errors\"\n\t\"golang.org/x/exp/maps\"\n)\n\ntype ModelCreator func() FactorizationMachines\n\ntype ModelSearch struct {\n\tmodelCreators map[string]ModelCreator\n\tmodelTypes    []string\n\ttrainSet      dataset.CTRSplit\n\ttestSet       dataset.CTRSplit\n\tconfig        *FitConfig\n\tctx           context.Context\n\tspan          *monitor.Span\n\tresult        meta.Model[Score]\n}\n\nfunc NewModelSearch(models map[string]ModelCreator, trainSet, testSet dataset.CTRSplit, config *FitConfig) *ModelSearch {\n\treturn &ModelSearch{\n\t\tmodelCreators: models,\n\t\tmodelTypes:    maps.Keys(models),\n\t\ttrainSet:      trainSet,\n\t\ttestSet:       testSet,\n\t\tconfig:        config,\n\t}\n}\n\nfunc (ms *ModelSearch) WithContext(ctx context.Context) *ModelSearch {\n\tms.ctx = ctx\n\treturn ms\n}\n\nfunc (ms *ModelSearch) WithSpan(span *monitor.Span) *ModelSearch {\n\tms.span = span\n\treturn ms\n}\n\nfunc (ms *ModelSearch) Objective(trial goptuna.Trial) (float64, error) {\n\tif len(ms.modelCreators) == 0 {\n\t\treturn 0, errors.New(\"no model to search\")\n\t}\n\tmodelType, err := trial.SuggestCategorical(\"Model\", ms.modelTypes)\n\tif err != nil {\n\t\treturn 0, errors.Trace(err)\n\t}\n\tm := ms.modelCreators[modelType]()\n\tm.SetParams(m.SuggestParams(trial))\n\tscore := m.Fit(ms.ctx, ms.trainSet, ms.testSet, ms.config)\n\tif score.AUC > ms.result.Score.AUC {\n\t\tms.result = meta.Model[Score]{\n\t\t\tType:   modelType,\n\t\t\tParams: m.GetParams(),\n\t\t\tScore:  score,\n\t\t}\n\t}\n\tif ms.span != nil {\n\t\tms.span.Add(1)\n\t}\n\treturn float64(score.AUC), nil\n}\n\nfunc (ms *ModelSearch) Result() meta.Model[Score] {\n\treturn ms.result\n}\n"
  },
  {
    "path": "model/ctr/optimize_test.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage ctr\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/c-bata/goptuna\"\n\t\"github.com/c-bata/goptuna/tpe\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/model\"\n\t\"github.com/samber/lo\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// NewMapIndexDataset creates a data set.\nfunc NewMapIndexDataset() *Dataset {\n\ts := new(Dataset)\n\ts.Index = dataset.NewUnifiedDirectIndex(0)\n\treturn s\n}\n\ntype mockFactorizationMachineForSearch struct {\n\tmodel.BaseModel\n}\n\nfunc (m *mockFactorizationMachineForSearch) Marshal(_ io.Writer) error {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockFactorizationMachineForSearch) Invalid() bool {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockFactorizationMachineForSearch) GetUserIndex() dataset.Index {\n\tpanic(\"don't call me\")\n}\n\nfunc (m *mockFactorizationMachineForSearch) GetItemIndex() dataset.Index {\n\tpanic(\"don't call me\")\n}\n\nfunc (m *mockFactorizationMachineForSearch) Fit(_ context.Context, _, _ dataset.CTRSplit, cfg *FitConfig) Score {\n\tscore := float32(0)\n\tscore += m.Params.GetFloat32(model.NFactors, 0.0)\n\tscore += m.Params.GetFloat32(model.InitMean, 0.0)\n\tscore += m.Params.GetFloat32(model.InitStdDev, 0.0)\n\treturn Score{AUC: score}\n}\n\nfunc (m *mockFactorizationMachineForSearch) Predict(_, _ string, _, _ []Label) float32 {\n\tpanic(\"don't call me\")\n}\n\nfunc (m *mockFactorizationMachineForSearch) InternalPredict(_ []int32, _ []float32) float32 {\n\tpanic(\"don't call me\")\n}\n\nfunc (m *mockFactorizationMachineForSearch) Clear() {\n\t// do nothing\n}\n\nfunc (m *mockFactorizationMachineForSearch) GetParamsGrid(_ bool) model.ParamsGrid {\n\treturn model.ParamsGrid{\n\t\tmodel.NFactors:   []interface{}{1, 2, 3, 4},\n\t\tmodel.InitMean:   []interface{}{4, 3, 2, 1},\n\t\tmodel.InitStdDev: []interface{}{4, 4, 4, 4},\n\t}\n}\n\nfunc (m *mockFactorizationMachineForSearch) SuggestParams(trial goptuna.Trial) model.Params {\n\treturn model.Params{\n\t\tmodel.NFactors:   lo.Must(trial.SuggestDiscreteFloat(string(model.NFactors), 1, 4, 1)),\n\t\tmodel.InitMean:   lo.Must(trial.SuggestDiscreteFloat(string(model.InitMean), 1, 4, 1)),\n\t\tmodel.InitStdDev: lo.Must(trial.SuggestDiscreteFloat(string(model.InitStdDev), 4, 4, 1)),\n\t}\n}\n\nfunc TestTPE(t *testing.T) {\n\tsearch := NewModelSearch(map[string]ModelCreator{\n\t\t\"mock\": func() FactorizationMachines {\n\t\t\treturn &mockFactorizationMachineForSearch{}\n\t\t},\n\t}, nil, nil, nil)\n\tstudy, err := goptuna.CreateStudy(\"TestTPE\",\n\t\tgoptuna.StudyOptionDirection(goptuna.StudyDirectionMaximize),\n\t\tgoptuna.StudyOptionSampler(tpe.NewSampler()))\n\tassert.NoError(t, err)\n\terr = study.Optimize(search.Objective, 10)\n\tassert.NoError(t, err)\n\tv, _ := study.GetBestValue()\n\tassert.Equal(t, float64(12), v)\n\tresult := search.Result()\n\tassert.Equal(t, \"mock\", result.Type)\n\tassert.Equal(t, model.Params{\n\t\tmodel.NFactors:   float64(4),\n\t\tmodel.InitMean:   float64(4),\n\t\tmodel.InitStdDev: float64(4),\n\t}, result.Params)\n\tassert.Equal(t, Score{AUC: 12}, result.Score)\n}\n"
  },
  {
    "path": "model/model.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage model\n\nimport (\n\t\"github.com/c-bata/goptuna\"\n\t\"github.com/gorse-io/gorse/common/util\"\n)\n\n// Model is the interface for all models. Any model in this\n// package should implement it.\ntype Model interface {\n\tSetParams(params Params)\n\tGetParams() Params\n\tSuggestParams(trial goptuna.Trial) Params\n\tClear()\n\tInvalid() bool\n}\n\n// BaseModel model must be included by every recommendation model. Hyper-parameters,\n// ID sets, random generator and fitting options are managed the BaseModel model.\ntype BaseModel struct {\n\tParams    Params               // Hyper-parameters\n\trng       util.RandomGenerator // Random generator\n\trandState int64                // Random seed\n}\n\n// SetParams sets hyper-parameters for the BaseModel model.\nfunc (model *BaseModel) SetParams(params Params) {\n\tmodel.Params = params\n\tmodel.randState = model.Params.GetInt64(RandomState, 0)\n\tmodel.rng = util.NewRandomGenerator(model.randState)\n}\n\n// GetParams returns all hyper-parameters.\nfunc (model *BaseModel) GetParams() Params {\n\treturn model.Params\n}\n\nfunc (model *BaseModel) GetRandomGenerator() util.RandomGenerator {\n\treturn model.rng\n}\n"
  },
  {
    "path": "model/params.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage model\n\nimport (\n\t\"reflect\"\n\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"go.uber.org/zap\"\n)\n\n/* ParamName */\n\n// ParamName is the type of hyper-parameter names.\ntype ParamName string\n\n// Predefined hyper-parameter names\nconst (\n\tLr           ParamName = \"Lr\"          // learning rate\n\tReg          ParamName = \"Reg\"         // regularization strength\n\tNEpochs      ParamName = \"NEpochs\"     // number of epochs\n\tNFactors     ParamName = \"NFactors\"    // number of factors\n\tRandomState  ParamName = \"RandomState\" // random state (seed)\n\tInitMean     ParamName = \"InitMean\"    // mean of gaussian initial parameter\n\tInitStdDev   ParamName = \"InitStdDev\"  // standard deviation of gaussian initial parameter\n\tAlpha        ParamName = \"Alpha\"       // weight for negative samples in ALS\n\tSimilarity   ParamName = \"Similarity\"\n\tUseFeature   ParamName = \"UseFeature\"\n\tBatchSize    ParamName = \"BatchSize\"\n\tHiddenLayers ParamName = \"HiddenLayers\"\n\tOptimizer    ParamName = \"Optimizer\"\n\n\tSGD  = \"sgd\"\n\tAdam = \"adam\"\n)\n\n// Params stores hyper-parameters for an model. It is a map between strings\n// (names) and interface{}s (values). For example, hyper-parameters for SVD\n// is given by:\n//\n//\t base.Params{\n//\t\t\tbase.Lr:       0.007,\n//\t\t\tbase.NEpochs:  100,\n//\t\t\tbase.NFactors: 80,\n//\t\t\tbase.Reg:      0.1,\n//\t\t}\ntype Params map[ParamName]interface{}\n\n// Copy hyper-parameters.\nfunc (parameters Params) Copy() Params {\n\tnewParams := make(Params)\n\tfor k, v := range parameters {\n\t\tnewParams[k] = v\n\t}\n\treturn newParams\n}\n\n// GetBool gets a boolean parameter by name. Returns _default if not exists or type doesn't match.\nfunc (parameters Params) GetBool(name ParamName, _default bool) bool {\n\tif val, exist := parameters[name]; exist {\n\t\tswitch val := val.(type) {\n\t\tcase bool:\n\t\t\treturn val\n\t\tdefault:\n\t\t\tlog.Logger().Error(\"type mismatch\",\n\t\t\t\tzap.String(\"param_name\", string(name)),\n\t\t\t\tzap.String(\"actual_type\", reflect.TypeOf(name).Name()))\n\t\t}\n\t}\n\treturn _default\n}\n\n// GetInt gets a integer parameter by name. Returns _default if not exists or type doesn't match.\nfunc (parameters Params) GetInt(name ParamName, _default int) int {\n\tif val, exist := parameters[name]; exist {\n\t\tswitch val := val.(type) {\n\t\tcase int:\n\t\t\treturn val\n\t\tdefault:\n\t\t\tlog.Logger().Error(\"type mismatch\",\n\t\t\t\tzap.String(\"param_name\", string(name)),\n\t\t\t\tzap.String(\"actual_type\", reflect.TypeOf(name).Name()))\n\t\t}\n\t}\n\treturn _default\n}\n\n// GetInt64 gets a int64 parameter by name. Returns _default if not exists or type doesn't match. The\n// type will be converted if given int.\nfunc (parameters Params) GetInt64(name ParamName, _default int64) int64 {\n\tif val, exist := parameters[name]; exist {\n\t\tswitch val := val.(type) {\n\t\tcase int64:\n\t\t\treturn val\n\t\tcase int:\n\t\t\treturn int64(val)\n\t\tdefault:\n\t\t\tlog.Logger().Error(\"type mismatch\",\n\t\t\t\tzap.String(\"param_name\", string(name)),\n\t\t\t\tzap.String(\"actual_type\", reflect.TypeOf(name).Name()))\n\t\t}\n\t}\n\treturn _default\n}\n\nfunc (parameters Params) GetFloat32(name ParamName, _default float32) float32 {\n\tif val, exist := parameters[name]; exist {\n\t\tswitch val := val.(type) {\n\t\tcase float32:\n\t\t\treturn val\n\t\tcase float64:\n\t\t\treturn float32(val)\n\t\tcase int:\n\t\t\treturn float32(val)\n\t\tdefault:\n\t\t\tlog.Logger().Error(\"type mismatch\",\n\t\t\t\tzap.String(\"param_name\", string(name)),\n\t\t\t\tzap.String(\"actual_type\", reflect.TypeOf(name).Name()))\n\t\t}\n\t}\n\treturn _default\n}\n\n// GetString gets a string parameter\nfunc (parameters Params) GetString(name ParamName, _default string) string {\n\tif val, exist := parameters[name]; exist {\n\t\treturn val.(string)\n\t}\n\treturn _default\n}\n\nfunc (parameters Params) GetIntSlice(name ParamName, _default []int) []int {\n\tif val, exist := parameters[name]; exist {\n\t\tswitch val := val.(type) {\n\t\tcase []int:\n\t\t\treturn val\n\t\tdefault:\n\t\t\tlog.Logger().Error(\"type mismatch\",\n\t\t\t\tzap.String(\"param_name\", string(name)),\n\t\t\t\tzap.String(\"actual_type\", reflect.TypeOf(name).Name()))\n\t\t}\n\t}\n\treturn _default\n}\n\nfunc (parameters Params) Overwrite(params Params) Params {\n\tmerged := make(Params)\n\tfor k, v := range parameters {\n\t\tmerged[k] = v\n\t}\n\tfor k, v := range params {\n\t\tmerged[k] = v\n\t}\n\treturn merged\n}\n\n// ParamsGrid contains candidate for grid search.\ntype ParamsGrid map[ParamName][]interface{}\n\nfunc (grid ParamsGrid) Len() int {\n\treturn len(grid)\n}\n\nfunc (grid ParamsGrid) NumCombinations() int {\n\tcount := 1\n\tfor _, values := range grid {\n\t\tcount *= len(values)\n\t}\n\treturn count\n}\n\nfunc (grid ParamsGrid) Fill(_default ParamsGrid) {\n\tfor param, values := range _default {\n\t\tif _, exist := grid[param]; !exist {\n\t\t\tgrid[param] = values\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "model/params_test.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage model\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestParams_Copy(t *testing.T) {\n\t// Create parameters\n\ta := Params{\n\t\tNFactors:    1,\n\t\tLr:          0.1,\n\t\tRandomState: 0,\n\t}\n\t// Create copy\n\tb := a.Copy()\n\tb[NFactors] = 2\n\tb[Lr] = 0.2\n\tb[RandomState] = 1\n\t// Check original parameters\n\tassert.Equal(t, 1, a.GetInt(NFactors, -1))\n\tassert.Equal(t, float32(0.1), a.GetFloat32(Lr, -0.1))\n\tassert.Equal(t, int64(0), a.GetInt64(RandomState, -1))\n\t// Check copy parameters\n\tassert.Equal(t, 2, b.GetInt(NFactors, -1))\n\tassert.Equal(t, float32(0.2), b.GetFloat32(Lr, -0.1))\n\tassert.Equal(t, int64(1), b.GetInt64(RandomState, -1))\n}\n\nfunc TestParams_GetFloat32(t *testing.T) {\n\tp := Params{}\n\t// Empty case\n\tassert.Equal(t, float32(0.1), p.GetFloat32(Lr, 0.1))\n\t// Normal case\n\tp[Lr] = float32(1.0)\n\tassert.Equal(t, float32(1.0), p.GetFloat32(Lr, 0.1))\n\t// Convertible case\n\tp[Lr] = 2.0\n\tassert.Equal(t, float32(2.0), p.GetFloat32(Lr, 0.1))\n\tp[Lr] = int(3)\n\tassert.Equal(t, float32(3.0), p.GetFloat32(Lr, 0.1))\n\t// Wrong type case\n\tp[Lr] = 1\n\tassert.Equal(t, float32(1.0), p.GetFloat32(Lr, 0.1))\n\tp[Lr] = \"hello\"\n\tassert.Equal(t, float32(0.1), p.GetFloat32(Lr, 0.1))\n}\n\nfunc TestParams_GetBool(t *testing.T) {\n\tp := Params{}\n\t// Empty case\n\tassert.True(t, p.GetBool(UseFeature, true))\n\t// Normal case\n\tp[UseFeature] = false\n\tassert.False(t, p.GetBool(UseFeature, true))\n\t// Wrong type case\n\tp[UseFeature] = 1\n\tassert.True(t, p.GetBool(UseFeature, true))\n}\n\nfunc TestParams_GetInt(t *testing.T) {\n\tp := Params{}\n\t// Empty case\n\tassert.Equal(t, -1, p.GetInt(NFactors, -1))\n\t// Normal case\n\tp[NFactors] = 0\n\tassert.Equal(t, 0, p.GetInt(NFactors, -1))\n\t// Wrong type case\n\tp[NFactors] = \"hello\"\n\tassert.Equal(t, -1, p.GetInt(NFactors, -1))\n}\n\nfunc TestParams_GetInt64(t *testing.T) {\n\tp := Params{}\n\t// Empty case\n\tassert.Equal(t, int64(-1), p.GetInt64(RandomState, -1))\n\t// Normal case\n\tp[RandomState] = int64(0)\n\tassert.Equal(t, int64(0), p.GetInt64(RandomState, -1))\n\t// Wrong type case\n\tp[RandomState] = 0\n\tassert.Equal(t, int64(0), p.GetInt64(RandomState, -1))\n\tp[RandomState] = \"hello\"\n\tassert.Equal(t, int64(-1), p.GetInt64(RandomState, -1))\n}\n\nfunc TestParams_GetString(t *testing.T) {\n\tp := Params{}\n\t// Empty case\n\tassert.Equal(t, \"xyz\", p.GetString(Similarity, \"xyz\"))\n\t// Normal case\n\tp[Similarity] = \"abc\"\n\tassert.Equal(t, \"abc\", p.GetString(Similarity, \"abc\"))\n}\n\nfunc TestParams_GetIntSlice(t *testing.T) {\n\tp := Params{}\n\t// Empty case\n\tassert.Equal(t, []int{1, 2, 3}, p.GetIntSlice(HiddenLayers, []int{1, 2, 3}))\n\t// Normal case\n\tp[HiddenLayers] = []int{4, 5, 6}\n\tassert.Equal(t, []int{4, 5, 6}, p.GetIntSlice(HiddenLayers, []int{1, 2, 3}))\n\t// Wrong type case\n\tp[HiddenLayers] = []string{\"hello\"}\n\tassert.Equal(t, []int{1, 2, 3}, p.GetIntSlice(HiddenLayers, []int{1, 2, 3}))\n}\n\nfunc TestParams_Overwrite(t *testing.T) {\n\ta := Params{\n\t\tNFactors: 10,\n\t\tLr:       0.5,\n\t}\n\tb := Params{\n\t\tNEpochs:  100,\n\t\tNFactors: 20,\n\t}\n\tc := a.Overwrite(b)\n\tassert.Equal(t, 20, c[NFactors])\n\tassert.Equal(t, 0.5, c[Lr])\n\tassert.Equal(t, 100, c[NEpochs])\n}\n\nfunc TestParamsGrid(t *testing.T) {\n\tgrid := ParamsGrid{}\n\tgrid[\"a\"] = []interface{}{0, 1}\n\tdefaultGrid := ParamsGrid{}\n\tdefaultGrid[\"a\"] = []interface{}{2, 3}\n\tdefaultGrid[\"b\"] = []interface{}{4, 5}\n\tassert.Equal(t, 1, grid.Len())\n\tgrid.Fill(defaultGrid)\n\tassert.Equal(t, []interface{}{0, 1}, grid[\"a\"])\n\tassert.Equal(t, []interface{}{4, 5}, grid[\"b\"])\n\tassert.Equal(t, 4, grid.NumCombinations())\n}\n"
  },
  {
    "path": "protocol/cache_store.pb.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.10\n// \tprotoc        v6.33.1\n// source: cache_store.proto\n\npackage protocol\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\ttimestamppb \"google.golang.org/protobuf/types/known/timestamppb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype Value struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tValue         string                 `protobuf:\"bytes,2,opt,name=value,proto3\" json:\"value,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Value) Reset() {\n\t*x = Value{}\n\tmi := &file_cache_store_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Value) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Value) ProtoMessage() {}\n\nfunc (x *Value) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Value.ProtoReflect.Descriptor instead.\nfunc (*Value) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *Value) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *Value) GetValue() string {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn \"\"\n}\n\ntype Score struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tScore         float64                `protobuf:\"fixed64,2,opt,name=score,proto3\" json:\"score,omitempty\"`\n\tIsHidden      bool                   `protobuf:\"varint,3,opt,name=is_hidden,json=isHidden,proto3\" json:\"is_hidden,omitempty\"`\n\tCategories    []string               `protobuf:\"bytes,4,rep,name=categories,proto3\" json:\"categories,omitempty\"`\n\tTimestamp     *timestamppb.Timestamp `protobuf:\"bytes,5,opt,name=timestamp,proto3\" json:\"timestamp,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Score) Reset() {\n\t*x = Score{}\n\tmi := &file_cache_store_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Score) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Score) ProtoMessage() {}\n\nfunc (x *Score) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Score.ProtoReflect.Descriptor instead.\nfunc (*Score) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *Score) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *Score) GetScore() float64 {\n\tif x != nil {\n\t\treturn x.Score\n\t}\n\treturn 0\n}\n\nfunc (x *Score) GetIsHidden() bool {\n\tif x != nil {\n\t\treturn x.IsHidden\n\t}\n\treturn false\n}\n\nfunc (x *Score) GetCategories() []string {\n\tif x != nil {\n\t\treturn x.Categories\n\t}\n\treturn nil\n}\n\nfunc (x *Score) GetTimestamp() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.Timestamp\n\t}\n\treturn nil\n}\n\ntype ScoreCondition struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tSubset        *string                `protobuf:\"bytes,1,opt,name=subset,proto3,oneof\" json:\"subset,omitempty\"`\n\tId            *string                `protobuf:\"bytes,2,opt,name=id,proto3,oneof\" json:\"id,omitempty\"`\n\tBefore        *timestamppb.Timestamp `protobuf:\"bytes,3,opt,name=before,proto3,oneof\" json:\"before,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ScoreCondition) Reset() {\n\t*x = ScoreCondition{}\n\tmi := &file_cache_store_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ScoreCondition) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ScoreCondition) ProtoMessage() {}\n\nfunc (x *ScoreCondition) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ScoreCondition.ProtoReflect.Descriptor instead.\nfunc (*ScoreCondition) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *ScoreCondition) GetSubset() string {\n\tif x != nil && x.Subset != nil {\n\t\treturn *x.Subset\n\t}\n\treturn \"\"\n}\n\nfunc (x *ScoreCondition) GetId() string {\n\tif x != nil && x.Id != nil {\n\t\treturn *x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *ScoreCondition) GetBefore() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.Before\n\t}\n\treturn nil\n}\n\ntype ScorePatch struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tIsHidden      *bool                  `protobuf:\"varint,1,opt,name=is_hidden,json=isHidden,proto3,oneof\" json:\"is_hidden,omitempty\"`\n\tCategories    []string               `protobuf:\"bytes,2,rep,name=categories,proto3\" json:\"categories,omitempty\"`\n\tScore         *float64               `protobuf:\"fixed64,3,opt,name=score,proto3,oneof\" json:\"score,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ScorePatch) Reset() {\n\t*x = ScorePatch{}\n\tmi := &file_cache_store_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ScorePatch) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ScorePatch) ProtoMessage() {}\n\nfunc (x *ScorePatch) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ScorePatch.ProtoReflect.Descriptor instead.\nfunc (*ScorePatch) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *ScorePatch) GetIsHidden() bool {\n\tif x != nil && x.IsHidden != nil {\n\t\treturn *x.IsHidden\n\t}\n\treturn false\n}\n\nfunc (x *ScorePatch) GetCategories() []string {\n\tif x != nil {\n\t\treturn x.Categories\n\t}\n\treturn nil\n}\n\nfunc (x *ScorePatch) GetScore() float64 {\n\tif x != nil && x.Score != nil {\n\t\treturn *x.Score\n\t}\n\treturn 0\n}\n\ntype TimeSeriesPoint struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tTimestamp     *timestamppb.Timestamp `protobuf:\"bytes,2,opt,name=timestamp,proto3\" json:\"timestamp,omitempty\"`\n\tValue         float64                `protobuf:\"fixed64,3,opt,name=value,proto3\" json:\"value,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TimeSeriesPoint) Reset() {\n\t*x = TimeSeriesPoint{}\n\tmi := &file_cache_store_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TimeSeriesPoint) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TimeSeriesPoint) ProtoMessage() {}\n\nfunc (x *TimeSeriesPoint) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TimeSeriesPoint.ProtoReflect.Descriptor instead.\nfunc (*TimeSeriesPoint) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *TimeSeriesPoint) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *TimeSeriesPoint) GetTimestamp() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.Timestamp\n\t}\n\treturn nil\n}\n\nfunc (x *TimeSeriesPoint) GetValue() float64 {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn 0\n}\n\ntype GetRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetRequest) Reset() {\n\t*x = GetRequest{}\n\tmi := &file_cache_store_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetRequest) ProtoMessage() {}\n\nfunc (x *GetRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetRequest.ProtoReflect.Descriptor instead.\nfunc (*GetRequest) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *GetRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\ntype GetResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tValue         *string                `protobuf:\"bytes,1,opt,name=value,proto3,oneof\" json:\"value,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetResponse) Reset() {\n\t*x = GetResponse{}\n\tmi := &file_cache_store_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetResponse) ProtoMessage() {}\n\nfunc (x *GetResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetResponse.ProtoReflect.Descriptor instead.\nfunc (*GetResponse) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *GetResponse) GetValue() string {\n\tif x != nil && x.Value != nil {\n\t\treturn *x.Value\n\t}\n\treturn \"\"\n}\n\ntype SetRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tValues        []*Value               `protobuf:\"bytes,1,rep,name=values,proto3\" json:\"values,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SetRequest) Reset() {\n\t*x = SetRequest{}\n\tmi := &file_cache_store_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SetRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SetRequest) ProtoMessage() {}\n\nfunc (x *SetRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SetRequest.ProtoReflect.Descriptor instead.\nfunc (*SetRequest) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{7}\n}\n\nfunc (x *SetRequest) GetValues() []*Value {\n\tif x != nil {\n\t\treturn x.Values\n\t}\n\treturn nil\n}\n\ntype SetResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SetResponse) Reset() {\n\t*x = SetResponse{}\n\tmi := &file_cache_store_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SetResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SetResponse) ProtoMessage() {}\n\nfunc (x *SetResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SetResponse.ProtoReflect.Descriptor instead.\nfunc (*SetResponse) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{8}\n}\n\ntype DeleteRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteRequest) Reset() {\n\t*x = DeleteRequest{}\n\tmi := &file_cache_store_proto_msgTypes[9]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteRequest) ProtoMessage() {}\n\nfunc (x *DeleteRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[9]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteRequest.ProtoReflect.Descriptor instead.\nfunc (*DeleteRequest) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{9}\n}\n\nfunc (x *DeleteRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\ntype DeleteResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteResponse) Reset() {\n\t*x = DeleteResponse{}\n\tmi := &file_cache_store_proto_msgTypes[10]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteResponse) ProtoMessage() {}\n\nfunc (x *DeleteResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[10]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteResponse.ProtoReflect.Descriptor instead.\nfunc (*DeleteResponse) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{10}\n}\n\ntype PushRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tValue         string                 `protobuf:\"bytes,2,opt,name=value,proto3\" json:\"value,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PushRequest) Reset() {\n\t*x = PushRequest{}\n\tmi := &file_cache_store_proto_msgTypes[11]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PushRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PushRequest) ProtoMessage() {}\n\nfunc (x *PushRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[11]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PushRequest.ProtoReflect.Descriptor instead.\nfunc (*PushRequest) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{11}\n}\n\nfunc (x *PushRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *PushRequest) GetValue() string {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn \"\"\n}\n\ntype PushResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PushResponse) Reset() {\n\t*x = PushResponse{}\n\tmi := &file_cache_store_proto_msgTypes[12]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PushResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PushResponse) ProtoMessage() {}\n\nfunc (x *PushResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[12]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PushResponse.ProtoReflect.Descriptor instead.\nfunc (*PushResponse) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{12}\n}\n\ntype PopRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PopRequest) Reset() {\n\t*x = PopRequest{}\n\tmi := &file_cache_store_proto_msgTypes[13]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PopRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PopRequest) ProtoMessage() {}\n\nfunc (x *PopRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[13]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PopRequest.ProtoReflect.Descriptor instead.\nfunc (*PopRequest) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{13}\n}\n\nfunc (x *PopRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\ntype PopResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tValue         *string                `protobuf:\"bytes,1,opt,name=value,proto3,oneof\" json:\"value,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PopResponse) Reset() {\n\t*x = PopResponse{}\n\tmi := &file_cache_store_proto_msgTypes[14]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PopResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PopResponse) ProtoMessage() {}\n\nfunc (x *PopResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[14]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PopResponse.ProtoReflect.Descriptor instead.\nfunc (*PopResponse) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{14}\n}\n\nfunc (x *PopResponse) GetValue() string {\n\tif x != nil && x.Value != nil {\n\t\treturn *x.Value\n\t}\n\treturn \"\"\n}\n\ntype RemainRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RemainRequest) Reset() {\n\t*x = RemainRequest{}\n\tmi := &file_cache_store_proto_msgTypes[15]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RemainRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RemainRequest) ProtoMessage() {}\n\nfunc (x *RemainRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[15]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RemainRequest.ProtoReflect.Descriptor instead.\nfunc (*RemainRequest) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{15}\n}\n\nfunc (x *RemainRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\ntype RemainResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCount         int64                  `protobuf:\"varint,1,opt,name=count,proto3\" json:\"count,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RemainResponse) Reset() {\n\t*x = RemainResponse{}\n\tmi := &file_cache_store_proto_msgTypes[16]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RemainResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RemainResponse) ProtoMessage() {}\n\nfunc (x *RemainResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[16]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RemainResponse.ProtoReflect.Descriptor instead.\nfunc (*RemainResponse) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{16}\n}\n\nfunc (x *RemainResponse) GetCount() int64 {\n\tif x != nil {\n\t\treturn x.Count\n\t}\n\treturn 0\n}\n\ntype AddScoresRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCollection    string                 `protobuf:\"bytes,1,opt,name=collection,proto3\" json:\"collection,omitempty\"`\n\tSubset        string                 `protobuf:\"bytes,2,opt,name=subset,proto3\" json:\"subset,omitempty\"`\n\tDocuments     []*Score               `protobuf:\"bytes,3,rep,name=documents,proto3\" json:\"documents,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *AddScoresRequest) Reset() {\n\t*x = AddScoresRequest{}\n\tmi := &file_cache_store_proto_msgTypes[17]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *AddScoresRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*AddScoresRequest) ProtoMessage() {}\n\nfunc (x *AddScoresRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[17]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use AddScoresRequest.ProtoReflect.Descriptor instead.\nfunc (*AddScoresRequest) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{17}\n}\n\nfunc (x *AddScoresRequest) GetCollection() string {\n\tif x != nil {\n\t\treturn x.Collection\n\t}\n\treturn \"\"\n}\n\nfunc (x *AddScoresRequest) GetSubset() string {\n\tif x != nil {\n\t\treturn x.Subset\n\t}\n\treturn \"\"\n}\n\nfunc (x *AddScoresRequest) GetDocuments() []*Score {\n\tif x != nil {\n\t\treturn x.Documents\n\t}\n\treturn nil\n}\n\ntype AddScoresResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *AddScoresResponse) Reset() {\n\t*x = AddScoresResponse{}\n\tmi := &file_cache_store_proto_msgTypes[18]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *AddScoresResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*AddScoresResponse) ProtoMessage() {}\n\nfunc (x *AddScoresResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[18]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use AddScoresResponse.ProtoReflect.Descriptor instead.\nfunc (*AddScoresResponse) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{18}\n}\n\ntype SearchScoresRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCollection    string                 `protobuf:\"bytes,1,opt,name=collection,proto3\" json:\"collection,omitempty\"`\n\tSubset        string                 `protobuf:\"bytes,2,opt,name=subset,proto3\" json:\"subset,omitempty\"`\n\tQuery         []string               `protobuf:\"bytes,3,rep,name=query,proto3\" json:\"query,omitempty\"`\n\tBegin         int32                  `protobuf:\"varint,4,opt,name=begin,proto3\" json:\"begin,omitempty\"`\n\tEnd           int32                  `protobuf:\"varint,5,opt,name=end,proto3\" json:\"end,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SearchScoresRequest) Reset() {\n\t*x = SearchScoresRequest{}\n\tmi := &file_cache_store_proto_msgTypes[19]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SearchScoresRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SearchScoresRequest) ProtoMessage() {}\n\nfunc (x *SearchScoresRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[19]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SearchScoresRequest.ProtoReflect.Descriptor instead.\nfunc (*SearchScoresRequest) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{19}\n}\n\nfunc (x *SearchScoresRequest) GetCollection() string {\n\tif x != nil {\n\t\treturn x.Collection\n\t}\n\treturn \"\"\n}\n\nfunc (x *SearchScoresRequest) GetSubset() string {\n\tif x != nil {\n\t\treturn x.Subset\n\t}\n\treturn \"\"\n}\n\nfunc (x *SearchScoresRequest) GetQuery() []string {\n\tif x != nil {\n\t\treturn x.Query\n\t}\n\treturn nil\n}\n\nfunc (x *SearchScoresRequest) GetBegin() int32 {\n\tif x != nil {\n\t\treturn x.Begin\n\t}\n\treturn 0\n}\n\nfunc (x *SearchScoresRequest) GetEnd() int32 {\n\tif x != nil {\n\t\treturn x.End\n\t}\n\treturn 0\n}\n\ntype SearchScoresResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tDocuments     []*Score               `protobuf:\"bytes,1,rep,name=documents,proto3\" json:\"documents,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SearchScoresResponse) Reset() {\n\t*x = SearchScoresResponse{}\n\tmi := &file_cache_store_proto_msgTypes[20]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SearchScoresResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SearchScoresResponse) ProtoMessage() {}\n\nfunc (x *SearchScoresResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[20]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SearchScoresResponse.ProtoReflect.Descriptor instead.\nfunc (*SearchScoresResponse) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{20}\n}\n\nfunc (x *SearchScoresResponse) GetDocuments() []*Score {\n\tif x != nil {\n\t\treturn x.Documents\n\t}\n\treturn nil\n}\n\ntype DeleteScoresRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCollection    []string               `protobuf:\"bytes,1,rep,name=collection,proto3\" json:\"collection,omitempty\"`\n\tCondition     *ScoreCondition        `protobuf:\"bytes,2,opt,name=condition,proto3\" json:\"condition,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteScoresRequest) Reset() {\n\t*x = DeleteScoresRequest{}\n\tmi := &file_cache_store_proto_msgTypes[21]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteScoresRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteScoresRequest) ProtoMessage() {}\n\nfunc (x *DeleteScoresRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[21]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteScoresRequest.ProtoReflect.Descriptor instead.\nfunc (*DeleteScoresRequest) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{21}\n}\n\nfunc (x *DeleteScoresRequest) GetCollection() []string {\n\tif x != nil {\n\t\treturn x.Collection\n\t}\n\treturn nil\n}\n\nfunc (x *DeleteScoresRequest) GetCondition() *ScoreCondition {\n\tif x != nil {\n\t\treturn x.Condition\n\t}\n\treturn nil\n}\n\ntype DeleteScoresResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteScoresResponse) Reset() {\n\t*x = DeleteScoresResponse{}\n\tmi := &file_cache_store_proto_msgTypes[22]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteScoresResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteScoresResponse) ProtoMessage() {}\n\nfunc (x *DeleteScoresResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[22]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteScoresResponse.ProtoReflect.Descriptor instead.\nfunc (*DeleteScoresResponse) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{22}\n}\n\ntype UpdateScoresRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCollection    []string               `protobuf:\"bytes,1,rep,name=collection,proto3\" json:\"collection,omitempty\"`\n\tSubset        *string                `protobuf:\"bytes,2,opt,name=subset,proto3,oneof\" json:\"subset,omitempty\"`\n\tId            string                 `protobuf:\"bytes,3,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tPatch         *ScorePatch            `protobuf:\"bytes,4,opt,name=patch,proto3\" json:\"patch,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UpdateScoresRequest) Reset() {\n\t*x = UpdateScoresRequest{}\n\tmi := &file_cache_store_proto_msgTypes[23]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UpdateScoresRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UpdateScoresRequest) ProtoMessage() {}\n\nfunc (x *UpdateScoresRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[23]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UpdateScoresRequest.ProtoReflect.Descriptor instead.\nfunc (*UpdateScoresRequest) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{23}\n}\n\nfunc (x *UpdateScoresRequest) GetCollection() []string {\n\tif x != nil {\n\t\treturn x.Collection\n\t}\n\treturn nil\n}\n\nfunc (x *UpdateScoresRequest) GetSubset() string {\n\tif x != nil && x.Subset != nil {\n\t\treturn *x.Subset\n\t}\n\treturn \"\"\n}\n\nfunc (x *UpdateScoresRequest) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *UpdateScoresRequest) GetPatch() *ScorePatch {\n\tif x != nil {\n\t\treturn x.Patch\n\t}\n\treturn nil\n}\n\ntype UpdateScoresResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UpdateScoresResponse) Reset() {\n\t*x = UpdateScoresResponse{}\n\tmi := &file_cache_store_proto_msgTypes[24]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UpdateScoresResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UpdateScoresResponse) ProtoMessage() {}\n\nfunc (x *UpdateScoresResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[24]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UpdateScoresResponse.ProtoReflect.Descriptor instead.\nfunc (*UpdateScoresResponse) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{24}\n}\n\ntype ScanScoresRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ScanScoresRequest) Reset() {\n\t*x = ScanScoresRequest{}\n\tmi := &file_cache_store_proto_msgTypes[25]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ScanScoresRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ScanScoresRequest) ProtoMessage() {}\n\nfunc (x *ScanScoresRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[25]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ScanScoresRequest.ProtoReflect.Descriptor instead.\nfunc (*ScanScoresRequest) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{25}\n}\n\ntype ScanScoresResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCollection    string                 `protobuf:\"bytes,1,opt,name=collection,proto3\" json:\"collection,omitempty\"`\n\tId            string                 `protobuf:\"bytes,2,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tSubset        string                 `protobuf:\"bytes,3,opt,name=subset,proto3\" json:\"subset,omitempty\"`\n\tTimestamp     *timestamppb.Timestamp `protobuf:\"bytes,4,opt,name=timestamp,proto3\" json:\"timestamp,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ScanScoresResponse) Reset() {\n\t*x = ScanScoresResponse{}\n\tmi := &file_cache_store_proto_msgTypes[26]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ScanScoresResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ScanScoresResponse) ProtoMessage() {}\n\nfunc (x *ScanScoresResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[26]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ScanScoresResponse.ProtoReflect.Descriptor instead.\nfunc (*ScanScoresResponse) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{26}\n}\n\nfunc (x *ScanScoresResponse) GetCollection() string {\n\tif x != nil {\n\t\treturn x.Collection\n\t}\n\treturn \"\"\n}\n\nfunc (x *ScanScoresResponse) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *ScanScoresResponse) GetSubset() string {\n\tif x != nil {\n\t\treturn x.Subset\n\t}\n\treturn \"\"\n}\n\nfunc (x *ScanScoresResponse) GetTimestamp() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.Timestamp\n\t}\n\treturn nil\n}\n\ntype AddTimeSeriesPointsRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tPoints        []*TimeSeriesPoint     `protobuf:\"bytes,1,rep,name=points,proto3\" json:\"points,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *AddTimeSeriesPointsRequest) Reset() {\n\t*x = AddTimeSeriesPointsRequest{}\n\tmi := &file_cache_store_proto_msgTypes[27]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *AddTimeSeriesPointsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*AddTimeSeriesPointsRequest) ProtoMessage() {}\n\nfunc (x *AddTimeSeriesPointsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[27]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use AddTimeSeriesPointsRequest.ProtoReflect.Descriptor instead.\nfunc (*AddTimeSeriesPointsRequest) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{27}\n}\n\nfunc (x *AddTimeSeriesPointsRequest) GetPoints() []*TimeSeriesPoint {\n\tif x != nil {\n\t\treturn x.Points\n\t}\n\treturn nil\n}\n\ntype AddTimeSeriesPointsResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *AddTimeSeriesPointsResponse) Reset() {\n\t*x = AddTimeSeriesPointsResponse{}\n\tmi := &file_cache_store_proto_msgTypes[28]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *AddTimeSeriesPointsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*AddTimeSeriesPointsResponse) ProtoMessage() {}\n\nfunc (x *AddTimeSeriesPointsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[28]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use AddTimeSeriesPointsResponse.ProtoReflect.Descriptor instead.\nfunc (*AddTimeSeriesPointsResponse) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{28}\n}\n\ntype GetTimeSeriesPointsRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tBegin         *timestamppb.Timestamp `protobuf:\"bytes,2,opt,name=begin,proto3\" json:\"begin,omitempty\"`\n\tEnd           *timestamppb.Timestamp `protobuf:\"bytes,3,opt,name=end,proto3\" json:\"end,omitempty\"`\n\tDuration      int64                  `protobuf:\"varint,4,opt,name=duration,proto3\" json:\"duration,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetTimeSeriesPointsRequest) Reset() {\n\t*x = GetTimeSeriesPointsRequest{}\n\tmi := &file_cache_store_proto_msgTypes[29]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetTimeSeriesPointsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetTimeSeriesPointsRequest) ProtoMessage() {}\n\nfunc (x *GetTimeSeriesPointsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[29]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetTimeSeriesPointsRequest.ProtoReflect.Descriptor instead.\nfunc (*GetTimeSeriesPointsRequest) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{29}\n}\n\nfunc (x *GetTimeSeriesPointsRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *GetTimeSeriesPointsRequest) GetBegin() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.Begin\n\t}\n\treturn nil\n}\n\nfunc (x *GetTimeSeriesPointsRequest) GetEnd() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.End\n\t}\n\treturn nil\n}\n\nfunc (x *GetTimeSeriesPointsRequest) GetDuration() int64 {\n\tif x != nil {\n\t\treturn x.Duration\n\t}\n\treturn 0\n}\n\ntype GetTimeSeriesPointsResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tPoints        []*TimeSeriesPoint     `protobuf:\"bytes,1,rep,name=points,proto3\" json:\"points,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetTimeSeriesPointsResponse) Reset() {\n\t*x = GetTimeSeriesPointsResponse{}\n\tmi := &file_cache_store_proto_msgTypes[30]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetTimeSeriesPointsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetTimeSeriesPointsResponse) ProtoMessage() {}\n\nfunc (x *GetTimeSeriesPointsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_cache_store_proto_msgTypes[30]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetTimeSeriesPointsResponse.ProtoReflect.Descriptor instead.\nfunc (*GetTimeSeriesPointsResponse) Descriptor() ([]byte, []int) {\n\treturn file_cache_store_proto_rawDescGZIP(), []int{30}\n}\n\nfunc (x *GetTimeSeriesPointsResponse) GetPoints() []*TimeSeriesPoint {\n\tif x != nil {\n\t\treturn x.Points\n\t}\n\treturn nil\n}\n\nvar File_cache_store_proto protoreflect.FileDescriptor\n\nconst file_cache_store_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x11cache_store.proto\\x12\\bprotocol\\x1a\\x1fgoogle/protobuf/timestamp.proto\\x1a\\x0eprotocol.proto\\\"1\\n\" +\n\t\"\\x05Value\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\tR\\x05value\\\"\\xa4\\x01\\n\" +\n\t\"\\x05Score\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x14\\n\" +\n\t\"\\x05score\\x18\\x02 \\x01(\\x01R\\x05score\\x12\\x1b\\n\" +\n\t\"\\tis_hidden\\x18\\x03 \\x01(\\bR\\bisHidden\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"categories\\x18\\x04 \\x03(\\tR\\n\" +\n\t\"categories\\x128\\n\" +\n\t\"\\ttimestamp\\x18\\x05 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\ttimestamp\\\"\\x98\\x01\\n\" +\n\t\"\\x0eScoreCondition\\x12\\x1b\\n\" +\n\t\"\\x06subset\\x18\\x01 \\x01(\\tH\\x00R\\x06subset\\x88\\x01\\x01\\x12\\x13\\n\" +\n\t\"\\x02id\\x18\\x02 \\x01(\\tH\\x01R\\x02id\\x88\\x01\\x01\\x127\\n\" +\n\t\"\\x06before\\x18\\x03 \\x01(\\v2\\x1a.google.protobuf.TimestampH\\x02R\\x06before\\x88\\x01\\x01B\\t\\n\" +\n\t\"\\a_subsetB\\x05\\n\" +\n\t\"\\x03_idB\\t\\n\" +\n\t\"\\a_before\\\"\\x81\\x01\\n\" +\n\t\"\\n\" +\n\t\"ScorePatch\\x12 \\n\" +\n\t\"\\tis_hidden\\x18\\x01 \\x01(\\bH\\x00R\\bisHidden\\x88\\x01\\x01\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"categories\\x18\\x02 \\x03(\\tR\\n\" +\n\t\"categories\\x12\\x19\\n\" +\n\t\"\\x05score\\x18\\x03 \\x01(\\x01H\\x01R\\x05score\\x88\\x01\\x01B\\f\\n\" +\n\t\"\\n\" +\n\t\"_is_hiddenB\\b\\n\" +\n\t\"\\x06_score\\\"u\\n\" +\n\t\"\\x0fTimeSeriesPoint\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x128\\n\" +\n\t\"\\ttimestamp\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\ttimestamp\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x03 \\x01(\\x01R\\x05value\\\" \\n\" +\n\t\"\\n\" +\n\t\"GetRequest\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\\"2\\n\" +\n\t\"\\vGetResponse\\x12\\x19\\n\" +\n\t\"\\x05value\\x18\\x01 \\x01(\\tH\\x00R\\x05value\\x88\\x01\\x01B\\b\\n\" +\n\t\"\\x06_value\\\"5\\n\" +\n\t\"\\n\" +\n\t\"SetRequest\\x12'\\n\" +\n\t\"\\x06values\\x18\\x01 \\x03(\\v2\\x0f.protocol.ValueR\\x06values\\\"\\r\\n\" +\n\t\"\\vSetResponse\\\"#\\n\" +\n\t\"\\rDeleteRequest\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\\"\\x10\\n\" +\n\t\"\\x0eDeleteResponse\\\"7\\n\" +\n\t\"\\vPushRequest\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\tR\\x05value\\\"\\x0e\\n\" +\n\t\"\\fPushResponse\\\" \\n\" +\n\t\"\\n\" +\n\t\"PopRequest\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\\"2\\n\" +\n\t\"\\vPopResponse\\x12\\x19\\n\" +\n\t\"\\x05value\\x18\\x01 \\x01(\\tH\\x00R\\x05value\\x88\\x01\\x01B\\b\\n\" +\n\t\"\\x06_value\\\"#\\n\" +\n\t\"\\rRemainRequest\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\\"&\\n\" +\n\t\"\\x0eRemainResponse\\x12\\x14\\n\" +\n\t\"\\x05count\\x18\\x01 \\x01(\\x03R\\x05count\\\"y\\n\" +\n\t\"\\x10AddScoresRequest\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"collection\\x18\\x01 \\x01(\\tR\\n\" +\n\t\"collection\\x12\\x16\\n\" +\n\t\"\\x06subset\\x18\\x02 \\x01(\\tR\\x06subset\\x12-\\n\" +\n\t\"\\tdocuments\\x18\\x03 \\x03(\\v2\\x0f.protocol.ScoreR\\tdocuments\\\"\\x13\\n\" +\n\t\"\\x11AddScoresResponse\\\"\\x8b\\x01\\n\" +\n\t\"\\x13SearchScoresRequest\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"collection\\x18\\x01 \\x01(\\tR\\n\" +\n\t\"collection\\x12\\x16\\n\" +\n\t\"\\x06subset\\x18\\x02 \\x01(\\tR\\x06subset\\x12\\x14\\n\" +\n\t\"\\x05query\\x18\\x03 \\x03(\\tR\\x05query\\x12\\x14\\n\" +\n\t\"\\x05begin\\x18\\x04 \\x01(\\x05R\\x05begin\\x12\\x10\\n\" +\n\t\"\\x03end\\x18\\x05 \\x01(\\x05R\\x03end\\\"E\\n\" +\n\t\"\\x14SearchScoresResponse\\x12-\\n\" +\n\t\"\\tdocuments\\x18\\x01 \\x03(\\v2\\x0f.protocol.ScoreR\\tdocuments\\\"m\\n\" +\n\t\"\\x13DeleteScoresRequest\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"collection\\x18\\x01 \\x03(\\tR\\n\" +\n\t\"collection\\x126\\n\" +\n\t\"\\tcondition\\x18\\x02 \\x01(\\v2\\x18.protocol.ScoreConditionR\\tcondition\\\"\\x16\\n\" +\n\t\"\\x14DeleteScoresResponse\\\"\\x99\\x01\\n\" +\n\t\"\\x13UpdateScoresRequest\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"collection\\x18\\x01 \\x03(\\tR\\n\" +\n\t\"collection\\x12\\x1b\\n\" +\n\t\"\\x06subset\\x18\\x02 \\x01(\\tH\\x00R\\x06subset\\x88\\x01\\x01\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x03 \\x01(\\tR\\x02id\\x12*\\n\" +\n\t\"\\x05patch\\x18\\x04 \\x01(\\v2\\x14.protocol.ScorePatchR\\x05patchB\\t\\n\" +\n\t\"\\a_subset\\\"\\x16\\n\" +\n\t\"\\x14UpdateScoresResponse\\\"\\x13\\n\" +\n\t\"\\x11ScanScoresRequest\\\"\\x96\\x01\\n\" +\n\t\"\\x12ScanScoresResponse\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"collection\\x18\\x01 \\x01(\\tR\\n\" +\n\t\"collection\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x02 \\x01(\\tR\\x02id\\x12\\x16\\n\" +\n\t\"\\x06subset\\x18\\x03 \\x01(\\tR\\x06subset\\x128\\n\" +\n\t\"\\ttimestamp\\x18\\x04 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\ttimestamp\\\"O\\n\" +\n\t\"\\x1aAddTimeSeriesPointsRequest\\x121\\n\" +\n\t\"\\x06points\\x18\\x01 \\x03(\\v2\\x19.protocol.TimeSeriesPointR\\x06points\\\"\\x1d\\n\" +\n\t\"\\x1bAddTimeSeriesPointsResponse\\\"\\xac\\x01\\n\" +\n\t\"\\x1aGetTimeSeriesPointsRequest\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x120\\n\" +\n\t\"\\x05begin\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\x05begin\\x12,\\n\" +\n\t\"\\x03end\\x18\\x03 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\x03end\\x12\\x1a\\n\" +\n\t\"\\bduration\\x18\\x04 \\x01(\\x03R\\bduration\\\"P\\n\" +\n\t\"\\x1bGetTimeSeriesPointsResponse\\x121\\n\" +\n\t\"\\x06points\\x18\\x01 \\x03(\\v2\\x19.protocol.TimeSeriesPointR\\x06points2\\xf2\\a\\n\" +\n\t\"\\n\" +\n\t\"CacheStore\\x127\\n\" +\n\t\"\\x04Ping\\x12\\x15.protocol.PingRequest\\x1a\\x16.protocol.PingResponse\\\"\\x00\\x124\\n\" +\n\t\"\\x03Get\\x12\\x14.protocol.GetRequest\\x1a\\x15.protocol.GetResponse\\\"\\x00\\x124\\n\" +\n\t\"\\x03Set\\x12\\x14.protocol.SetRequest\\x1a\\x15.protocol.SetResponse\\\"\\x00\\x12=\\n\" +\n\t\"\\x06Delete\\x12\\x17.protocol.DeleteRequest\\x1a\\x18.protocol.DeleteResponse\\\"\\x00\\x127\\n\" +\n\t\"\\x04Push\\x12\\x15.protocol.PushRequest\\x1a\\x16.protocol.PushResponse\\\"\\x00\\x124\\n\" +\n\t\"\\x03Pop\\x12\\x14.protocol.PopRequest\\x1a\\x15.protocol.PopResponse\\\"\\x00\\x12=\\n\" +\n\t\"\\x06Remain\\x12\\x17.protocol.RemainRequest\\x1a\\x18.protocol.RemainResponse\\\"\\x00\\x12F\\n\" +\n\t\"\\tAddScores\\x12\\x1a.protocol.AddScoresRequest\\x1a\\x1b.protocol.AddScoresResponse\\\"\\x00\\x12O\\n\" +\n\t\"\\fSearchScores\\x12\\x1d.protocol.SearchScoresRequest\\x1a\\x1e.protocol.SearchScoresResponse\\\"\\x00\\x12O\\n\" +\n\t\"\\fDeleteScores\\x12\\x1d.protocol.DeleteScoresRequest\\x1a\\x1e.protocol.DeleteScoresResponse\\\"\\x00\\x12O\\n\" +\n\t\"\\fUpdateScores\\x12\\x1d.protocol.UpdateScoresRequest\\x1a\\x1e.protocol.UpdateScoresResponse\\\"\\x00\\x12K\\n\" +\n\t\"\\n\" +\n\t\"ScanScores\\x12\\x1b.protocol.ScanScoresRequest\\x1a\\x1c.protocol.ScanScoresResponse\\\"\\x000\\x01\\x12d\\n\" +\n\t\"\\x13AddTimeSeriesPoints\\x12$.protocol.AddTimeSeriesPointsRequest\\x1a%.protocol.AddTimeSeriesPointsResponse\\\"\\x00\\x12d\\n\" +\n\t\"\\x13GetTimeSeriesPoints\\x12$.protocol.GetTimeSeriesPointsRequest\\x1a%.protocol.GetTimeSeriesPointsResponse\\\"\\x00B$Z\\\"github.com/gorse-io/gorse/protocolb\\x06proto3\"\n\nvar (\n\tfile_cache_store_proto_rawDescOnce sync.Once\n\tfile_cache_store_proto_rawDescData []byte\n)\n\nfunc file_cache_store_proto_rawDescGZIP() []byte {\n\tfile_cache_store_proto_rawDescOnce.Do(func() {\n\t\tfile_cache_store_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_cache_store_proto_rawDesc), len(file_cache_store_proto_rawDesc)))\n\t})\n\treturn file_cache_store_proto_rawDescData\n}\n\nvar file_cache_store_proto_msgTypes = make([]protoimpl.MessageInfo, 31)\nvar file_cache_store_proto_goTypes = []any{\n\t(*Value)(nil),                       // 0: protocol.Value\n\t(*Score)(nil),                       // 1: protocol.Score\n\t(*ScoreCondition)(nil),              // 2: protocol.ScoreCondition\n\t(*ScorePatch)(nil),                  // 3: protocol.ScorePatch\n\t(*TimeSeriesPoint)(nil),             // 4: protocol.TimeSeriesPoint\n\t(*GetRequest)(nil),                  // 5: protocol.GetRequest\n\t(*GetResponse)(nil),                 // 6: protocol.GetResponse\n\t(*SetRequest)(nil),                  // 7: protocol.SetRequest\n\t(*SetResponse)(nil),                 // 8: protocol.SetResponse\n\t(*DeleteRequest)(nil),               // 9: protocol.DeleteRequest\n\t(*DeleteResponse)(nil),              // 10: protocol.DeleteResponse\n\t(*PushRequest)(nil),                 // 11: protocol.PushRequest\n\t(*PushResponse)(nil),                // 12: protocol.PushResponse\n\t(*PopRequest)(nil),                  // 13: protocol.PopRequest\n\t(*PopResponse)(nil),                 // 14: protocol.PopResponse\n\t(*RemainRequest)(nil),               // 15: protocol.RemainRequest\n\t(*RemainResponse)(nil),              // 16: protocol.RemainResponse\n\t(*AddScoresRequest)(nil),            // 17: protocol.AddScoresRequest\n\t(*AddScoresResponse)(nil),           // 18: protocol.AddScoresResponse\n\t(*SearchScoresRequest)(nil),         // 19: protocol.SearchScoresRequest\n\t(*SearchScoresResponse)(nil),        // 20: protocol.SearchScoresResponse\n\t(*DeleteScoresRequest)(nil),         // 21: protocol.DeleteScoresRequest\n\t(*DeleteScoresResponse)(nil),        // 22: protocol.DeleteScoresResponse\n\t(*UpdateScoresRequest)(nil),         // 23: protocol.UpdateScoresRequest\n\t(*UpdateScoresResponse)(nil),        // 24: protocol.UpdateScoresResponse\n\t(*ScanScoresRequest)(nil),           // 25: protocol.ScanScoresRequest\n\t(*ScanScoresResponse)(nil),          // 26: protocol.ScanScoresResponse\n\t(*AddTimeSeriesPointsRequest)(nil),  // 27: protocol.AddTimeSeriesPointsRequest\n\t(*AddTimeSeriesPointsResponse)(nil), // 28: protocol.AddTimeSeriesPointsResponse\n\t(*GetTimeSeriesPointsRequest)(nil),  // 29: protocol.GetTimeSeriesPointsRequest\n\t(*GetTimeSeriesPointsResponse)(nil), // 30: protocol.GetTimeSeriesPointsResponse\n\t(*timestamppb.Timestamp)(nil),       // 31: google.protobuf.Timestamp\n\t(*PingRequest)(nil),                 // 32: protocol.PingRequest\n\t(*PingResponse)(nil),                // 33: protocol.PingResponse\n}\nvar file_cache_store_proto_depIdxs = []int32{\n\t31, // 0: protocol.Score.timestamp:type_name -> google.protobuf.Timestamp\n\t31, // 1: protocol.ScoreCondition.before:type_name -> google.protobuf.Timestamp\n\t31, // 2: protocol.TimeSeriesPoint.timestamp:type_name -> google.protobuf.Timestamp\n\t0,  // 3: protocol.SetRequest.values:type_name -> protocol.Value\n\t1,  // 4: protocol.AddScoresRequest.documents:type_name -> protocol.Score\n\t1,  // 5: protocol.SearchScoresResponse.documents:type_name -> protocol.Score\n\t2,  // 6: protocol.DeleteScoresRequest.condition:type_name -> protocol.ScoreCondition\n\t3,  // 7: protocol.UpdateScoresRequest.patch:type_name -> protocol.ScorePatch\n\t31, // 8: protocol.ScanScoresResponse.timestamp:type_name -> google.protobuf.Timestamp\n\t4,  // 9: protocol.AddTimeSeriesPointsRequest.points:type_name -> protocol.TimeSeriesPoint\n\t31, // 10: protocol.GetTimeSeriesPointsRequest.begin:type_name -> google.protobuf.Timestamp\n\t31, // 11: protocol.GetTimeSeriesPointsRequest.end:type_name -> google.protobuf.Timestamp\n\t4,  // 12: protocol.GetTimeSeriesPointsResponse.points:type_name -> protocol.TimeSeriesPoint\n\t32, // 13: protocol.CacheStore.Ping:input_type -> protocol.PingRequest\n\t5,  // 14: protocol.CacheStore.Get:input_type -> protocol.GetRequest\n\t7,  // 15: protocol.CacheStore.Set:input_type -> protocol.SetRequest\n\t9,  // 16: protocol.CacheStore.Delete:input_type -> protocol.DeleteRequest\n\t11, // 17: protocol.CacheStore.Push:input_type -> protocol.PushRequest\n\t13, // 18: protocol.CacheStore.Pop:input_type -> protocol.PopRequest\n\t15, // 19: protocol.CacheStore.Remain:input_type -> protocol.RemainRequest\n\t17, // 20: protocol.CacheStore.AddScores:input_type -> protocol.AddScoresRequest\n\t19, // 21: protocol.CacheStore.SearchScores:input_type -> protocol.SearchScoresRequest\n\t21, // 22: protocol.CacheStore.DeleteScores:input_type -> protocol.DeleteScoresRequest\n\t23, // 23: protocol.CacheStore.UpdateScores:input_type -> protocol.UpdateScoresRequest\n\t25, // 24: protocol.CacheStore.ScanScores:input_type -> protocol.ScanScoresRequest\n\t27, // 25: protocol.CacheStore.AddTimeSeriesPoints:input_type -> protocol.AddTimeSeriesPointsRequest\n\t29, // 26: protocol.CacheStore.GetTimeSeriesPoints:input_type -> protocol.GetTimeSeriesPointsRequest\n\t33, // 27: protocol.CacheStore.Ping:output_type -> protocol.PingResponse\n\t6,  // 28: protocol.CacheStore.Get:output_type -> protocol.GetResponse\n\t8,  // 29: protocol.CacheStore.Set:output_type -> protocol.SetResponse\n\t10, // 30: protocol.CacheStore.Delete:output_type -> protocol.DeleteResponse\n\t12, // 31: protocol.CacheStore.Push:output_type -> protocol.PushResponse\n\t14, // 32: protocol.CacheStore.Pop:output_type -> protocol.PopResponse\n\t16, // 33: protocol.CacheStore.Remain:output_type -> protocol.RemainResponse\n\t18, // 34: protocol.CacheStore.AddScores:output_type -> protocol.AddScoresResponse\n\t20, // 35: protocol.CacheStore.SearchScores:output_type -> protocol.SearchScoresResponse\n\t22, // 36: protocol.CacheStore.DeleteScores:output_type -> protocol.DeleteScoresResponse\n\t24, // 37: protocol.CacheStore.UpdateScores:output_type -> protocol.UpdateScoresResponse\n\t26, // 38: protocol.CacheStore.ScanScores:output_type -> protocol.ScanScoresResponse\n\t28, // 39: protocol.CacheStore.AddTimeSeriesPoints:output_type -> protocol.AddTimeSeriesPointsResponse\n\t30, // 40: protocol.CacheStore.GetTimeSeriesPoints:output_type -> protocol.GetTimeSeriesPointsResponse\n\t27, // [27:41] is the sub-list for method output_type\n\t13, // [13:27] is the sub-list for method input_type\n\t13, // [13:13] is the sub-list for extension type_name\n\t13, // [13:13] is the sub-list for extension extendee\n\t0,  // [0:13] is the sub-list for field type_name\n}\n\nfunc init() { file_cache_store_proto_init() }\nfunc file_cache_store_proto_init() {\n\tif File_cache_store_proto != nil {\n\t\treturn\n\t}\n\tfile_protocol_proto_init()\n\tfile_cache_store_proto_msgTypes[2].OneofWrappers = []any{}\n\tfile_cache_store_proto_msgTypes[3].OneofWrappers = []any{}\n\tfile_cache_store_proto_msgTypes[6].OneofWrappers = []any{}\n\tfile_cache_store_proto_msgTypes[14].OneofWrappers = []any{}\n\tfile_cache_store_proto_msgTypes[23].OneofWrappers = []any{}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_cache_store_proto_rawDesc), len(file_cache_store_proto_rawDesc)),\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   31,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_cache_store_proto_goTypes,\n\t\tDependencyIndexes: file_cache_store_proto_depIdxs,\n\t\tMessageInfos:      file_cache_store_proto_msgTypes,\n\t}.Build()\n\tFile_cache_store_proto = out.File\n\tfile_cache_store_proto_goTypes = nil\n\tfile_cache_store_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "protocol/cache_store.proto",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nsyntax = \"proto3\";\n\noption go_package = \"github.com/gorse-io/gorse/protocol\";\n\npackage protocol;\n\nimport \"google/protobuf/timestamp.proto\";\nimport \"protocol.proto\";\n\nmessage Value {\n  string name = 1;\n  string value = 2;\n}\n\nmessage Score {\n  string id = 1;\n  double score = 2;\n  bool is_hidden = 3;\n  repeated string categories = 4;\n  google.protobuf.Timestamp timestamp = 5;\n}\n\nmessage ScoreCondition {\n  optional string subset = 1;\n  optional string id = 2;\n  optional google.protobuf.Timestamp before = 3;\n}\n\nmessage ScorePatch {\n  optional bool is_hidden = 1;\n  repeated string categories = 2;\n  optional double score = 3;\n}\n\nmessage TimeSeriesPoint {\n  string name = 1;\n  google.protobuf.Timestamp timestamp = 2;\n  double value = 3;\n}\n\nmessage GetRequest { string name = 1; }\n\nmessage GetResponse { optional string value = 1; }\n\nmessage SetRequest { repeated Value values = 1; }\n\nmessage SetResponse {}\n\nmessage DeleteRequest { string name = 1; }\n\nmessage DeleteResponse {}\n\n\n\nmessage PushRequest {\n  string name = 1;\n  string value = 2;\n}\n\nmessage PushResponse {}\n\nmessage PopRequest { string name = 1; }\n\nmessage PopResponse { optional string value = 1; }\n\nmessage RemainRequest { string name = 1; }\n\nmessage RemainResponse { int64 count = 1; }\n\nmessage AddScoresRequest {\n  string collection = 1;\n  string subset = 2;\n  repeated Score documents = 3;\n}\n\nmessage AddScoresResponse {}\n\nmessage SearchScoresRequest {\n  string collection = 1;\n  string subset = 2;\n  repeated string query = 3;\n  int32 begin = 4;\n  int32 end = 5;\n}\n\nmessage SearchScoresResponse { repeated Score documents = 1; }\n\nmessage DeleteScoresRequest {\n  repeated string collection = 1;\n  ScoreCondition condition = 2;\n}\n\nmessage DeleteScoresResponse {}\n\nmessage UpdateScoresRequest {\n  repeated string collection = 1;\n  optional string subset = 2;\n  string id = 3;\n  ScorePatch patch = 4;\n}\n\nmessage UpdateScoresResponse {}\n\nmessage ScanScoresRequest {}\n\nmessage ScanScoresResponse {\n  string collection = 1;\n  string id = 2;\n  string subset = 3;\n  google.protobuf.Timestamp timestamp = 4;\n}\n\nmessage AddTimeSeriesPointsRequest { repeated TimeSeriesPoint points = 1; }\n\nmessage AddTimeSeriesPointsResponse {}\n\nmessage GetTimeSeriesPointsRequest {\n  string name = 1;\n  google.protobuf.Timestamp begin = 2;\n  google.protobuf.Timestamp end = 3;\n  int64 duration = 4;\n}\n\nmessage GetTimeSeriesPointsResponse { repeated TimeSeriesPoint points = 1; }\n\nservice CacheStore {\n  rpc Ping(PingRequest) returns (PingResponse) {}\n  rpc Get(GetRequest) returns (GetResponse) {}\n  rpc Set(SetRequest) returns (SetResponse) {}\n  rpc Delete(DeleteRequest) returns (DeleteResponse) {}\n  rpc Push(PushRequest) returns (PushResponse) {}\n  rpc Pop(PopRequest) returns (PopResponse) {}\n  rpc Remain(RemainRequest) returns (RemainResponse) {}\n  rpc AddScores(AddScoresRequest) returns (AddScoresResponse) {}\n  rpc SearchScores(SearchScoresRequest) returns (SearchScoresResponse) {}\n  rpc DeleteScores(DeleteScoresRequest) returns (DeleteScoresResponse) {}\n  rpc UpdateScores(UpdateScoresRequest) returns (UpdateScoresResponse) {}\n  rpc ScanScores(ScanScoresRequest) returns (stream ScanScoresResponse) {}\n  rpc AddTimeSeriesPoints(AddTimeSeriesPointsRequest)\n      returns (AddTimeSeriesPointsResponse) {}\n  rpc GetTimeSeriesPoints(GetTimeSeriesPointsRequest)\n      returns (GetTimeSeriesPointsResponse) {}\n}\n"
  },
  {
    "path": "protocol/cache_store_grpc.pb.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.6.0\n// - protoc             v6.33.1\n// source: cache_store.proto\n\npackage protocol\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.64.0 or later.\nconst _ = grpc.SupportPackageIsVersion9\n\nconst (\n\tCacheStore_Ping_FullMethodName                = \"/protocol.CacheStore/Ping\"\n\tCacheStore_Get_FullMethodName                 = \"/protocol.CacheStore/Get\"\n\tCacheStore_Set_FullMethodName                 = \"/protocol.CacheStore/Set\"\n\tCacheStore_Delete_FullMethodName              = \"/protocol.CacheStore/Delete\"\n\tCacheStore_Push_FullMethodName                = \"/protocol.CacheStore/Push\"\n\tCacheStore_Pop_FullMethodName                 = \"/protocol.CacheStore/Pop\"\n\tCacheStore_Remain_FullMethodName              = \"/protocol.CacheStore/Remain\"\n\tCacheStore_AddScores_FullMethodName           = \"/protocol.CacheStore/AddScores\"\n\tCacheStore_SearchScores_FullMethodName        = \"/protocol.CacheStore/SearchScores\"\n\tCacheStore_DeleteScores_FullMethodName        = \"/protocol.CacheStore/DeleteScores\"\n\tCacheStore_UpdateScores_FullMethodName        = \"/protocol.CacheStore/UpdateScores\"\n\tCacheStore_ScanScores_FullMethodName          = \"/protocol.CacheStore/ScanScores\"\n\tCacheStore_AddTimeSeriesPoints_FullMethodName = \"/protocol.CacheStore/AddTimeSeriesPoints\"\n\tCacheStore_GetTimeSeriesPoints_FullMethodName = \"/protocol.CacheStore/GetTimeSeriesPoints\"\n)\n\n// CacheStoreClient is the client API for CacheStore service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype CacheStoreClient interface {\n\tPing(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error)\n\tGet(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error)\n\tSet(ctx context.Context, in *SetRequest, opts ...grpc.CallOption) (*SetResponse, error)\n\tDelete(ctx context.Context, in *DeleteRequest, opts ...grpc.CallOption) (*DeleteResponse, error)\n\tPush(ctx context.Context, in *PushRequest, opts ...grpc.CallOption) (*PushResponse, error)\n\tPop(ctx context.Context, in *PopRequest, opts ...grpc.CallOption) (*PopResponse, error)\n\tRemain(ctx context.Context, in *RemainRequest, opts ...grpc.CallOption) (*RemainResponse, error)\n\tAddScores(ctx context.Context, in *AddScoresRequest, opts ...grpc.CallOption) (*AddScoresResponse, error)\n\tSearchScores(ctx context.Context, in *SearchScoresRequest, opts ...grpc.CallOption) (*SearchScoresResponse, error)\n\tDeleteScores(ctx context.Context, in *DeleteScoresRequest, opts ...grpc.CallOption) (*DeleteScoresResponse, error)\n\tUpdateScores(ctx context.Context, in *UpdateScoresRequest, opts ...grpc.CallOption) (*UpdateScoresResponse, error)\n\tScanScores(ctx context.Context, in *ScanScoresRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ScanScoresResponse], error)\n\tAddTimeSeriesPoints(ctx context.Context, in *AddTimeSeriesPointsRequest, opts ...grpc.CallOption) (*AddTimeSeriesPointsResponse, error)\n\tGetTimeSeriesPoints(ctx context.Context, in *GetTimeSeriesPointsRequest, opts ...grpc.CallOption) (*GetTimeSeriesPointsResponse, error)\n}\n\ntype cacheStoreClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewCacheStoreClient(cc grpc.ClientConnInterface) CacheStoreClient {\n\treturn &cacheStoreClient{cc}\n}\n\nfunc (c *cacheStoreClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(PingResponse)\n\terr := c.cc.Invoke(ctx, CacheStore_Ping_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *cacheStoreClient) Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(GetResponse)\n\terr := c.cc.Invoke(ctx, CacheStore_Get_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *cacheStoreClient) Set(ctx context.Context, in *SetRequest, opts ...grpc.CallOption) (*SetResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(SetResponse)\n\terr := c.cc.Invoke(ctx, CacheStore_Set_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *cacheStoreClient) Delete(ctx context.Context, in *DeleteRequest, opts ...grpc.CallOption) (*DeleteResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(DeleteResponse)\n\terr := c.cc.Invoke(ctx, CacheStore_Delete_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *cacheStoreClient) Push(ctx context.Context, in *PushRequest, opts ...grpc.CallOption) (*PushResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(PushResponse)\n\terr := c.cc.Invoke(ctx, CacheStore_Push_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *cacheStoreClient) Pop(ctx context.Context, in *PopRequest, opts ...grpc.CallOption) (*PopResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(PopResponse)\n\terr := c.cc.Invoke(ctx, CacheStore_Pop_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *cacheStoreClient) Remain(ctx context.Context, in *RemainRequest, opts ...grpc.CallOption) (*RemainResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(RemainResponse)\n\terr := c.cc.Invoke(ctx, CacheStore_Remain_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *cacheStoreClient) AddScores(ctx context.Context, in *AddScoresRequest, opts ...grpc.CallOption) (*AddScoresResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(AddScoresResponse)\n\terr := c.cc.Invoke(ctx, CacheStore_AddScores_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *cacheStoreClient) SearchScores(ctx context.Context, in *SearchScoresRequest, opts ...grpc.CallOption) (*SearchScoresResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(SearchScoresResponse)\n\terr := c.cc.Invoke(ctx, CacheStore_SearchScores_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *cacheStoreClient) DeleteScores(ctx context.Context, in *DeleteScoresRequest, opts ...grpc.CallOption) (*DeleteScoresResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(DeleteScoresResponse)\n\terr := c.cc.Invoke(ctx, CacheStore_DeleteScores_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *cacheStoreClient) UpdateScores(ctx context.Context, in *UpdateScoresRequest, opts ...grpc.CallOption) (*UpdateScoresResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(UpdateScoresResponse)\n\terr := c.cc.Invoke(ctx, CacheStore_UpdateScores_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *cacheStoreClient) ScanScores(ctx context.Context, in *ScanScoresRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ScanScoresResponse], error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tstream, err := c.cc.NewStream(ctx, &CacheStore_ServiceDesc.Streams[0], CacheStore_ScanScores_FullMethodName, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &grpc.GenericClientStream[ScanScoresRequest, ScanScoresResponse]{ClientStream: stream}\n\tif err := x.ClientStream.SendMsg(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := x.ClientStream.CloseSend(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn x, nil\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype CacheStore_ScanScoresClient = grpc.ServerStreamingClient[ScanScoresResponse]\n\nfunc (c *cacheStoreClient) AddTimeSeriesPoints(ctx context.Context, in *AddTimeSeriesPointsRequest, opts ...grpc.CallOption) (*AddTimeSeriesPointsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(AddTimeSeriesPointsResponse)\n\terr := c.cc.Invoke(ctx, CacheStore_AddTimeSeriesPoints_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *cacheStoreClient) GetTimeSeriesPoints(ctx context.Context, in *GetTimeSeriesPointsRequest, opts ...grpc.CallOption) (*GetTimeSeriesPointsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(GetTimeSeriesPointsResponse)\n\terr := c.cc.Invoke(ctx, CacheStore_GetTimeSeriesPoints_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// CacheStoreServer is the server API for CacheStore service.\n// All implementations must embed UnimplementedCacheStoreServer\n// for forward compatibility.\ntype CacheStoreServer interface {\n\tPing(context.Context, *PingRequest) (*PingResponse, error)\n\tGet(context.Context, *GetRequest) (*GetResponse, error)\n\tSet(context.Context, *SetRequest) (*SetResponse, error)\n\tDelete(context.Context, *DeleteRequest) (*DeleteResponse, error)\n\tPush(context.Context, *PushRequest) (*PushResponse, error)\n\tPop(context.Context, *PopRequest) (*PopResponse, error)\n\tRemain(context.Context, *RemainRequest) (*RemainResponse, error)\n\tAddScores(context.Context, *AddScoresRequest) (*AddScoresResponse, error)\n\tSearchScores(context.Context, *SearchScoresRequest) (*SearchScoresResponse, error)\n\tDeleteScores(context.Context, *DeleteScoresRequest) (*DeleteScoresResponse, error)\n\tUpdateScores(context.Context, *UpdateScoresRequest) (*UpdateScoresResponse, error)\n\tScanScores(*ScanScoresRequest, grpc.ServerStreamingServer[ScanScoresResponse]) error\n\tAddTimeSeriesPoints(context.Context, *AddTimeSeriesPointsRequest) (*AddTimeSeriesPointsResponse, error)\n\tGetTimeSeriesPoints(context.Context, *GetTimeSeriesPointsRequest) (*GetTimeSeriesPointsResponse, error)\n\tmustEmbedUnimplementedCacheStoreServer()\n}\n\n// UnimplementedCacheStoreServer must be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedCacheStoreServer struct{}\n\nfunc (UnimplementedCacheStoreServer) Ping(context.Context, *PingRequest) (*PingResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method Ping not implemented\")\n}\nfunc (UnimplementedCacheStoreServer) Get(context.Context, *GetRequest) (*GetResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method Get not implemented\")\n}\nfunc (UnimplementedCacheStoreServer) Set(context.Context, *SetRequest) (*SetResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method Set not implemented\")\n}\nfunc (UnimplementedCacheStoreServer) Delete(context.Context, *DeleteRequest) (*DeleteResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method Delete not implemented\")\n}\nfunc (UnimplementedCacheStoreServer) Push(context.Context, *PushRequest) (*PushResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method Push not implemented\")\n}\nfunc (UnimplementedCacheStoreServer) Pop(context.Context, *PopRequest) (*PopResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method Pop not implemented\")\n}\nfunc (UnimplementedCacheStoreServer) Remain(context.Context, *RemainRequest) (*RemainResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method Remain not implemented\")\n}\nfunc (UnimplementedCacheStoreServer) AddScores(context.Context, *AddScoresRequest) (*AddScoresResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method AddScores not implemented\")\n}\nfunc (UnimplementedCacheStoreServer) SearchScores(context.Context, *SearchScoresRequest) (*SearchScoresResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method SearchScores not implemented\")\n}\nfunc (UnimplementedCacheStoreServer) DeleteScores(context.Context, *DeleteScoresRequest) (*DeleteScoresResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method DeleteScores not implemented\")\n}\nfunc (UnimplementedCacheStoreServer) UpdateScores(context.Context, *UpdateScoresRequest) (*UpdateScoresResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method UpdateScores not implemented\")\n}\nfunc (UnimplementedCacheStoreServer) ScanScores(*ScanScoresRequest, grpc.ServerStreamingServer[ScanScoresResponse]) error {\n\treturn status.Error(codes.Unimplemented, \"method ScanScores not implemented\")\n}\nfunc (UnimplementedCacheStoreServer) AddTimeSeriesPoints(context.Context, *AddTimeSeriesPointsRequest) (*AddTimeSeriesPointsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method AddTimeSeriesPoints not implemented\")\n}\nfunc (UnimplementedCacheStoreServer) GetTimeSeriesPoints(context.Context, *GetTimeSeriesPointsRequest) (*GetTimeSeriesPointsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetTimeSeriesPoints not implemented\")\n}\nfunc (UnimplementedCacheStoreServer) mustEmbedUnimplementedCacheStoreServer() {}\nfunc (UnimplementedCacheStoreServer) testEmbeddedByValue()                    {}\n\n// UnsafeCacheStoreServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to CacheStoreServer will\n// result in compilation errors.\ntype UnsafeCacheStoreServer interface {\n\tmustEmbedUnimplementedCacheStoreServer()\n}\n\nfunc RegisterCacheStoreServer(s grpc.ServiceRegistrar, srv CacheStoreServer) {\n\t// If the following call panics, it indicates UnimplementedCacheStoreServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&CacheStore_ServiceDesc, srv)\n}\n\nfunc _CacheStore_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(PingRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(CacheStoreServer).Ping(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: CacheStore_Ping_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(CacheStoreServer).Ping(ctx, req.(*PingRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _CacheStore_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(CacheStoreServer).Get(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: CacheStore_Get_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(CacheStoreServer).Get(ctx, req.(*GetRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _CacheStore_Set_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(SetRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(CacheStoreServer).Set(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: CacheStore_Set_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(CacheStoreServer).Set(ctx, req.(*SetRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _CacheStore_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DeleteRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(CacheStoreServer).Delete(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: CacheStore_Delete_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(CacheStoreServer).Delete(ctx, req.(*DeleteRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _CacheStore_Push_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(PushRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(CacheStoreServer).Push(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: CacheStore_Push_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(CacheStoreServer).Push(ctx, req.(*PushRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _CacheStore_Pop_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(PopRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(CacheStoreServer).Pop(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: CacheStore_Pop_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(CacheStoreServer).Pop(ctx, req.(*PopRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _CacheStore_Remain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(RemainRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(CacheStoreServer).Remain(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: CacheStore_Remain_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(CacheStoreServer).Remain(ctx, req.(*RemainRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _CacheStore_AddScores_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(AddScoresRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(CacheStoreServer).AddScores(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: CacheStore_AddScores_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(CacheStoreServer).AddScores(ctx, req.(*AddScoresRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _CacheStore_SearchScores_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(SearchScoresRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(CacheStoreServer).SearchScores(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: CacheStore_SearchScores_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(CacheStoreServer).SearchScores(ctx, req.(*SearchScoresRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _CacheStore_DeleteScores_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DeleteScoresRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(CacheStoreServer).DeleteScores(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: CacheStore_DeleteScores_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(CacheStoreServer).DeleteScores(ctx, req.(*DeleteScoresRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _CacheStore_UpdateScores_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(UpdateScoresRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(CacheStoreServer).UpdateScores(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: CacheStore_UpdateScores_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(CacheStoreServer).UpdateScores(ctx, req.(*UpdateScoresRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _CacheStore_ScanScores_Handler(srv interface{}, stream grpc.ServerStream) error {\n\tm := new(ScanScoresRequest)\n\tif err := stream.RecvMsg(m); err != nil {\n\t\treturn err\n\t}\n\treturn srv.(CacheStoreServer).ScanScores(m, &grpc.GenericServerStream[ScanScoresRequest, ScanScoresResponse]{ServerStream: stream})\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype CacheStore_ScanScoresServer = grpc.ServerStreamingServer[ScanScoresResponse]\n\nfunc _CacheStore_AddTimeSeriesPoints_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(AddTimeSeriesPointsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(CacheStoreServer).AddTimeSeriesPoints(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: CacheStore_AddTimeSeriesPoints_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(CacheStoreServer).AddTimeSeriesPoints(ctx, req.(*AddTimeSeriesPointsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _CacheStore_GetTimeSeriesPoints_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetTimeSeriesPointsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(CacheStoreServer).GetTimeSeriesPoints(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: CacheStore_GetTimeSeriesPoints_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(CacheStoreServer).GetTimeSeriesPoints(ctx, req.(*GetTimeSeriesPointsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// CacheStore_ServiceDesc is the grpc.ServiceDesc for CacheStore service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar CacheStore_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"protocol.CacheStore\",\n\tHandlerType: (*CacheStoreServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"Ping\",\n\t\t\tHandler:    _CacheStore_Ping_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"Get\",\n\t\t\tHandler:    _CacheStore_Get_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"Set\",\n\t\t\tHandler:    _CacheStore_Set_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"Delete\",\n\t\t\tHandler:    _CacheStore_Delete_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"Push\",\n\t\t\tHandler:    _CacheStore_Push_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"Pop\",\n\t\t\tHandler:    _CacheStore_Pop_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"Remain\",\n\t\t\tHandler:    _CacheStore_Remain_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"AddScores\",\n\t\t\tHandler:    _CacheStore_AddScores_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"SearchScores\",\n\t\t\tHandler:    _CacheStore_SearchScores_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DeleteScores\",\n\t\t\tHandler:    _CacheStore_DeleteScores_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"UpdateScores\",\n\t\t\tHandler:    _CacheStore_UpdateScores_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"AddTimeSeriesPoints\",\n\t\t\tHandler:    _CacheStore_AddTimeSeriesPoints_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetTimeSeriesPoints\",\n\t\t\tHandler:    _CacheStore_GetTimeSeriesPoints_Handler,\n\t\t},\n\t},\n\tStreams: []grpc.StreamDesc{\n\t\t{\n\t\t\tStreamName:    \"ScanScores\",\n\t\t\tHandler:       _CacheStore_ScanScores_Handler,\n\t\t\tServerStreams: true,\n\t\t},\n\t},\n\tMetadata: \"cache_store.proto\",\n}\n"
  },
  {
    "path": "protocol/data_store.pb.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.10\n// \tprotoc        v6.33.1\n// source: data_store.proto\n\npackage protocol\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\ttimestamppb \"google.golang.org/protobuf/types/known/timestamppb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype ExpressionType int32\n\nconst (\n\tExpressionType_None           ExpressionType = 0\n\tExpressionType_Less           ExpressionType = 1\n\tExpressionType_LessOrEqual    ExpressionType = 2\n\tExpressionType_Greater        ExpressionType = 3\n\tExpressionType_GreaterOrEqual ExpressionType = 4\n)\n\n// Enum value maps for ExpressionType.\nvar (\n\tExpressionType_name = map[int32]string{\n\t\t0: \"None\",\n\t\t1: \"Less\",\n\t\t2: \"LessOrEqual\",\n\t\t3: \"Greater\",\n\t\t4: \"GreaterOrEqual\",\n\t}\n\tExpressionType_value = map[string]int32{\n\t\t\"None\":           0,\n\t\t\"Less\":           1,\n\t\t\"LessOrEqual\":    2,\n\t\t\"Greater\":        3,\n\t\t\"GreaterOrEqual\": 4,\n\t}\n)\n\nfunc (x ExpressionType) Enum() *ExpressionType {\n\tp := new(ExpressionType)\n\t*p = x\n\treturn p\n}\n\nfunc (x ExpressionType) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (ExpressionType) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_data_store_proto_enumTypes[0].Descriptor()\n}\n\nfunc (ExpressionType) Type() protoreflect.EnumType {\n\treturn &file_data_store_proto_enumTypes[0]\n}\n\nfunc (x ExpressionType) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use ExpressionType.Descriptor instead.\nfunc (ExpressionType) EnumDescriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{0}\n}\n\ntype FeedbackTypeExpression struct {\n\tstate          protoimpl.MessageState `protogen:\"open.v1\"`\n\tFeedbackType   string                 `protobuf:\"bytes,1,opt,name=feedback_type,json=feedbackType,proto3\" json:\"feedback_type,omitempty\"`\n\tExpressionType ExpressionType         `protobuf:\"varint,2,opt,name=expression_type,json=expressionType,proto3,enum=protocol.ExpressionType\" json:\"expression_type,omitempty\"`\n\tValue          float64                `protobuf:\"fixed64,3,opt,name=value,proto3\" json:\"value,omitempty\"`\n\tunknownFields  protoimpl.UnknownFields\n\tsizeCache      protoimpl.SizeCache\n}\n\nfunc (x *FeedbackTypeExpression) Reset() {\n\t*x = FeedbackTypeExpression{}\n\tmi := &file_data_store_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *FeedbackTypeExpression) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*FeedbackTypeExpression) ProtoMessage() {}\n\nfunc (x *FeedbackTypeExpression) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use FeedbackTypeExpression.ProtoReflect.Descriptor instead.\nfunc (*FeedbackTypeExpression) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *FeedbackTypeExpression) GetFeedbackType() string {\n\tif x != nil {\n\t\treturn x.FeedbackType\n\t}\n\treturn \"\"\n}\n\nfunc (x *FeedbackTypeExpression) GetExpressionType() ExpressionType {\n\tif x != nil {\n\t\treturn x.ExpressionType\n\t}\n\treturn ExpressionType_None\n}\n\nfunc (x *FeedbackTypeExpression) GetValue() float64 {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn 0\n}\n\ntype UserPatch struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tLabels        []byte                 `protobuf:\"bytes,1,opt,name=labels,proto3\" json:\"labels,omitempty\"`\n\tComment       *string                `protobuf:\"bytes,2,opt,name=comment,proto3,oneof\" json:\"comment,omitempty\"`\n\tSubscribe     []string               `protobuf:\"bytes,3,rep,name=subscribe,proto3\" json:\"subscribe,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UserPatch) Reset() {\n\t*x = UserPatch{}\n\tmi := &file_data_store_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UserPatch) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UserPatch) ProtoMessage() {}\n\nfunc (x *UserPatch) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UserPatch.ProtoReflect.Descriptor instead.\nfunc (*UserPatch) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *UserPatch) GetLabels() []byte {\n\tif x != nil {\n\t\treturn x.Labels\n\t}\n\treturn nil\n}\n\nfunc (x *UserPatch) GetComment() string {\n\tif x != nil && x.Comment != nil {\n\t\treturn *x.Comment\n\t}\n\treturn \"\"\n}\n\nfunc (x *UserPatch) GetSubscribe() []string {\n\tif x != nil {\n\t\treturn x.Subscribe\n\t}\n\treturn nil\n}\n\ntype ItemPatch struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tIsHidden      *bool                  `protobuf:\"varint,1,opt,name=is_hidden,json=isHidden,proto3,oneof\" json:\"is_hidden,omitempty\"`\n\tCategories    []string               `protobuf:\"bytes,2,rep,name=categories,proto3\" json:\"categories,omitempty\"`\n\tTimestamp     *timestamppb.Timestamp `protobuf:\"bytes,3,opt,name=timestamp,proto3,oneof\" json:\"timestamp,omitempty\"`\n\tLabels        []byte                 `protobuf:\"bytes,4,opt,name=labels,proto3\" json:\"labels,omitempty\"`\n\tComment       *string                `protobuf:\"bytes,5,opt,name=comment,proto3,oneof\" json:\"comment,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ItemPatch) Reset() {\n\t*x = ItemPatch{}\n\tmi := &file_data_store_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ItemPatch) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ItemPatch) ProtoMessage() {}\n\nfunc (x *ItemPatch) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ItemPatch.ProtoReflect.Descriptor instead.\nfunc (*ItemPatch) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *ItemPatch) GetIsHidden() bool {\n\tif x != nil && x.IsHidden != nil {\n\t\treturn *x.IsHidden\n\t}\n\treturn false\n}\n\nfunc (x *ItemPatch) GetCategories() []string {\n\tif x != nil {\n\t\treturn x.Categories\n\t}\n\treturn nil\n}\n\nfunc (x *ItemPatch) GetTimestamp() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.Timestamp\n\t}\n\treturn nil\n}\n\nfunc (x *ItemPatch) GetLabels() []byte {\n\tif x != nil {\n\t\treturn x.Labels\n\t}\n\treturn nil\n}\n\nfunc (x *ItemPatch) GetComment() string {\n\tif x != nil && x.Comment != nil {\n\t\treturn *x.Comment\n\t}\n\treturn \"\"\n}\n\ntype ScanOptions struct {\n\tstate         protoimpl.MessageState    `protogen:\"open.v1\"`\n\tBeginUserId   *string                   `protobuf:\"bytes,1,opt,name=begin_user_id,json=beginUserId,proto3,oneof\" json:\"begin_user_id,omitempty\"`\n\tEndUserId     *string                   `protobuf:\"bytes,2,opt,name=end_user_id,json=endUserId,proto3,oneof\" json:\"end_user_id,omitempty\"`\n\tBeginItemId   *string                   `protobuf:\"bytes,3,opt,name=begin_item_id,json=beginItemId,proto3,oneof\" json:\"begin_item_id,omitempty\"`\n\tEndItemId     *string                   `protobuf:\"bytes,4,opt,name=end_item_id,json=endItemId,proto3,oneof\" json:\"end_item_id,omitempty\"`\n\tBeginTime     *timestamppb.Timestamp    `protobuf:\"bytes,5,opt,name=begin_time,json=beginTime,proto3,oneof\" json:\"begin_time,omitempty\"`\n\tEndTime       *timestamppb.Timestamp    `protobuf:\"bytes,6,opt,name=end_time,json=endTime,proto3,oneof\" json:\"end_time,omitempty\"`\n\tFeedbackTypes []*FeedbackTypeExpression `protobuf:\"bytes,7,rep,name=feedback_types,json=feedbackTypes,proto3\" json:\"feedback_types,omitempty\"`\n\tOrderByItemId bool                      `protobuf:\"varint,8,opt,name=order_by_item_id,json=orderByItemId,proto3\" json:\"order_by_item_id,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ScanOptions) Reset() {\n\t*x = ScanOptions{}\n\tmi := &file_data_store_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ScanOptions) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ScanOptions) ProtoMessage() {}\n\nfunc (x *ScanOptions) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ScanOptions.ProtoReflect.Descriptor instead.\nfunc (*ScanOptions) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *ScanOptions) GetBeginUserId() string {\n\tif x != nil && x.BeginUserId != nil {\n\t\treturn *x.BeginUserId\n\t}\n\treturn \"\"\n}\n\nfunc (x *ScanOptions) GetEndUserId() string {\n\tif x != nil && x.EndUserId != nil {\n\t\treturn *x.EndUserId\n\t}\n\treturn \"\"\n}\n\nfunc (x *ScanOptions) GetBeginItemId() string {\n\tif x != nil && x.BeginItemId != nil {\n\t\treturn *x.BeginItemId\n\t}\n\treturn \"\"\n}\n\nfunc (x *ScanOptions) GetEndItemId() string {\n\tif x != nil && x.EndItemId != nil {\n\t\treturn *x.EndItemId\n\t}\n\treturn \"\"\n}\n\nfunc (x *ScanOptions) GetBeginTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.BeginTime\n\t}\n\treturn nil\n}\n\nfunc (x *ScanOptions) GetEndTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.EndTime\n\t}\n\treturn nil\n}\n\nfunc (x *ScanOptions) GetFeedbackTypes() []*FeedbackTypeExpression {\n\tif x != nil {\n\t\treturn x.FeedbackTypes\n\t}\n\treturn nil\n}\n\nfunc (x *ScanOptions) GetOrderByItemId() bool {\n\tif x != nil {\n\t\treturn x.OrderByItemId\n\t}\n\treturn false\n}\n\ntype BatchInsertItemsRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tItems         []*Item                `protobuf:\"bytes,1,rep,name=items,proto3\" json:\"items,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BatchInsertItemsRequest) Reset() {\n\t*x = BatchInsertItemsRequest{}\n\tmi := &file_data_store_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BatchInsertItemsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BatchInsertItemsRequest) ProtoMessage() {}\n\nfunc (x *BatchInsertItemsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BatchInsertItemsRequest.ProtoReflect.Descriptor instead.\nfunc (*BatchInsertItemsRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *BatchInsertItemsRequest) GetItems() []*Item {\n\tif x != nil {\n\t\treturn x.Items\n\t}\n\treturn nil\n}\n\ntype BatchInsertItemsResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BatchInsertItemsResponse) Reset() {\n\t*x = BatchInsertItemsResponse{}\n\tmi := &file_data_store_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BatchInsertItemsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BatchInsertItemsResponse) ProtoMessage() {}\n\nfunc (x *BatchInsertItemsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BatchInsertItemsResponse.ProtoReflect.Descriptor instead.\nfunc (*BatchInsertItemsResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{5}\n}\n\ntype BatchGetItemsRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tItemIds       []string               `protobuf:\"bytes,1,rep,name=item_ids,json=itemIds,proto3\" json:\"item_ids,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BatchGetItemsRequest) Reset() {\n\t*x = BatchGetItemsRequest{}\n\tmi := &file_data_store_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BatchGetItemsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BatchGetItemsRequest) ProtoMessage() {}\n\nfunc (x *BatchGetItemsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BatchGetItemsRequest.ProtoReflect.Descriptor instead.\nfunc (*BatchGetItemsRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *BatchGetItemsRequest) GetItemIds() []string {\n\tif x != nil {\n\t\treturn x.ItemIds\n\t}\n\treturn nil\n}\n\ntype BatchGetItemsResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tItems         []*Item                `protobuf:\"bytes,1,rep,name=items,proto3\" json:\"items,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BatchGetItemsResponse) Reset() {\n\t*x = BatchGetItemsResponse{}\n\tmi := &file_data_store_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BatchGetItemsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BatchGetItemsResponse) ProtoMessage() {}\n\nfunc (x *BatchGetItemsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BatchGetItemsResponse.ProtoReflect.Descriptor instead.\nfunc (*BatchGetItemsResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{7}\n}\n\nfunc (x *BatchGetItemsResponse) GetItems() []*Item {\n\tif x != nil {\n\t\treturn x.Items\n\t}\n\treturn nil\n}\n\ntype DeleteItemRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tItemId        string                 `protobuf:\"bytes,1,opt,name=item_id,json=itemId,proto3\" json:\"item_id,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteItemRequest) Reset() {\n\t*x = DeleteItemRequest{}\n\tmi := &file_data_store_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteItemRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteItemRequest) ProtoMessage() {}\n\nfunc (x *DeleteItemRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteItemRequest.ProtoReflect.Descriptor instead.\nfunc (*DeleteItemRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{8}\n}\n\nfunc (x *DeleteItemRequest) GetItemId() string {\n\tif x != nil {\n\t\treturn x.ItemId\n\t}\n\treturn \"\"\n}\n\ntype DeleteItemResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteItemResponse) Reset() {\n\t*x = DeleteItemResponse{}\n\tmi := &file_data_store_proto_msgTypes[9]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteItemResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteItemResponse) ProtoMessage() {}\n\nfunc (x *DeleteItemResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[9]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteItemResponse.ProtoReflect.Descriptor instead.\nfunc (*DeleteItemResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{9}\n}\n\ntype GetItemRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tItemId        string                 `protobuf:\"bytes,1,opt,name=item_id,json=itemId,proto3\" json:\"item_id,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetItemRequest) Reset() {\n\t*x = GetItemRequest{}\n\tmi := &file_data_store_proto_msgTypes[10]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetItemRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetItemRequest) ProtoMessage() {}\n\nfunc (x *GetItemRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[10]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetItemRequest.ProtoReflect.Descriptor instead.\nfunc (*GetItemRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{10}\n}\n\nfunc (x *GetItemRequest) GetItemId() string {\n\tif x != nil {\n\t\treturn x.ItemId\n\t}\n\treturn \"\"\n}\n\ntype GetItemResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tItem          *Item                  `protobuf:\"bytes,1,opt,name=item,proto3,oneof\" json:\"item,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetItemResponse) Reset() {\n\t*x = GetItemResponse{}\n\tmi := &file_data_store_proto_msgTypes[11]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetItemResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetItemResponse) ProtoMessage() {}\n\nfunc (x *GetItemResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[11]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetItemResponse.ProtoReflect.Descriptor instead.\nfunc (*GetItemResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{11}\n}\n\nfunc (x *GetItemResponse) GetItem() *Item {\n\tif x != nil {\n\t\treturn x.Item\n\t}\n\treturn nil\n}\n\ntype ModifyItemRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tItemId        string                 `protobuf:\"bytes,1,opt,name=item_id,json=itemId,proto3\" json:\"item_id,omitempty\"`\n\tPatch         *ItemPatch             `protobuf:\"bytes,2,opt,name=patch,proto3\" json:\"patch,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ModifyItemRequest) Reset() {\n\t*x = ModifyItemRequest{}\n\tmi := &file_data_store_proto_msgTypes[12]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ModifyItemRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ModifyItemRequest) ProtoMessage() {}\n\nfunc (x *ModifyItemRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[12]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ModifyItemRequest.ProtoReflect.Descriptor instead.\nfunc (*ModifyItemRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{12}\n}\n\nfunc (x *ModifyItemRequest) GetItemId() string {\n\tif x != nil {\n\t\treturn x.ItemId\n\t}\n\treturn \"\"\n}\n\nfunc (x *ModifyItemRequest) GetPatch() *ItemPatch {\n\tif x != nil {\n\t\treturn x.Patch\n\t}\n\treturn nil\n}\n\ntype ModifyItemResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ModifyItemResponse) Reset() {\n\t*x = ModifyItemResponse{}\n\tmi := &file_data_store_proto_msgTypes[13]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ModifyItemResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ModifyItemResponse) ProtoMessage() {}\n\nfunc (x *ModifyItemResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[13]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ModifyItemResponse.ProtoReflect.Descriptor instead.\nfunc (*ModifyItemResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{13}\n}\n\ntype GetItemsRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCursor        string                 `protobuf:\"bytes,1,opt,name=cursor,proto3\" json:\"cursor,omitempty\"`\n\tN             int32                  `protobuf:\"varint,2,opt,name=n,proto3\" json:\"n,omitempty\"`\n\tBeginTime     *timestamppb.Timestamp `protobuf:\"bytes,3,opt,name=begin_time,json=beginTime,proto3\" json:\"begin_time,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetItemsRequest) Reset() {\n\t*x = GetItemsRequest{}\n\tmi := &file_data_store_proto_msgTypes[14]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetItemsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetItemsRequest) ProtoMessage() {}\n\nfunc (x *GetItemsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[14]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetItemsRequest.ProtoReflect.Descriptor instead.\nfunc (*GetItemsRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{14}\n}\n\nfunc (x *GetItemsRequest) GetCursor() string {\n\tif x != nil {\n\t\treturn x.Cursor\n\t}\n\treturn \"\"\n}\n\nfunc (x *GetItemsRequest) GetN() int32 {\n\tif x != nil {\n\t\treturn x.N\n\t}\n\treturn 0\n}\n\nfunc (x *GetItemsRequest) GetBeginTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.BeginTime\n\t}\n\treturn nil\n}\n\ntype GetItemsResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCursor        string                 `protobuf:\"bytes,1,opt,name=cursor,proto3\" json:\"cursor,omitempty\"`\n\tItems         []*Item                `protobuf:\"bytes,2,rep,name=items,proto3\" json:\"items,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetItemsResponse) Reset() {\n\t*x = GetItemsResponse{}\n\tmi := &file_data_store_proto_msgTypes[15]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetItemsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetItemsResponse) ProtoMessage() {}\n\nfunc (x *GetItemsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[15]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetItemsResponse.ProtoReflect.Descriptor instead.\nfunc (*GetItemsResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{15}\n}\n\nfunc (x *GetItemsResponse) GetCursor() string {\n\tif x != nil {\n\t\treturn x.Cursor\n\t}\n\treturn \"\"\n}\n\nfunc (x *GetItemsResponse) GetItems() []*Item {\n\tif x != nil {\n\t\treturn x.Items\n\t}\n\treturn nil\n}\n\ntype GetItemFeedbackRequest struct {\n\tstate         protoimpl.MessageState    `protogen:\"open.v1\"`\n\tItemId        string                    `protobuf:\"bytes,1,opt,name=item_id,json=itemId,proto3\" json:\"item_id,omitempty\"`\n\tFeedbackTypes []*FeedbackTypeExpression `protobuf:\"bytes,2,rep,name=feedback_types,json=feedbackTypes,proto3\" json:\"feedback_types,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetItemFeedbackRequest) Reset() {\n\t*x = GetItemFeedbackRequest{}\n\tmi := &file_data_store_proto_msgTypes[16]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetItemFeedbackRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetItemFeedbackRequest) ProtoMessage() {}\n\nfunc (x *GetItemFeedbackRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[16]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetItemFeedbackRequest.ProtoReflect.Descriptor instead.\nfunc (*GetItemFeedbackRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{16}\n}\n\nfunc (x *GetItemFeedbackRequest) GetItemId() string {\n\tif x != nil {\n\t\treturn x.ItemId\n\t}\n\treturn \"\"\n}\n\nfunc (x *GetItemFeedbackRequest) GetFeedbackTypes() []*FeedbackTypeExpression {\n\tif x != nil {\n\t\treturn x.FeedbackTypes\n\t}\n\treturn nil\n}\n\ntype BatchInsertUsersRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tUsers         []*User                `protobuf:\"bytes,1,rep,name=users,proto3\" json:\"users,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BatchInsertUsersRequest) Reset() {\n\t*x = BatchInsertUsersRequest{}\n\tmi := &file_data_store_proto_msgTypes[17]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BatchInsertUsersRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BatchInsertUsersRequest) ProtoMessage() {}\n\nfunc (x *BatchInsertUsersRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[17]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BatchInsertUsersRequest.ProtoReflect.Descriptor instead.\nfunc (*BatchInsertUsersRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{17}\n}\n\nfunc (x *BatchInsertUsersRequest) GetUsers() []*User {\n\tif x != nil {\n\t\treturn x.Users\n\t}\n\treturn nil\n}\n\ntype BatchInsertUsersResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BatchInsertUsersResponse) Reset() {\n\t*x = BatchInsertUsersResponse{}\n\tmi := &file_data_store_proto_msgTypes[18]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BatchInsertUsersResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BatchInsertUsersResponse) ProtoMessage() {}\n\nfunc (x *BatchInsertUsersResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[18]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BatchInsertUsersResponse.ProtoReflect.Descriptor instead.\nfunc (*BatchInsertUsersResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{18}\n}\n\ntype DeleteUserRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tUserId        string                 `protobuf:\"bytes,1,opt,name=user_id,json=userId,proto3\" json:\"user_id,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteUserRequest) Reset() {\n\t*x = DeleteUserRequest{}\n\tmi := &file_data_store_proto_msgTypes[19]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteUserRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteUserRequest) ProtoMessage() {}\n\nfunc (x *DeleteUserRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[19]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteUserRequest.ProtoReflect.Descriptor instead.\nfunc (*DeleteUserRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{19}\n}\n\nfunc (x *DeleteUserRequest) GetUserId() string {\n\tif x != nil {\n\t\treturn x.UserId\n\t}\n\treturn \"\"\n}\n\ntype DeleteUserResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteUserResponse) Reset() {\n\t*x = DeleteUserResponse{}\n\tmi := &file_data_store_proto_msgTypes[20]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteUserResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteUserResponse) ProtoMessage() {}\n\nfunc (x *DeleteUserResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[20]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteUserResponse.ProtoReflect.Descriptor instead.\nfunc (*DeleteUserResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{20}\n}\n\ntype GetUserRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tUserId        string                 `protobuf:\"bytes,1,opt,name=user_id,json=userId,proto3\" json:\"user_id,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetUserRequest) Reset() {\n\t*x = GetUserRequest{}\n\tmi := &file_data_store_proto_msgTypes[21]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetUserRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetUserRequest) ProtoMessage() {}\n\nfunc (x *GetUserRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[21]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetUserRequest.ProtoReflect.Descriptor instead.\nfunc (*GetUserRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{21}\n}\n\nfunc (x *GetUserRequest) GetUserId() string {\n\tif x != nil {\n\t\treturn x.UserId\n\t}\n\treturn \"\"\n}\n\ntype GetUserResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tUser          *User                  `protobuf:\"bytes,1,opt,name=user,proto3,oneof\" json:\"user,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetUserResponse) Reset() {\n\t*x = GetUserResponse{}\n\tmi := &file_data_store_proto_msgTypes[22]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetUserResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetUserResponse) ProtoMessage() {}\n\nfunc (x *GetUserResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[22]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetUserResponse.ProtoReflect.Descriptor instead.\nfunc (*GetUserResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{22}\n}\n\nfunc (x *GetUserResponse) GetUser() *User {\n\tif x != nil {\n\t\treturn x.User\n\t}\n\treturn nil\n}\n\ntype ModifyUserRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tUserId        string                 `protobuf:\"bytes,1,opt,name=user_id,json=userId,proto3\" json:\"user_id,omitempty\"`\n\tPatch         *UserPatch             `protobuf:\"bytes,2,opt,name=patch,proto3\" json:\"patch,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ModifyUserRequest) Reset() {\n\t*x = ModifyUserRequest{}\n\tmi := &file_data_store_proto_msgTypes[23]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ModifyUserRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ModifyUserRequest) ProtoMessage() {}\n\nfunc (x *ModifyUserRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[23]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ModifyUserRequest.ProtoReflect.Descriptor instead.\nfunc (*ModifyUserRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{23}\n}\n\nfunc (x *ModifyUserRequest) GetUserId() string {\n\tif x != nil {\n\t\treturn x.UserId\n\t}\n\treturn \"\"\n}\n\nfunc (x *ModifyUserRequest) GetPatch() *UserPatch {\n\tif x != nil {\n\t\treturn x.Patch\n\t}\n\treturn nil\n}\n\ntype ModifyUserResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ModifyUserResponse) Reset() {\n\t*x = ModifyUserResponse{}\n\tmi := &file_data_store_proto_msgTypes[24]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ModifyUserResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ModifyUserResponse) ProtoMessage() {}\n\nfunc (x *ModifyUserResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[24]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ModifyUserResponse.ProtoReflect.Descriptor instead.\nfunc (*ModifyUserResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{24}\n}\n\ntype GetUsersRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCursor        string                 `protobuf:\"bytes,1,opt,name=cursor,proto3\" json:\"cursor,omitempty\"`\n\tN             int32                  `protobuf:\"varint,2,opt,name=n,proto3\" json:\"n,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetUsersRequest) Reset() {\n\t*x = GetUsersRequest{}\n\tmi := &file_data_store_proto_msgTypes[25]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetUsersRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetUsersRequest) ProtoMessage() {}\n\nfunc (x *GetUsersRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[25]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetUsersRequest.ProtoReflect.Descriptor instead.\nfunc (*GetUsersRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{25}\n}\n\nfunc (x *GetUsersRequest) GetCursor() string {\n\tif x != nil {\n\t\treturn x.Cursor\n\t}\n\treturn \"\"\n}\n\nfunc (x *GetUsersRequest) GetN() int32 {\n\tif x != nil {\n\t\treturn x.N\n\t}\n\treturn 0\n}\n\ntype GetUsersResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCursor        string                 `protobuf:\"bytes,1,opt,name=cursor,proto3\" json:\"cursor,omitempty\"`\n\tUsers         []*User                `protobuf:\"bytes,2,rep,name=users,proto3\" json:\"users,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetUsersResponse) Reset() {\n\t*x = GetUsersResponse{}\n\tmi := &file_data_store_proto_msgTypes[26]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetUsersResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetUsersResponse) ProtoMessage() {}\n\nfunc (x *GetUsersResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[26]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetUsersResponse.ProtoReflect.Descriptor instead.\nfunc (*GetUsersResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{26}\n}\n\nfunc (x *GetUsersResponse) GetCursor() string {\n\tif x != nil {\n\t\treturn x.Cursor\n\t}\n\treturn \"\"\n}\n\nfunc (x *GetUsersResponse) GetUsers() []*User {\n\tif x != nil {\n\t\treturn x.Users\n\t}\n\treturn nil\n}\n\ntype GetUserFeedbackRequest struct {\n\tstate         protoimpl.MessageState    `protogen:\"open.v1\"`\n\tUserId        string                    `protobuf:\"bytes,1,opt,name=user_id,json=userId,proto3\" json:\"user_id,omitempty\"`\n\tEndTime       *timestamppb.Timestamp    `protobuf:\"bytes,2,opt,name=end_time,json=endTime,proto3\" json:\"end_time,omitempty\"`\n\tFeedbackTypes []*FeedbackTypeExpression `protobuf:\"bytes,3,rep,name=feedback_types,json=feedbackTypes,proto3\" json:\"feedback_types,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetUserFeedbackRequest) Reset() {\n\t*x = GetUserFeedbackRequest{}\n\tmi := &file_data_store_proto_msgTypes[27]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetUserFeedbackRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetUserFeedbackRequest) ProtoMessage() {}\n\nfunc (x *GetUserFeedbackRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[27]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetUserFeedbackRequest.ProtoReflect.Descriptor instead.\nfunc (*GetUserFeedbackRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{27}\n}\n\nfunc (x *GetUserFeedbackRequest) GetUserId() string {\n\tif x != nil {\n\t\treturn x.UserId\n\t}\n\treturn \"\"\n}\n\nfunc (x *GetUserFeedbackRequest) GetEndTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.EndTime\n\t}\n\treturn nil\n}\n\nfunc (x *GetUserFeedbackRequest) GetFeedbackTypes() []*FeedbackTypeExpression {\n\tif x != nil {\n\t\treturn x.FeedbackTypes\n\t}\n\treturn nil\n}\n\ntype GetUserItemFeedbackRequest struct {\n\tstate         protoimpl.MessageState    `protogen:\"open.v1\"`\n\tUserId        string                    `protobuf:\"bytes,1,opt,name=user_id,json=userId,proto3\" json:\"user_id,omitempty\"`\n\tItemId        string                    `protobuf:\"bytes,2,opt,name=item_id,json=itemId,proto3\" json:\"item_id,omitempty\"`\n\tFeedbackTypes []*FeedbackTypeExpression `protobuf:\"bytes,3,rep,name=feedback_types,json=feedbackTypes,proto3\" json:\"feedback_types,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetUserItemFeedbackRequest) Reset() {\n\t*x = GetUserItemFeedbackRequest{}\n\tmi := &file_data_store_proto_msgTypes[28]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetUserItemFeedbackRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetUserItemFeedbackRequest) ProtoMessage() {}\n\nfunc (x *GetUserItemFeedbackRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[28]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetUserItemFeedbackRequest.ProtoReflect.Descriptor instead.\nfunc (*GetUserItemFeedbackRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{28}\n}\n\nfunc (x *GetUserItemFeedbackRequest) GetUserId() string {\n\tif x != nil {\n\t\treturn x.UserId\n\t}\n\treturn \"\"\n}\n\nfunc (x *GetUserItemFeedbackRequest) GetItemId() string {\n\tif x != nil {\n\t\treturn x.ItemId\n\t}\n\treturn \"\"\n}\n\nfunc (x *GetUserItemFeedbackRequest) GetFeedbackTypes() []*FeedbackTypeExpression {\n\tif x != nil {\n\t\treturn x.FeedbackTypes\n\t}\n\treturn nil\n}\n\ntype DeleteUserItemFeedbackRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tUserId        string                 `protobuf:\"bytes,1,opt,name=user_id,json=userId,proto3\" json:\"user_id,omitempty\"`\n\tItemId        string                 `protobuf:\"bytes,2,opt,name=item_id,json=itemId,proto3\" json:\"item_id,omitempty\"`\n\tFeedbackTypes []string               `protobuf:\"bytes,3,rep,name=feedback_types,json=feedbackTypes,proto3\" json:\"feedback_types,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteUserItemFeedbackRequest) Reset() {\n\t*x = DeleteUserItemFeedbackRequest{}\n\tmi := &file_data_store_proto_msgTypes[29]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteUserItemFeedbackRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteUserItemFeedbackRequest) ProtoMessage() {}\n\nfunc (x *DeleteUserItemFeedbackRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[29]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteUserItemFeedbackRequest.ProtoReflect.Descriptor instead.\nfunc (*DeleteUserItemFeedbackRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{29}\n}\n\nfunc (x *DeleteUserItemFeedbackRequest) GetUserId() string {\n\tif x != nil {\n\t\treturn x.UserId\n\t}\n\treturn \"\"\n}\n\nfunc (x *DeleteUserItemFeedbackRequest) GetItemId() string {\n\tif x != nil {\n\t\treturn x.ItemId\n\t}\n\treturn \"\"\n}\n\nfunc (x *DeleteUserItemFeedbackRequest) GetFeedbackTypes() []string {\n\tif x != nil {\n\t\treturn x.FeedbackTypes\n\t}\n\treturn nil\n}\n\ntype DeleteUserItemFeedbackResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCount         int32                  `protobuf:\"varint,1,opt,name=count,proto3\" json:\"count,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteUserItemFeedbackResponse) Reset() {\n\t*x = DeleteUserItemFeedbackResponse{}\n\tmi := &file_data_store_proto_msgTypes[30]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteUserItemFeedbackResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteUserItemFeedbackResponse) ProtoMessage() {}\n\nfunc (x *DeleteUserItemFeedbackResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[30]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteUserItemFeedbackResponse.ProtoReflect.Descriptor instead.\nfunc (*DeleteUserItemFeedbackResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{30}\n}\n\nfunc (x *DeleteUserItemFeedbackResponse) GetCount() int32 {\n\tif x != nil {\n\t\treturn x.Count\n\t}\n\treturn 0\n}\n\ntype BatchInsertFeedbackRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tFeedback      []*Feedback            `protobuf:\"bytes,1,rep,name=feedback,proto3\" json:\"feedback,omitempty\"`\n\tInsertUser    bool                   `protobuf:\"varint,2,opt,name=insert_user,json=insertUser,proto3\" json:\"insert_user,omitempty\"`\n\tInsertItem    bool                   `protobuf:\"varint,3,opt,name=insert_item,json=insertItem,proto3\" json:\"insert_item,omitempty\"`\n\tOverwrite     bool                   `protobuf:\"varint,4,opt,name=overwrite,proto3\" json:\"overwrite,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BatchInsertFeedbackRequest) Reset() {\n\t*x = BatchInsertFeedbackRequest{}\n\tmi := &file_data_store_proto_msgTypes[31]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BatchInsertFeedbackRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BatchInsertFeedbackRequest) ProtoMessage() {}\n\nfunc (x *BatchInsertFeedbackRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[31]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BatchInsertFeedbackRequest.ProtoReflect.Descriptor instead.\nfunc (*BatchInsertFeedbackRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{31}\n}\n\nfunc (x *BatchInsertFeedbackRequest) GetFeedback() []*Feedback {\n\tif x != nil {\n\t\treturn x.Feedback\n\t}\n\treturn nil\n}\n\nfunc (x *BatchInsertFeedbackRequest) GetInsertUser() bool {\n\tif x != nil {\n\t\treturn x.InsertUser\n\t}\n\treturn false\n}\n\nfunc (x *BatchInsertFeedbackRequest) GetInsertItem() bool {\n\tif x != nil {\n\t\treturn x.InsertItem\n\t}\n\treturn false\n}\n\nfunc (x *BatchInsertFeedbackRequest) GetOverwrite() bool {\n\tif x != nil {\n\t\treturn x.Overwrite\n\t}\n\treturn false\n}\n\ntype BatchInsertFeedbackResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BatchInsertFeedbackResponse) Reset() {\n\t*x = BatchInsertFeedbackResponse{}\n\tmi := &file_data_store_proto_msgTypes[32]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BatchInsertFeedbackResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BatchInsertFeedbackResponse) ProtoMessage() {}\n\nfunc (x *BatchInsertFeedbackResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[32]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BatchInsertFeedbackResponse.ProtoReflect.Descriptor instead.\nfunc (*BatchInsertFeedbackResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{32}\n}\n\ntype GetFeedbackRequest struct {\n\tstate         protoimpl.MessageState    `protogen:\"open.v1\"`\n\tCursor        string                    `protobuf:\"bytes,1,opt,name=cursor,proto3\" json:\"cursor,omitempty\"`\n\tN             int32                     `protobuf:\"varint,2,opt,name=n,proto3\" json:\"n,omitempty\"`\n\tBeginTime     *timestamppb.Timestamp    `protobuf:\"bytes,3,opt,name=begin_time,json=beginTime,proto3\" json:\"begin_time,omitempty\"`\n\tEndTime       *timestamppb.Timestamp    `protobuf:\"bytes,4,opt,name=end_time,json=endTime,proto3\" json:\"end_time,omitempty\"`\n\tFeedbackTypes []*FeedbackTypeExpression `protobuf:\"bytes,5,rep,name=feedback_types,json=feedbackTypes,proto3\" json:\"feedback_types,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetFeedbackRequest) Reset() {\n\t*x = GetFeedbackRequest{}\n\tmi := &file_data_store_proto_msgTypes[33]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetFeedbackRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetFeedbackRequest) ProtoMessage() {}\n\nfunc (x *GetFeedbackRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[33]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetFeedbackRequest.ProtoReflect.Descriptor instead.\nfunc (*GetFeedbackRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{33}\n}\n\nfunc (x *GetFeedbackRequest) GetCursor() string {\n\tif x != nil {\n\t\treturn x.Cursor\n\t}\n\treturn \"\"\n}\n\nfunc (x *GetFeedbackRequest) GetN() int32 {\n\tif x != nil {\n\t\treturn x.N\n\t}\n\treturn 0\n}\n\nfunc (x *GetFeedbackRequest) GetBeginTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.BeginTime\n\t}\n\treturn nil\n}\n\nfunc (x *GetFeedbackRequest) GetEndTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.EndTime\n\t}\n\treturn nil\n}\n\nfunc (x *GetFeedbackRequest) GetFeedbackTypes() []*FeedbackTypeExpression {\n\tif x != nil {\n\t\treturn x.FeedbackTypes\n\t}\n\treturn nil\n}\n\ntype GetFeedbackResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCursor        string                 `protobuf:\"bytes,1,opt,name=cursor,proto3\" json:\"cursor,omitempty\"`\n\tFeedback      []*Feedback            `protobuf:\"bytes,2,rep,name=feedback,proto3\" json:\"feedback,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetFeedbackResponse) Reset() {\n\t*x = GetFeedbackResponse{}\n\tmi := &file_data_store_proto_msgTypes[34]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetFeedbackResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetFeedbackResponse) ProtoMessage() {}\n\nfunc (x *GetFeedbackResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[34]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetFeedbackResponse.ProtoReflect.Descriptor instead.\nfunc (*GetFeedbackResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{34}\n}\n\nfunc (x *GetFeedbackResponse) GetCursor() string {\n\tif x != nil {\n\t\treturn x.Cursor\n\t}\n\treturn \"\"\n}\n\nfunc (x *GetFeedbackResponse) GetFeedback() []*Feedback {\n\tif x != nil {\n\t\treturn x.Feedback\n\t}\n\treturn nil\n}\n\ntype GetUserStreamRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tBatchSize     int32                  `protobuf:\"varint,1,opt,name=batch_size,json=batchSize,proto3\" json:\"batch_size,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetUserStreamRequest) Reset() {\n\t*x = GetUserStreamRequest{}\n\tmi := &file_data_store_proto_msgTypes[35]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetUserStreamRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetUserStreamRequest) ProtoMessage() {}\n\nfunc (x *GetUserStreamRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[35]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetUserStreamRequest.ProtoReflect.Descriptor instead.\nfunc (*GetUserStreamRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{35}\n}\n\nfunc (x *GetUserStreamRequest) GetBatchSize() int32 {\n\tif x != nil {\n\t\treturn x.BatchSize\n\t}\n\treturn 0\n}\n\ntype GetUserStreamResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tUsers         []*User                `protobuf:\"bytes,1,rep,name=users,proto3\" json:\"users,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetUserStreamResponse) Reset() {\n\t*x = GetUserStreamResponse{}\n\tmi := &file_data_store_proto_msgTypes[36]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetUserStreamResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetUserStreamResponse) ProtoMessage() {}\n\nfunc (x *GetUserStreamResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[36]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetUserStreamResponse.ProtoReflect.Descriptor instead.\nfunc (*GetUserStreamResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{36}\n}\n\nfunc (x *GetUserStreamResponse) GetUsers() []*User {\n\tif x != nil {\n\t\treturn x.Users\n\t}\n\treturn nil\n}\n\ntype GetItemStreamRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tBatchSize     int32                  `protobuf:\"varint,1,opt,name=batch_size,json=batchSize,proto3\" json:\"batch_size,omitempty\"`\n\tTimeLimit     *timestamppb.Timestamp `protobuf:\"bytes,2,opt,name=time_limit,json=timeLimit,proto3\" json:\"time_limit,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetItemStreamRequest) Reset() {\n\t*x = GetItemStreamRequest{}\n\tmi := &file_data_store_proto_msgTypes[37]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetItemStreamRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetItemStreamRequest) ProtoMessage() {}\n\nfunc (x *GetItemStreamRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[37]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetItemStreamRequest.ProtoReflect.Descriptor instead.\nfunc (*GetItemStreamRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{37}\n}\n\nfunc (x *GetItemStreamRequest) GetBatchSize() int32 {\n\tif x != nil {\n\t\treturn x.BatchSize\n\t}\n\treturn 0\n}\n\nfunc (x *GetItemStreamRequest) GetTimeLimit() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.TimeLimit\n\t}\n\treturn nil\n}\n\ntype GetItemStreamResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tItems         []*Item                `protobuf:\"bytes,1,rep,name=items,proto3\" json:\"items,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetItemStreamResponse) Reset() {\n\t*x = GetItemStreamResponse{}\n\tmi := &file_data_store_proto_msgTypes[38]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetItemStreamResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetItemStreamResponse) ProtoMessage() {}\n\nfunc (x *GetItemStreamResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[38]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetItemStreamResponse.ProtoReflect.Descriptor instead.\nfunc (*GetItemStreamResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{38}\n}\n\nfunc (x *GetItemStreamResponse) GetItems() []*Item {\n\tif x != nil {\n\t\treturn x.Items\n\t}\n\treturn nil\n}\n\ntype GetFeedbackStreamRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tBatchSize     int32                  `protobuf:\"varint,1,opt,name=batch_size,json=batchSize,proto3\" json:\"batch_size,omitempty\"`\n\tScanOptions   *ScanOptions           `protobuf:\"bytes,2,opt,name=scan_options,json=scanOptions,proto3\" json:\"scan_options,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetFeedbackStreamRequest) Reset() {\n\t*x = GetFeedbackStreamRequest{}\n\tmi := &file_data_store_proto_msgTypes[39]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetFeedbackStreamRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetFeedbackStreamRequest) ProtoMessage() {}\n\nfunc (x *GetFeedbackStreamRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[39]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetFeedbackStreamRequest.ProtoReflect.Descriptor instead.\nfunc (*GetFeedbackStreamRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{39}\n}\n\nfunc (x *GetFeedbackStreamRequest) GetBatchSize() int32 {\n\tif x != nil {\n\t\treturn x.BatchSize\n\t}\n\treturn 0\n}\n\nfunc (x *GetFeedbackStreamRequest) GetScanOptions() *ScanOptions {\n\tif x != nil {\n\t\treturn x.ScanOptions\n\t}\n\treturn nil\n}\n\ntype GetFeedbackStreamResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tFeedback      []*Feedback            `protobuf:\"bytes,1,rep,name=feedback,proto3\" json:\"feedback,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetFeedbackStreamResponse) Reset() {\n\t*x = GetFeedbackStreamResponse{}\n\tmi := &file_data_store_proto_msgTypes[40]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetFeedbackStreamResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetFeedbackStreamResponse) ProtoMessage() {}\n\nfunc (x *GetFeedbackStreamResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[40]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetFeedbackStreamResponse.ProtoReflect.Descriptor instead.\nfunc (*GetFeedbackStreamResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{40}\n}\n\nfunc (x *GetFeedbackStreamResponse) GetFeedback() []*Feedback {\n\tif x != nil {\n\t\treturn x.Feedback\n\t}\n\treturn nil\n}\n\ntype CountUsersRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CountUsersRequest) Reset() {\n\t*x = CountUsersRequest{}\n\tmi := &file_data_store_proto_msgTypes[41]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CountUsersRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CountUsersRequest) ProtoMessage() {}\n\nfunc (x *CountUsersRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[41]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CountUsersRequest.ProtoReflect.Descriptor instead.\nfunc (*CountUsersRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{41}\n}\n\ntype CountUsersResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCount         int32                  `protobuf:\"varint,1,opt,name=count,proto3\" json:\"count,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CountUsersResponse) Reset() {\n\t*x = CountUsersResponse{}\n\tmi := &file_data_store_proto_msgTypes[42]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CountUsersResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CountUsersResponse) ProtoMessage() {}\n\nfunc (x *CountUsersResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[42]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CountUsersResponse.ProtoReflect.Descriptor instead.\nfunc (*CountUsersResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{42}\n}\n\nfunc (x *CountUsersResponse) GetCount() int32 {\n\tif x != nil {\n\t\treturn x.Count\n\t}\n\treturn 0\n}\n\ntype CountItemsRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CountItemsRequest) Reset() {\n\t*x = CountItemsRequest{}\n\tmi := &file_data_store_proto_msgTypes[43]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CountItemsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CountItemsRequest) ProtoMessage() {}\n\nfunc (x *CountItemsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[43]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CountItemsRequest.ProtoReflect.Descriptor instead.\nfunc (*CountItemsRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{43}\n}\n\ntype CountItemsResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCount         int32                  `protobuf:\"varint,1,opt,name=count,proto3\" json:\"count,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CountItemsResponse) Reset() {\n\t*x = CountItemsResponse{}\n\tmi := &file_data_store_proto_msgTypes[44]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CountItemsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CountItemsResponse) ProtoMessage() {}\n\nfunc (x *CountItemsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[44]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CountItemsResponse.ProtoReflect.Descriptor instead.\nfunc (*CountItemsResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{44}\n}\n\nfunc (x *CountItemsResponse) GetCount() int32 {\n\tif x != nil {\n\t\treturn x.Count\n\t}\n\treturn 0\n}\n\ntype CountFeedbackRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CountFeedbackRequest) Reset() {\n\t*x = CountFeedbackRequest{}\n\tmi := &file_data_store_proto_msgTypes[45]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CountFeedbackRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CountFeedbackRequest) ProtoMessage() {}\n\nfunc (x *CountFeedbackRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[45]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CountFeedbackRequest.ProtoReflect.Descriptor instead.\nfunc (*CountFeedbackRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{45}\n}\n\ntype CountFeedbackResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCount         int32                  `protobuf:\"varint,1,opt,name=count,proto3\" json:\"count,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CountFeedbackResponse) Reset() {\n\t*x = CountFeedbackResponse{}\n\tmi := &file_data_store_proto_msgTypes[46]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CountFeedbackResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CountFeedbackResponse) ProtoMessage() {}\n\nfunc (x *CountFeedbackResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[46]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CountFeedbackResponse.ProtoReflect.Descriptor instead.\nfunc (*CountFeedbackResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{46}\n}\n\nfunc (x *CountFeedbackResponse) GetCount() int32 {\n\tif x != nil {\n\t\treturn x.Count\n\t}\n\treturn 0\n}\n\ntype GetLatestItemsRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tN             int32                  `protobuf:\"varint,1,opt,name=n,proto3\" json:\"n,omitempty\"`\n\tCategories    []string               `protobuf:\"bytes,2,rep,name=categories,proto3\" json:\"categories,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetLatestItemsRequest) Reset() {\n\t*x = GetLatestItemsRequest{}\n\tmi := &file_data_store_proto_msgTypes[47]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetLatestItemsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetLatestItemsRequest) ProtoMessage() {}\n\nfunc (x *GetLatestItemsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[47]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetLatestItemsRequest.ProtoReflect.Descriptor instead.\nfunc (*GetLatestItemsRequest) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{47}\n}\n\nfunc (x *GetLatestItemsRequest) GetN() int32 {\n\tif x != nil {\n\t\treturn x.N\n\t}\n\treturn 0\n}\n\nfunc (x *GetLatestItemsRequest) GetCategories() []string {\n\tif x != nil {\n\t\treturn x.Categories\n\t}\n\treturn nil\n}\n\ntype GetLatestItemsResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tItems         []*Item                `protobuf:\"bytes,1,rep,name=items,proto3\" json:\"items,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetLatestItemsResponse) Reset() {\n\t*x = GetLatestItemsResponse{}\n\tmi := &file_data_store_proto_msgTypes[48]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetLatestItemsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetLatestItemsResponse) ProtoMessage() {}\n\nfunc (x *GetLatestItemsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_data_store_proto_msgTypes[48]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetLatestItemsResponse.ProtoReflect.Descriptor instead.\nfunc (*GetLatestItemsResponse) Descriptor() ([]byte, []int) {\n\treturn file_data_store_proto_rawDescGZIP(), []int{48}\n}\n\nfunc (x *GetLatestItemsResponse) GetItems() []*Item {\n\tif x != nil {\n\t\treturn x.Items\n\t}\n\treturn nil\n}\n\nvar File_data_store_proto protoreflect.FileDescriptor\n\nconst file_data_store_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x10data_store.proto\\x12\\bprotocol\\x1a\\x1fgoogle/protobuf/timestamp.proto\\x1a\\x0eprotocol.proto\\\"\\x96\\x01\\n\" +\n\t\"\\x16FeedbackTypeExpression\\x12#\\n\" +\n\t\"\\rfeedback_type\\x18\\x01 \\x01(\\tR\\ffeedbackType\\x12A\\n\" +\n\t\"\\x0fexpression_type\\x18\\x02 \\x01(\\x0e2\\x18.protocol.ExpressionTypeR\\x0eexpressionType\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x03 \\x01(\\x01R\\x05value\\\"l\\n\" +\n\t\"\\tUserPatch\\x12\\x16\\n\" +\n\t\"\\x06labels\\x18\\x01 \\x01(\\fR\\x06labels\\x12\\x1d\\n\" +\n\t\"\\acomment\\x18\\x02 \\x01(\\tH\\x00R\\acomment\\x88\\x01\\x01\\x12\\x1c\\n\" +\n\t\"\\tsubscribe\\x18\\x03 \\x03(\\tR\\tsubscribeB\\n\" +\n\t\"\\n\" +\n\t\"\\b_comment\\\"\\xeb\\x01\\n\" +\n\t\"\\tItemPatch\\x12 \\n\" +\n\t\"\\tis_hidden\\x18\\x01 \\x01(\\bH\\x00R\\bisHidden\\x88\\x01\\x01\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"categories\\x18\\x02 \\x03(\\tR\\n\" +\n\t\"categories\\x12=\\n\" +\n\t\"\\ttimestamp\\x18\\x03 \\x01(\\v2\\x1a.google.protobuf.TimestampH\\x01R\\ttimestamp\\x88\\x01\\x01\\x12\\x16\\n\" +\n\t\"\\x06labels\\x18\\x04 \\x01(\\fR\\x06labels\\x12\\x1d\\n\" +\n\t\"\\acomment\\x18\\x05 \\x01(\\tH\\x02R\\acomment\\x88\\x01\\x01B\\f\\n\" +\n\t\"\\n\" +\n\t\"_is_hiddenB\\f\\n\" +\n\t\"\\n\" +\n\t\"_timestampB\\n\" +\n\t\"\\n\" +\n\t\"\\b_comment\\\"\\xf7\\x03\\n\" +\n\t\"\\vScanOptions\\x12'\\n\" +\n\t\"\\rbegin_user_id\\x18\\x01 \\x01(\\tH\\x00R\\vbeginUserId\\x88\\x01\\x01\\x12#\\n\" +\n\t\"\\vend_user_id\\x18\\x02 \\x01(\\tH\\x01R\\tendUserId\\x88\\x01\\x01\\x12'\\n\" +\n\t\"\\rbegin_item_id\\x18\\x03 \\x01(\\tH\\x02R\\vbeginItemId\\x88\\x01\\x01\\x12#\\n\" +\n\t\"\\vend_item_id\\x18\\x04 \\x01(\\tH\\x03R\\tendItemId\\x88\\x01\\x01\\x12>\\n\" +\n\t\"\\n\" +\n\t\"begin_time\\x18\\x05 \\x01(\\v2\\x1a.google.protobuf.TimestampH\\x04R\\tbeginTime\\x88\\x01\\x01\\x12:\\n\" +\n\t\"\\bend_time\\x18\\x06 \\x01(\\v2\\x1a.google.protobuf.TimestampH\\x05R\\aendTime\\x88\\x01\\x01\\x12G\\n\" +\n\t\"\\x0efeedback_types\\x18\\a \\x03(\\v2 .protocol.FeedbackTypeExpressionR\\rfeedbackTypes\\x12'\\n\" +\n\t\"\\x10order_by_item_id\\x18\\b \\x01(\\bR\\rorderByItemIdB\\x10\\n\" +\n\t\"\\x0e_begin_user_idB\\x0e\\n\" +\n\t\"\\f_end_user_idB\\x10\\n\" +\n\t\"\\x0e_begin_item_idB\\x0e\\n\" +\n\t\"\\f_end_item_idB\\r\\n\" +\n\t\"\\v_begin_timeB\\v\\n\" +\n\t\"\\t_end_time\\\"?\\n\" +\n\t\"\\x17BatchInsertItemsRequest\\x12$\\n\" +\n\t\"\\x05items\\x18\\x01 \\x03(\\v2\\x0e.protocol.ItemR\\x05items\\\"\\x1a\\n\" +\n\t\"\\x18BatchInsertItemsResponse\\\"1\\n\" +\n\t\"\\x14BatchGetItemsRequest\\x12\\x19\\n\" +\n\t\"\\bitem_ids\\x18\\x01 \\x03(\\tR\\aitemIds\\\"=\\n\" +\n\t\"\\x15BatchGetItemsResponse\\x12$\\n\" +\n\t\"\\x05items\\x18\\x01 \\x03(\\v2\\x0e.protocol.ItemR\\x05items\\\",\\n\" +\n\t\"\\x11DeleteItemRequest\\x12\\x17\\n\" +\n\t\"\\aitem_id\\x18\\x01 \\x01(\\tR\\x06itemId\\\"\\x14\\n\" +\n\t\"\\x12DeleteItemResponse\\\")\\n\" +\n\t\"\\x0eGetItemRequest\\x12\\x17\\n\" +\n\t\"\\aitem_id\\x18\\x01 \\x01(\\tR\\x06itemId\\\"C\\n\" +\n\t\"\\x0fGetItemResponse\\x12'\\n\" +\n\t\"\\x04item\\x18\\x01 \\x01(\\v2\\x0e.protocol.ItemH\\x00R\\x04item\\x88\\x01\\x01B\\a\\n\" +\n\t\"\\x05_item\\\"W\\n\" +\n\t\"\\x11ModifyItemRequest\\x12\\x17\\n\" +\n\t\"\\aitem_id\\x18\\x01 \\x01(\\tR\\x06itemId\\x12)\\n\" +\n\t\"\\x05patch\\x18\\x02 \\x01(\\v2\\x13.protocol.ItemPatchR\\x05patch\\\"\\x14\\n\" +\n\t\"\\x12ModifyItemResponse\\\"r\\n\" +\n\t\"\\x0fGetItemsRequest\\x12\\x16\\n\" +\n\t\"\\x06cursor\\x18\\x01 \\x01(\\tR\\x06cursor\\x12\\f\\n\" +\n\t\"\\x01n\\x18\\x02 \\x01(\\x05R\\x01n\\x129\\n\" +\n\t\"\\n\" +\n\t\"begin_time\\x18\\x03 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\tbeginTime\\\"P\\n\" +\n\t\"\\x10GetItemsResponse\\x12\\x16\\n\" +\n\t\"\\x06cursor\\x18\\x01 \\x01(\\tR\\x06cursor\\x12$\\n\" +\n\t\"\\x05items\\x18\\x02 \\x03(\\v2\\x0e.protocol.ItemR\\x05items\\\"z\\n\" +\n\t\"\\x16GetItemFeedbackRequest\\x12\\x17\\n\" +\n\t\"\\aitem_id\\x18\\x01 \\x01(\\tR\\x06itemId\\x12G\\n\" +\n\t\"\\x0efeedback_types\\x18\\x02 \\x03(\\v2 .protocol.FeedbackTypeExpressionR\\rfeedbackTypes\\\"?\\n\" +\n\t\"\\x17BatchInsertUsersRequest\\x12$\\n\" +\n\t\"\\x05users\\x18\\x01 \\x03(\\v2\\x0e.protocol.UserR\\x05users\\\"\\x1a\\n\" +\n\t\"\\x18BatchInsertUsersResponse\\\",\\n\" +\n\t\"\\x11DeleteUserRequest\\x12\\x17\\n\" +\n\t\"\\auser_id\\x18\\x01 \\x01(\\tR\\x06userId\\\"\\x14\\n\" +\n\t\"\\x12DeleteUserResponse\\\")\\n\" +\n\t\"\\x0eGetUserRequest\\x12\\x17\\n\" +\n\t\"\\auser_id\\x18\\x01 \\x01(\\tR\\x06userId\\\"C\\n\" +\n\t\"\\x0fGetUserResponse\\x12'\\n\" +\n\t\"\\x04user\\x18\\x01 \\x01(\\v2\\x0e.protocol.UserH\\x00R\\x04user\\x88\\x01\\x01B\\a\\n\" +\n\t\"\\x05_user\\\"W\\n\" +\n\t\"\\x11ModifyUserRequest\\x12\\x17\\n\" +\n\t\"\\auser_id\\x18\\x01 \\x01(\\tR\\x06userId\\x12)\\n\" +\n\t\"\\x05patch\\x18\\x02 \\x01(\\v2\\x13.protocol.UserPatchR\\x05patch\\\"\\x14\\n\" +\n\t\"\\x12ModifyUserResponse\\\"7\\n\" +\n\t\"\\x0fGetUsersRequest\\x12\\x16\\n\" +\n\t\"\\x06cursor\\x18\\x01 \\x01(\\tR\\x06cursor\\x12\\f\\n\" +\n\t\"\\x01n\\x18\\x02 \\x01(\\x05R\\x01n\\\"P\\n\" +\n\t\"\\x10GetUsersResponse\\x12\\x16\\n\" +\n\t\"\\x06cursor\\x18\\x01 \\x01(\\tR\\x06cursor\\x12$\\n\" +\n\t\"\\x05users\\x18\\x02 \\x03(\\v2\\x0e.protocol.UserR\\x05users\\\"\\xb1\\x01\\n\" +\n\t\"\\x16GetUserFeedbackRequest\\x12\\x17\\n\" +\n\t\"\\auser_id\\x18\\x01 \\x01(\\tR\\x06userId\\x125\\n\" +\n\t\"\\bend_time\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\aendTime\\x12G\\n\" +\n\t\"\\x0efeedback_types\\x18\\x03 \\x03(\\v2 .protocol.FeedbackTypeExpressionR\\rfeedbackTypes\\\"\\x97\\x01\\n\" +\n\t\"\\x1aGetUserItemFeedbackRequest\\x12\\x17\\n\" +\n\t\"\\auser_id\\x18\\x01 \\x01(\\tR\\x06userId\\x12\\x17\\n\" +\n\t\"\\aitem_id\\x18\\x02 \\x01(\\tR\\x06itemId\\x12G\\n\" +\n\t\"\\x0efeedback_types\\x18\\x03 \\x03(\\v2 .protocol.FeedbackTypeExpressionR\\rfeedbackTypes\\\"x\\n\" +\n\t\"\\x1dDeleteUserItemFeedbackRequest\\x12\\x17\\n\" +\n\t\"\\auser_id\\x18\\x01 \\x01(\\tR\\x06userId\\x12\\x17\\n\" +\n\t\"\\aitem_id\\x18\\x02 \\x01(\\tR\\x06itemId\\x12%\\n\" +\n\t\"\\x0efeedback_types\\x18\\x03 \\x03(\\tR\\rfeedbackTypes\\\"6\\n\" +\n\t\"\\x1eDeleteUserItemFeedbackResponse\\x12\\x14\\n\" +\n\t\"\\x05count\\x18\\x01 \\x01(\\x05R\\x05count\\\"\\xac\\x01\\n\" +\n\t\"\\x1aBatchInsertFeedbackRequest\\x12.\\n\" +\n\t\"\\bfeedback\\x18\\x01 \\x03(\\v2\\x12.protocol.FeedbackR\\bfeedback\\x12\\x1f\\n\" +\n\t\"\\vinsert_user\\x18\\x02 \\x01(\\bR\\n\" +\n\t\"insertUser\\x12\\x1f\\n\" +\n\t\"\\vinsert_item\\x18\\x03 \\x01(\\bR\\n\" +\n\t\"insertItem\\x12\\x1c\\n\" +\n\t\"\\toverwrite\\x18\\x04 \\x01(\\bR\\toverwrite\\\"\\x1d\\n\" +\n\t\"\\x1bBatchInsertFeedbackResponse\\\"\\xf5\\x01\\n\" +\n\t\"\\x12GetFeedbackRequest\\x12\\x16\\n\" +\n\t\"\\x06cursor\\x18\\x01 \\x01(\\tR\\x06cursor\\x12\\f\\n\" +\n\t\"\\x01n\\x18\\x02 \\x01(\\x05R\\x01n\\x129\\n\" +\n\t\"\\n\" +\n\t\"begin_time\\x18\\x03 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\tbeginTime\\x125\\n\" +\n\t\"\\bend_time\\x18\\x04 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\aendTime\\x12G\\n\" +\n\t\"\\x0efeedback_types\\x18\\x05 \\x03(\\v2 .protocol.FeedbackTypeExpressionR\\rfeedbackTypes\\\"]\\n\" +\n\t\"\\x13GetFeedbackResponse\\x12\\x16\\n\" +\n\t\"\\x06cursor\\x18\\x01 \\x01(\\tR\\x06cursor\\x12.\\n\" +\n\t\"\\bfeedback\\x18\\x02 \\x03(\\v2\\x12.protocol.FeedbackR\\bfeedback\\\"5\\n\" +\n\t\"\\x14GetUserStreamRequest\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"batch_size\\x18\\x01 \\x01(\\x05R\\tbatchSize\\\"=\\n\" +\n\t\"\\x15GetUserStreamResponse\\x12$\\n\" +\n\t\"\\x05users\\x18\\x01 \\x03(\\v2\\x0e.protocol.UserR\\x05users\\\"p\\n\" +\n\t\"\\x14GetItemStreamRequest\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"batch_size\\x18\\x01 \\x01(\\x05R\\tbatchSize\\x129\\n\" +\n\t\"\\n\" +\n\t\"time_limit\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\ttimeLimit\\\"=\\n\" +\n\t\"\\x15GetItemStreamResponse\\x12$\\n\" +\n\t\"\\x05items\\x18\\x01 \\x03(\\v2\\x0e.protocol.ItemR\\x05items\\\"s\\n\" +\n\t\"\\x18GetFeedbackStreamRequest\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"batch_size\\x18\\x01 \\x01(\\x05R\\tbatchSize\\x128\\n\" +\n\t\"\\fscan_options\\x18\\x02 \\x01(\\v2\\x15.protocol.ScanOptionsR\\vscanOptions\\\"K\\n\" +\n\t\"\\x19GetFeedbackStreamResponse\\x12.\\n\" +\n\t\"\\bfeedback\\x18\\x01 \\x03(\\v2\\x12.protocol.FeedbackR\\bfeedback\\\"\\x13\\n\" +\n\t\"\\x11CountUsersRequest\\\"*\\n\" +\n\t\"\\x12CountUsersResponse\\x12\\x14\\n\" +\n\t\"\\x05count\\x18\\x01 \\x01(\\x05R\\x05count\\\"\\x13\\n\" +\n\t\"\\x11CountItemsRequest\\\"*\\n\" +\n\t\"\\x12CountItemsResponse\\x12\\x14\\n\" +\n\t\"\\x05count\\x18\\x01 \\x01(\\x05R\\x05count\\\"\\x16\\n\" +\n\t\"\\x14CountFeedbackRequest\\\"-\\n\" +\n\t\"\\x15CountFeedbackResponse\\x12\\x14\\n\" +\n\t\"\\x05count\\x18\\x01 \\x01(\\x05R\\x05count\\\"E\\n\" +\n\t\"\\x15GetLatestItemsRequest\\x12\\f\\n\" +\n\t\"\\x01n\\x18\\x01 \\x01(\\x05R\\x01n\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"categories\\x18\\x02 \\x03(\\tR\\n\" +\n\t\"categories\\\">\\n\" +\n\t\"\\x16GetLatestItemsResponse\\x12$\\n\" +\n\t\"\\x05items\\x18\\x01 \\x03(\\v2\\x0e.protocol.ItemR\\x05items*V\\n\" +\n\t\"\\x0eExpressionType\\x12\\b\\n\" +\n\t\"\\x04None\\x10\\x00\\x12\\b\\n\" +\n\t\"\\x04Less\\x10\\x01\\x12\\x0f\\n\" +\n\t\"\\vLessOrEqual\\x10\\x02\\x12\\v\\n\" +\n\t\"\\aGreater\\x10\\x03\\x12\\x12\\n\" +\n\t\"\\x0eGreaterOrEqual\\x10\\x042\\x88\\x10\\n\" +\n\t\"\\tDataStore\\x127\\n\" +\n\t\"\\x04Ping\\x12\\x15.protocol.PingRequest\\x1a\\x16.protocol.PingResponse\\\"\\x00\\x12[\\n\" +\n\t\"\\x10BatchInsertItems\\x12!.protocol.BatchInsertItemsRequest\\x1a\\\".protocol.BatchInsertItemsResponse\\\"\\x00\\x12R\\n\" +\n\t\"\\rBatchGetItems\\x12\\x1e.protocol.BatchGetItemsRequest\\x1a\\x1f.protocol.BatchGetItemsResponse\\\"\\x00\\x12I\\n\" +\n\t\"\\n\" +\n\t\"DeleteItem\\x12\\x1b.protocol.DeleteItemRequest\\x1a\\x1c.protocol.DeleteItemResponse\\\"\\x00\\x12@\\n\" +\n\t\"\\aGetItem\\x12\\x18.protocol.GetItemRequest\\x1a\\x19.protocol.GetItemResponse\\\"\\x00\\x12I\\n\" +\n\t\"\\n\" +\n\t\"ModifyItem\\x12\\x1b.protocol.ModifyItemRequest\\x1a\\x1c.protocol.ModifyItemResponse\\\"\\x00\\x12C\\n\" +\n\t\"\\bGetItems\\x12\\x19.protocol.GetItemsRequest\\x1a\\x1a.protocol.GetItemsResponse\\\"\\x00\\x12T\\n\" +\n\t\"\\x0fGetItemFeedback\\x12 .protocol.GetItemFeedbackRequest\\x1a\\x1d.protocol.GetFeedbackResponse\\\"\\x00\\x12[\\n\" +\n\t\"\\x10BatchInsertUsers\\x12!.protocol.BatchInsertUsersRequest\\x1a\\\".protocol.BatchInsertUsersResponse\\\"\\x00\\x12I\\n\" +\n\t\"\\n\" +\n\t\"DeleteUser\\x12\\x1b.protocol.DeleteUserRequest\\x1a\\x1c.protocol.DeleteUserResponse\\\"\\x00\\x12@\\n\" +\n\t\"\\aGetUser\\x12\\x18.protocol.GetUserRequest\\x1a\\x19.protocol.GetUserResponse\\\"\\x00\\x12I\\n\" +\n\t\"\\n\" +\n\t\"ModifyUser\\x12\\x1b.protocol.ModifyUserRequest\\x1a\\x1c.protocol.ModifyUserResponse\\\"\\x00\\x12C\\n\" +\n\t\"\\bGetUsers\\x12\\x19.protocol.GetUsersRequest\\x1a\\x1a.protocol.GetUsersResponse\\\"\\x00\\x12T\\n\" +\n\t\"\\x0fGetUserFeedback\\x12 .protocol.GetUserFeedbackRequest\\x1a\\x1d.protocol.GetFeedbackResponse\\\"\\x00\\x12\\\\\\n\" +\n\t\"\\x13GetUserItemFeedback\\x12$.protocol.GetUserItemFeedbackRequest\\x1a\\x1d.protocol.GetFeedbackResponse\\\"\\x00\\x12m\\n\" +\n\t\"\\x16DeleteUserItemFeedback\\x12'.protocol.DeleteUserItemFeedbackRequest\\x1a(.protocol.DeleteUserItemFeedbackResponse\\\"\\x00\\x12d\\n\" +\n\t\"\\x13BatchInsertFeedback\\x12$.protocol.BatchInsertFeedbackRequest\\x1a%.protocol.BatchInsertFeedbackResponse\\\"\\x00\\x12L\\n\" +\n\t\"\\vGetFeedback\\x12\\x1c.protocol.GetFeedbackRequest\\x1a\\x1d.protocol.GetFeedbackResponse\\\"\\x00\\x12T\\n\" +\n\t\"\\rGetUserStream\\x12\\x1e.protocol.GetUserStreamRequest\\x1a\\x1f.protocol.GetUserStreamResponse\\\"\\x000\\x01\\x12T\\n\" +\n\t\"\\rGetItemStream\\x12\\x1e.protocol.GetItemStreamRequest\\x1a\\x1f.protocol.GetItemStreamResponse\\\"\\x000\\x01\\x12`\\n\" +\n\t\"\\x11GetFeedbackStream\\x12\\\".protocol.GetFeedbackStreamRequest\\x1a#.protocol.GetFeedbackStreamResponse\\\"\\x000\\x01\\x12I\\n\" +\n\t\"\\n\" +\n\t\"CountUsers\\x12\\x1b.protocol.CountUsersRequest\\x1a\\x1c.protocol.CountUsersResponse\\\"\\x00\\x12I\\n\" +\n\t\"\\n\" +\n\t\"CountItems\\x12\\x1b.protocol.CountItemsRequest\\x1a\\x1c.protocol.CountItemsResponse\\\"\\x00\\x12R\\n\" +\n\t\"\\rCountFeedback\\x12\\x1e.protocol.CountFeedbackRequest\\x1a\\x1f.protocol.CountFeedbackResponse\\\"\\x00\\x12U\\n\" +\n\t\"\\x0eGetLatestItems\\x12\\x1f.protocol.GetLatestItemsRequest\\x1a .protocol.GetLatestItemsResponse\\\"\\x00B$Z\\\"github.com/gorse-io/gorse/protocolb\\x06proto3\"\n\nvar (\n\tfile_data_store_proto_rawDescOnce sync.Once\n\tfile_data_store_proto_rawDescData []byte\n)\n\nfunc file_data_store_proto_rawDescGZIP() []byte {\n\tfile_data_store_proto_rawDescOnce.Do(func() {\n\t\tfile_data_store_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_data_store_proto_rawDesc), len(file_data_store_proto_rawDesc)))\n\t})\n\treturn file_data_store_proto_rawDescData\n}\n\nvar file_data_store_proto_enumTypes = make([]protoimpl.EnumInfo, 1)\nvar file_data_store_proto_msgTypes = make([]protoimpl.MessageInfo, 49)\nvar file_data_store_proto_goTypes = []any{\n\t(ExpressionType)(0),                    // 0: protocol.ExpressionType\n\t(*FeedbackTypeExpression)(nil),         // 1: protocol.FeedbackTypeExpression\n\t(*UserPatch)(nil),                      // 2: protocol.UserPatch\n\t(*ItemPatch)(nil),                      // 3: protocol.ItemPatch\n\t(*ScanOptions)(nil),                    // 4: protocol.ScanOptions\n\t(*BatchInsertItemsRequest)(nil),        // 5: protocol.BatchInsertItemsRequest\n\t(*BatchInsertItemsResponse)(nil),       // 6: protocol.BatchInsertItemsResponse\n\t(*BatchGetItemsRequest)(nil),           // 7: protocol.BatchGetItemsRequest\n\t(*BatchGetItemsResponse)(nil),          // 8: protocol.BatchGetItemsResponse\n\t(*DeleteItemRequest)(nil),              // 9: protocol.DeleteItemRequest\n\t(*DeleteItemResponse)(nil),             // 10: protocol.DeleteItemResponse\n\t(*GetItemRequest)(nil),                 // 11: protocol.GetItemRequest\n\t(*GetItemResponse)(nil),                // 12: protocol.GetItemResponse\n\t(*ModifyItemRequest)(nil),              // 13: protocol.ModifyItemRequest\n\t(*ModifyItemResponse)(nil),             // 14: protocol.ModifyItemResponse\n\t(*GetItemsRequest)(nil),                // 15: protocol.GetItemsRequest\n\t(*GetItemsResponse)(nil),               // 16: protocol.GetItemsResponse\n\t(*GetItemFeedbackRequest)(nil),         // 17: protocol.GetItemFeedbackRequest\n\t(*BatchInsertUsersRequest)(nil),        // 18: protocol.BatchInsertUsersRequest\n\t(*BatchInsertUsersResponse)(nil),       // 19: protocol.BatchInsertUsersResponse\n\t(*DeleteUserRequest)(nil),              // 20: protocol.DeleteUserRequest\n\t(*DeleteUserResponse)(nil),             // 21: protocol.DeleteUserResponse\n\t(*GetUserRequest)(nil),                 // 22: protocol.GetUserRequest\n\t(*GetUserResponse)(nil),                // 23: protocol.GetUserResponse\n\t(*ModifyUserRequest)(nil),              // 24: protocol.ModifyUserRequest\n\t(*ModifyUserResponse)(nil),             // 25: protocol.ModifyUserResponse\n\t(*GetUsersRequest)(nil),                // 26: protocol.GetUsersRequest\n\t(*GetUsersResponse)(nil),               // 27: protocol.GetUsersResponse\n\t(*GetUserFeedbackRequest)(nil),         // 28: protocol.GetUserFeedbackRequest\n\t(*GetUserItemFeedbackRequest)(nil),     // 29: protocol.GetUserItemFeedbackRequest\n\t(*DeleteUserItemFeedbackRequest)(nil),  // 30: protocol.DeleteUserItemFeedbackRequest\n\t(*DeleteUserItemFeedbackResponse)(nil), // 31: protocol.DeleteUserItemFeedbackResponse\n\t(*BatchInsertFeedbackRequest)(nil),     // 32: protocol.BatchInsertFeedbackRequest\n\t(*BatchInsertFeedbackResponse)(nil),    // 33: protocol.BatchInsertFeedbackResponse\n\t(*GetFeedbackRequest)(nil),             // 34: protocol.GetFeedbackRequest\n\t(*GetFeedbackResponse)(nil),            // 35: protocol.GetFeedbackResponse\n\t(*GetUserStreamRequest)(nil),           // 36: protocol.GetUserStreamRequest\n\t(*GetUserStreamResponse)(nil),          // 37: protocol.GetUserStreamResponse\n\t(*GetItemStreamRequest)(nil),           // 38: protocol.GetItemStreamRequest\n\t(*GetItemStreamResponse)(nil),          // 39: protocol.GetItemStreamResponse\n\t(*GetFeedbackStreamRequest)(nil),       // 40: protocol.GetFeedbackStreamRequest\n\t(*GetFeedbackStreamResponse)(nil),      // 41: protocol.GetFeedbackStreamResponse\n\t(*CountUsersRequest)(nil),              // 42: protocol.CountUsersRequest\n\t(*CountUsersResponse)(nil),             // 43: protocol.CountUsersResponse\n\t(*CountItemsRequest)(nil),              // 44: protocol.CountItemsRequest\n\t(*CountItemsResponse)(nil),             // 45: protocol.CountItemsResponse\n\t(*CountFeedbackRequest)(nil),           // 46: protocol.CountFeedbackRequest\n\t(*CountFeedbackResponse)(nil),          // 47: protocol.CountFeedbackResponse\n\t(*GetLatestItemsRequest)(nil),          // 48: protocol.GetLatestItemsRequest\n\t(*GetLatestItemsResponse)(nil),         // 49: protocol.GetLatestItemsResponse\n\t(*timestamppb.Timestamp)(nil),          // 50: google.protobuf.Timestamp\n\t(*Item)(nil),                           // 51: protocol.Item\n\t(*User)(nil),                           // 52: protocol.User\n\t(*Feedback)(nil),                       // 53: protocol.Feedback\n\t(*PingRequest)(nil),                    // 54: protocol.PingRequest\n\t(*PingResponse)(nil),                   // 55: protocol.PingResponse\n}\nvar file_data_store_proto_depIdxs = []int32{\n\t0,  // 0: protocol.FeedbackTypeExpression.expression_type:type_name -> protocol.ExpressionType\n\t50, // 1: protocol.ItemPatch.timestamp:type_name -> google.protobuf.Timestamp\n\t50, // 2: protocol.ScanOptions.begin_time:type_name -> google.protobuf.Timestamp\n\t50, // 3: protocol.ScanOptions.end_time:type_name -> google.protobuf.Timestamp\n\t1,  // 4: protocol.ScanOptions.feedback_types:type_name -> protocol.FeedbackTypeExpression\n\t51, // 5: protocol.BatchInsertItemsRequest.items:type_name -> protocol.Item\n\t51, // 6: protocol.BatchGetItemsResponse.items:type_name -> protocol.Item\n\t51, // 7: protocol.GetItemResponse.item:type_name -> protocol.Item\n\t3,  // 8: protocol.ModifyItemRequest.patch:type_name -> protocol.ItemPatch\n\t50, // 9: protocol.GetItemsRequest.begin_time:type_name -> google.protobuf.Timestamp\n\t51, // 10: protocol.GetItemsResponse.items:type_name -> protocol.Item\n\t1,  // 11: protocol.GetItemFeedbackRequest.feedback_types:type_name -> protocol.FeedbackTypeExpression\n\t52, // 12: protocol.BatchInsertUsersRequest.users:type_name -> protocol.User\n\t52, // 13: protocol.GetUserResponse.user:type_name -> protocol.User\n\t2,  // 14: protocol.ModifyUserRequest.patch:type_name -> protocol.UserPatch\n\t52, // 15: protocol.GetUsersResponse.users:type_name -> protocol.User\n\t50, // 16: protocol.GetUserFeedbackRequest.end_time:type_name -> google.protobuf.Timestamp\n\t1,  // 17: protocol.GetUserFeedbackRequest.feedback_types:type_name -> protocol.FeedbackTypeExpression\n\t1,  // 18: protocol.GetUserItemFeedbackRequest.feedback_types:type_name -> protocol.FeedbackTypeExpression\n\t53, // 19: protocol.BatchInsertFeedbackRequest.feedback:type_name -> protocol.Feedback\n\t50, // 20: protocol.GetFeedbackRequest.begin_time:type_name -> google.protobuf.Timestamp\n\t50, // 21: protocol.GetFeedbackRequest.end_time:type_name -> google.protobuf.Timestamp\n\t1,  // 22: protocol.GetFeedbackRequest.feedback_types:type_name -> protocol.FeedbackTypeExpression\n\t53, // 23: protocol.GetFeedbackResponse.feedback:type_name -> protocol.Feedback\n\t52, // 24: protocol.GetUserStreamResponse.users:type_name -> protocol.User\n\t50, // 25: protocol.GetItemStreamRequest.time_limit:type_name -> google.protobuf.Timestamp\n\t51, // 26: protocol.GetItemStreamResponse.items:type_name -> protocol.Item\n\t4,  // 27: protocol.GetFeedbackStreamRequest.scan_options:type_name -> protocol.ScanOptions\n\t53, // 28: protocol.GetFeedbackStreamResponse.feedback:type_name -> protocol.Feedback\n\t51, // 29: protocol.GetLatestItemsResponse.items:type_name -> protocol.Item\n\t54, // 30: protocol.DataStore.Ping:input_type -> protocol.PingRequest\n\t5,  // 31: protocol.DataStore.BatchInsertItems:input_type -> protocol.BatchInsertItemsRequest\n\t7,  // 32: protocol.DataStore.BatchGetItems:input_type -> protocol.BatchGetItemsRequest\n\t9,  // 33: protocol.DataStore.DeleteItem:input_type -> protocol.DeleteItemRequest\n\t11, // 34: protocol.DataStore.GetItem:input_type -> protocol.GetItemRequest\n\t13, // 35: protocol.DataStore.ModifyItem:input_type -> protocol.ModifyItemRequest\n\t15, // 36: protocol.DataStore.GetItems:input_type -> protocol.GetItemsRequest\n\t17, // 37: protocol.DataStore.GetItemFeedback:input_type -> protocol.GetItemFeedbackRequest\n\t18, // 38: protocol.DataStore.BatchInsertUsers:input_type -> protocol.BatchInsertUsersRequest\n\t20, // 39: protocol.DataStore.DeleteUser:input_type -> protocol.DeleteUserRequest\n\t22, // 40: protocol.DataStore.GetUser:input_type -> protocol.GetUserRequest\n\t24, // 41: protocol.DataStore.ModifyUser:input_type -> protocol.ModifyUserRequest\n\t26, // 42: protocol.DataStore.GetUsers:input_type -> protocol.GetUsersRequest\n\t28, // 43: protocol.DataStore.GetUserFeedback:input_type -> protocol.GetUserFeedbackRequest\n\t29, // 44: protocol.DataStore.GetUserItemFeedback:input_type -> protocol.GetUserItemFeedbackRequest\n\t30, // 45: protocol.DataStore.DeleteUserItemFeedback:input_type -> protocol.DeleteUserItemFeedbackRequest\n\t32, // 46: protocol.DataStore.BatchInsertFeedback:input_type -> protocol.BatchInsertFeedbackRequest\n\t34, // 47: protocol.DataStore.GetFeedback:input_type -> protocol.GetFeedbackRequest\n\t36, // 48: protocol.DataStore.GetUserStream:input_type -> protocol.GetUserStreamRequest\n\t38, // 49: protocol.DataStore.GetItemStream:input_type -> protocol.GetItemStreamRequest\n\t40, // 50: protocol.DataStore.GetFeedbackStream:input_type -> protocol.GetFeedbackStreamRequest\n\t42, // 51: protocol.DataStore.CountUsers:input_type -> protocol.CountUsersRequest\n\t44, // 52: protocol.DataStore.CountItems:input_type -> protocol.CountItemsRequest\n\t46, // 53: protocol.DataStore.CountFeedback:input_type -> protocol.CountFeedbackRequest\n\t48, // 54: protocol.DataStore.GetLatestItems:input_type -> protocol.GetLatestItemsRequest\n\t55, // 55: protocol.DataStore.Ping:output_type -> protocol.PingResponse\n\t6,  // 56: protocol.DataStore.BatchInsertItems:output_type -> protocol.BatchInsertItemsResponse\n\t8,  // 57: protocol.DataStore.BatchGetItems:output_type -> protocol.BatchGetItemsResponse\n\t10, // 58: protocol.DataStore.DeleteItem:output_type -> protocol.DeleteItemResponse\n\t12, // 59: protocol.DataStore.GetItem:output_type -> protocol.GetItemResponse\n\t14, // 60: protocol.DataStore.ModifyItem:output_type -> protocol.ModifyItemResponse\n\t16, // 61: protocol.DataStore.GetItems:output_type -> protocol.GetItemsResponse\n\t35, // 62: protocol.DataStore.GetItemFeedback:output_type -> protocol.GetFeedbackResponse\n\t19, // 63: protocol.DataStore.BatchInsertUsers:output_type -> protocol.BatchInsertUsersResponse\n\t21, // 64: protocol.DataStore.DeleteUser:output_type -> protocol.DeleteUserResponse\n\t23, // 65: protocol.DataStore.GetUser:output_type -> protocol.GetUserResponse\n\t25, // 66: protocol.DataStore.ModifyUser:output_type -> protocol.ModifyUserResponse\n\t27, // 67: protocol.DataStore.GetUsers:output_type -> protocol.GetUsersResponse\n\t35, // 68: protocol.DataStore.GetUserFeedback:output_type -> protocol.GetFeedbackResponse\n\t35, // 69: protocol.DataStore.GetUserItemFeedback:output_type -> protocol.GetFeedbackResponse\n\t31, // 70: protocol.DataStore.DeleteUserItemFeedback:output_type -> protocol.DeleteUserItemFeedbackResponse\n\t33, // 71: protocol.DataStore.BatchInsertFeedback:output_type -> protocol.BatchInsertFeedbackResponse\n\t35, // 72: protocol.DataStore.GetFeedback:output_type -> protocol.GetFeedbackResponse\n\t37, // 73: protocol.DataStore.GetUserStream:output_type -> protocol.GetUserStreamResponse\n\t39, // 74: protocol.DataStore.GetItemStream:output_type -> protocol.GetItemStreamResponse\n\t41, // 75: protocol.DataStore.GetFeedbackStream:output_type -> protocol.GetFeedbackStreamResponse\n\t43, // 76: protocol.DataStore.CountUsers:output_type -> protocol.CountUsersResponse\n\t45, // 77: protocol.DataStore.CountItems:output_type -> protocol.CountItemsResponse\n\t47, // 78: protocol.DataStore.CountFeedback:output_type -> protocol.CountFeedbackResponse\n\t49, // 79: protocol.DataStore.GetLatestItems:output_type -> protocol.GetLatestItemsResponse\n\t55, // [55:80] is the sub-list for method output_type\n\t30, // [30:55] is the sub-list for method input_type\n\t30, // [30:30] is the sub-list for extension type_name\n\t30, // [30:30] is the sub-list for extension extendee\n\t0,  // [0:30] is the sub-list for field type_name\n}\n\nfunc init() { file_data_store_proto_init() }\nfunc file_data_store_proto_init() {\n\tif File_data_store_proto != nil {\n\t\treturn\n\t}\n\tfile_protocol_proto_init()\n\tfile_data_store_proto_msgTypes[1].OneofWrappers = []any{}\n\tfile_data_store_proto_msgTypes[2].OneofWrappers = []any{}\n\tfile_data_store_proto_msgTypes[3].OneofWrappers = []any{}\n\tfile_data_store_proto_msgTypes[11].OneofWrappers = []any{}\n\tfile_data_store_proto_msgTypes[22].OneofWrappers = []any{}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_data_store_proto_rawDesc), len(file_data_store_proto_rawDesc)),\n\t\t\tNumEnums:      1,\n\t\t\tNumMessages:   49,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_data_store_proto_goTypes,\n\t\tDependencyIndexes: file_data_store_proto_depIdxs,\n\t\tEnumInfos:         file_data_store_proto_enumTypes,\n\t\tMessageInfos:      file_data_store_proto_msgTypes,\n\t}.Build()\n\tFile_data_store_proto = out.File\n\tfile_data_store_proto_goTypes = nil\n\tfile_data_store_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "protocol/data_store.proto",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nsyntax = \"proto3\";\n\noption go_package = \"github.com/gorse-io/gorse/protocol\";\n\npackage protocol;\n\nimport \"google/protobuf/timestamp.proto\";\nimport \"protocol.proto\";\n\nenum ExpressionType {\n  None = 0;\n  Less = 1;\n  LessOrEqual = 2;\n  Greater = 3;\n  GreaterOrEqual = 4;\n}\n\nmessage FeedbackTypeExpression {\n  string feedback_type = 1;\n  ExpressionType expression_type = 2;\n  double value = 3;\n}\n\nmessage UserPatch {\n  bytes labels = 1;\n  optional string comment = 2;\n  repeated string subscribe = 3;\n}\n\nmessage ItemPatch {\n  optional bool is_hidden = 1;\n  repeated string categories = 2;\n  optional google.protobuf.Timestamp timestamp = 3;\n  bytes labels = 4;\n  optional string comment = 5;\n}\n\nmessage ScanOptions {\n  optional string begin_user_id = 1;\n  optional string end_user_id = 2;\n  optional string begin_item_id = 3;\n  optional string end_item_id = 4;\n  optional google.protobuf.Timestamp begin_time = 5;\n  optional google.protobuf.Timestamp end_time = 6;\n  repeated FeedbackTypeExpression feedback_types = 7;\n  bool order_by_item_id = 8;\n}\n\nmessage BatchInsertItemsRequest { repeated Item items = 1; }\n\nmessage BatchInsertItemsResponse {}\n\nmessage BatchGetItemsRequest { repeated string item_ids = 1; }\n\nmessage BatchGetItemsResponse { repeated Item items = 1; }\n\nmessage DeleteItemRequest { string item_id = 1; }\n\nmessage DeleteItemResponse {}\n\nmessage GetItemRequest { string item_id = 1; }\n\nmessage GetItemResponse { optional Item item = 1; }\n\nmessage ModifyItemRequest {\n  string item_id = 1;\n  ItemPatch patch = 2;\n}\n\nmessage ModifyItemResponse {}\n\nmessage GetItemsRequest {\n  string cursor = 1;\n  int32 n = 2;\n  google.protobuf.Timestamp begin_time = 3;\n}\n\nmessage GetItemsResponse {\n  string cursor = 1;\n  repeated Item items = 2;\n}\n\nmessage GetItemFeedbackRequest {\n  string item_id = 1;\n  repeated FeedbackTypeExpression feedback_types = 2;\n}\n\nmessage BatchInsertUsersRequest { repeated User users = 1; }\n\nmessage BatchInsertUsersResponse {}\n\nmessage DeleteUserRequest { string user_id = 1; }\n\nmessage DeleteUserResponse {}\n\nmessage GetUserRequest { string user_id = 1; }\n\nmessage GetUserResponse { optional User user = 1; }\n\nmessage ModifyUserRequest {\n  string user_id = 1;\n  UserPatch patch = 2;\n}\n\nmessage ModifyUserResponse {}\n\nmessage GetUsersRequest {\n  string cursor = 1;\n  int32 n = 2;\n}\n\nmessage GetUsersResponse {\n  string cursor = 1;\n  repeated User users = 2;\n}\n\nmessage GetUserFeedbackRequest {\n  string user_id = 1;\n  google.protobuf.Timestamp end_time = 2;\n  repeated FeedbackTypeExpression feedback_types = 3;\n}\n\nmessage GetUserItemFeedbackRequest {\n  string user_id = 1;\n  string item_id = 2;\n  repeated FeedbackTypeExpression feedback_types = 3;\n}\n\nmessage DeleteUserItemFeedbackRequest {\n  string user_id = 1;\n  string item_id = 2;\n  repeated string feedback_types = 3;\n}\n\nmessage DeleteUserItemFeedbackResponse { int32 count = 1; }\n\nmessage BatchInsertFeedbackRequest {\n  repeated Feedback feedback = 1;\n  bool insert_user = 2;\n  bool insert_item = 3;\n  bool overwrite = 4;\n}\n\nmessage BatchInsertFeedbackResponse {}\n\nmessage GetFeedbackRequest {\n  string cursor = 1;\n  int32 n = 2;\n  google.protobuf.Timestamp begin_time = 3;\n  google.protobuf.Timestamp end_time = 4;\n  repeated FeedbackTypeExpression feedback_types = 5;\n}\n\nmessage GetFeedbackResponse {\n  string cursor = 1;\n  repeated Feedback feedback = 2;\n}\n\nmessage GetUserStreamRequest { int32 batch_size = 1; }\n\nmessage GetUserStreamResponse { repeated User users = 1; }\n\nmessage GetItemStreamRequest {\n  int32 batch_size = 1;\n  google.protobuf.Timestamp time_limit = 2;\n}\n\nmessage GetItemStreamResponse { repeated Item items = 1; }\n\nmessage GetFeedbackStreamRequest {\n  int32 batch_size = 1;\n  ScanOptions scan_options = 2;\n}\n\nmessage GetFeedbackStreamResponse { repeated Feedback feedback = 1; }\n\nmessage CountUsersRequest {}\n\nmessage CountUsersResponse { int32 count = 1; }\n\nmessage CountItemsRequest {}\n\nmessage CountItemsResponse { int32 count = 1; }\n\nmessage CountFeedbackRequest {}\n\nmessage CountFeedbackResponse { int32 count = 1; }\n\nmessage GetLatestItemsRequest {\n  int32 n = 1;\n  repeated string categories = 2;\n}\n\nmessage GetLatestItemsResponse {\n  repeated Item items = 1;\n}\n\nservice DataStore {\n  rpc Ping(PingRequest) returns (PingResponse) {}\n  rpc BatchInsertItems(BatchInsertItemsRequest)\n      returns (BatchInsertItemsResponse) {}\n  rpc BatchGetItems(BatchGetItemsRequest) returns (BatchGetItemsResponse) {}\n  rpc DeleteItem(DeleteItemRequest) returns (DeleteItemResponse) {}\n  rpc GetItem(GetItemRequest) returns (GetItemResponse) {}\n  rpc ModifyItem(ModifyItemRequest) returns (ModifyItemResponse) {}\n  rpc GetItems(GetItemsRequest) returns (GetItemsResponse) {}\n  rpc GetItemFeedback(GetItemFeedbackRequest) returns (GetFeedbackResponse) {}\n  rpc BatchInsertUsers(BatchInsertUsersRequest)\n      returns (BatchInsertUsersResponse) {}\n  rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse) {}\n  rpc GetUser(GetUserRequest) returns (GetUserResponse) {}\n  rpc ModifyUser(ModifyUserRequest) returns (ModifyUserResponse) {}\n  rpc GetUsers(GetUsersRequest) returns (GetUsersResponse) {}\n  rpc GetUserFeedback(GetUserFeedbackRequest) returns (GetFeedbackResponse) {}\n  rpc GetUserItemFeedback(GetUserItemFeedbackRequest)\n      returns (GetFeedbackResponse) {}\n  rpc DeleteUserItemFeedback(DeleteUserItemFeedbackRequest)\n      returns (DeleteUserItemFeedbackResponse) {}\n  rpc BatchInsertFeedback(BatchInsertFeedbackRequest)\n      returns (BatchInsertFeedbackResponse) {}\n  rpc GetFeedback(GetFeedbackRequest) returns (GetFeedbackResponse) {}\n  rpc GetUserStream(GetUserStreamRequest)\n      returns (stream GetUserStreamResponse) {}\n  rpc GetItemStream(GetItemStreamRequest)\n      returns (stream GetItemStreamResponse) {}\n  rpc GetFeedbackStream(GetFeedbackStreamRequest)\n      returns (stream GetFeedbackStreamResponse) {}\n  rpc CountUsers(CountUsersRequest) returns (CountUsersResponse) {}\n  rpc CountItems(CountItemsRequest) returns (CountItemsResponse) {}\n  rpc CountFeedback(CountFeedbackRequest) returns (CountFeedbackResponse) {}\n  rpc GetLatestItems(GetLatestItemsRequest) returns (GetLatestItemsResponse) {}\n}\n"
  },
  {
    "path": "protocol/data_store_grpc.pb.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.6.0\n// - protoc             v6.33.1\n// source: data_store.proto\n\npackage protocol\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.64.0 or later.\nconst _ = grpc.SupportPackageIsVersion9\n\nconst (\n\tDataStore_Ping_FullMethodName                   = \"/protocol.DataStore/Ping\"\n\tDataStore_BatchInsertItems_FullMethodName       = \"/protocol.DataStore/BatchInsertItems\"\n\tDataStore_BatchGetItems_FullMethodName          = \"/protocol.DataStore/BatchGetItems\"\n\tDataStore_DeleteItem_FullMethodName             = \"/protocol.DataStore/DeleteItem\"\n\tDataStore_GetItem_FullMethodName                = \"/protocol.DataStore/GetItem\"\n\tDataStore_ModifyItem_FullMethodName             = \"/protocol.DataStore/ModifyItem\"\n\tDataStore_GetItems_FullMethodName               = \"/protocol.DataStore/GetItems\"\n\tDataStore_GetItemFeedback_FullMethodName        = \"/protocol.DataStore/GetItemFeedback\"\n\tDataStore_BatchInsertUsers_FullMethodName       = \"/protocol.DataStore/BatchInsertUsers\"\n\tDataStore_DeleteUser_FullMethodName             = \"/protocol.DataStore/DeleteUser\"\n\tDataStore_GetUser_FullMethodName                = \"/protocol.DataStore/GetUser\"\n\tDataStore_ModifyUser_FullMethodName             = \"/protocol.DataStore/ModifyUser\"\n\tDataStore_GetUsers_FullMethodName               = \"/protocol.DataStore/GetUsers\"\n\tDataStore_GetUserFeedback_FullMethodName        = \"/protocol.DataStore/GetUserFeedback\"\n\tDataStore_GetUserItemFeedback_FullMethodName    = \"/protocol.DataStore/GetUserItemFeedback\"\n\tDataStore_DeleteUserItemFeedback_FullMethodName = \"/protocol.DataStore/DeleteUserItemFeedback\"\n\tDataStore_BatchInsertFeedback_FullMethodName    = \"/protocol.DataStore/BatchInsertFeedback\"\n\tDataStore_GetFeedback_FullMethodName            = \"/protocol.DataStore/GetFeedback\"\n\tDataStore_GetUserStream_FullMethodName          = \"/protocol.DataStore/GetUserStream\"\n\tDataStore_GetItemStream_FullMethodName          = \"/protocol.DataStore/GetItemStream\"\n\tDataStore_GetFeedbackStream_FullMethodName      = \"/protocol.DataStore/GetFeedbackStream\"\n\tDataStore_CountUsers_FullMethodName             = \"/protocol.DataStore/CountUsers\"\n\tDataStore_CountItems_FullMethodName             = \"/protocol.DataStore/CountItems\"\n\tDataStore_CountFeedback_FullMethodName          = \"/protocol.DataStore/CountFeedback\"\n\tDataStore_GetLatestItems_FullMethodName         = \"/protocol.DataStore/GetLatestItems\"\n)\n\n// DataStoreClient is the client API for DataStore service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype DataStoreClient interface {\n\tPing(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error)\n\tBatchInsertItems(ctx context.Context, in *BatchInsertItemsRequest, opts ...grpc.CallOption) (*BatchInsertItemsResponse, error)\n\tBatchGetItems(ctx context.Context, in *BatchGetItemsRequest, opts ...grpc.CallOption) (*BatchGetItemsResponse, error)\n\tDeleteItem(ctx context.Context, in *DeleteItemRequest, opts ...grpc.CallOption) (*DeleteItemResponse, error)\n\tGetItem(ctx context.Context, in *GetItemRequest, opts ...grpc.CallOption) (*GetItemResponse, error)\n\tModifyItem(ctx context.Context, in *ModifyItemRequest, opts ...grpc.CallOption) (*ModifyItemResponse, error)\n\tGetItems(ctx context.Context, in *GetItemsRequest, opts ...grpc.CallOption) (*GetItemsResponse, error)\n\tGetItemFeedback(ctx context.Context, in *GetItemFeedbackRequest, opts ...grpc.CallOption) (*GetFeedbackResponse, error)\n\tBatchInsertUsers(ctx context.Context, in *BatchInsertUsersRequest, opts ...grpc.CallOption) (*BatchInsertUsersResponse, error)\n\tDeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*DeleteUserResponse, error)\n\tGetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error)\n\tModifyUser(ctx context.Context, in *ModifyUserRequest, opts ...grpc.CallOption) (*ModifyUserResponse, error)\n\tGetUsers(ctx context.Context, in *GetUsersRequest, opts ...grpc.CallOption) (*GetUsersResponse, error)\n\tGetUserFeedback(ctx context.Context, in *GetUserFeedbackRequest, opts ...grpc.CallOption) (*GetFeedbackResponse, error)\n\tGetUserItemFeedback(ctx context.Context, in *GetUserItemFeedbackRequest, opts ...grpc.CallOption) (*GetFeedbackResponse, error)\n\tDeleteUserItemFeedback(ctx context.Context, in *DeleteUserItemFeedbackRequest, opts ...grpc.CallOption) (*DeleteUserItemFeedbackResponse, error)\n\tBatchInsertFeedback(ctx context.Context, in *BatchInsertFeedbackRequest, opts ...grpc.CallOption) (*BatchInsertFeedbackResponse, error)\n\tGetFeedback(ctx context.Context, in *GetFeedbackRequest, opts ...grpc.CallOption) (*GetFeedbackResponse, error)\n\tGetUserStream(ctx context.Context, in *GetUserStreamRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GetUserStreamResponse], error)\n\tGetItemStream(ctx context.Context, in *GetItemStreamRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GetItemStreamResponse], error)\n\tGetFeedbackStream(ctx context.Context, in *GetFeedbackStreamRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GetFeedbackStreamResponse], error)\n\tCountUsers(ctx context.Context, in *CountUsersRequest, opts ...grpc.CallOption) (*CountUsersResponse, error)\n\tCountItems(ctx context.Context, in *CountItemsRequest, opts ...grpc.CallOption) (*CountItemsResponse, error)\n\tCountFeedback(ctx context.Context, in *CountFeedbackRequest, opts ...grpc.CallOption) (*CountFeedbackResponse, error)\n\tGetLatestItems(ctx context.Context, in *GetLatestItemsRequest, opts ...grpc.CallOption) (*GetLatestItemsResponse, error)\n}\n\ntype dataStoreClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewDataStoreClient(cc grpc.ClientConnInterface) DataStoreClient {\n\treturn &dataStoreClient{cc}\n}\n\nfunc (c *dataStoreClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(PingResponse)\n\terr := c.cc.Invoke(ctx, DataStore_Ping_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) BatchInsertItems(ctx context.Context, in *BatchInsertItemsRequest, opts ...grpc.CallOption) (*BatchInsertItemsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(BatchInsertItemsResponse)\n\terr := c.cc.Invoke(ctx, DataStore_BatchInsertItems_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) BatchGetItems(ctx context.Context, in *BatchGetItemsRequest, opts ...grpc.CallOption) (*BatchGetItemsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(BatchGetItemsResponse)\n\terr := c.cc.Invoke(ctx, DataStore_BatchGetItems_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) DeleteItem(ctx context.Context, in *DeleteItemRequest, opts ...grpc.CallOption) (*DeleteItemResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(DeleteItemResponse)\n\terr := c.cc.Invoke(ctx, DataStore_DeleteItem_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) GetItem(ctx context.Context, in *GetItemRequest, opts ...grpc.CallOption) (*GetItemResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(GetItemResponse)\n\terr := c.cc.Invoke(ctx, DataStore_GetItem_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) ModifyItem(ctx context.Context, in *ModifyItemRequest, opts ...grpc.CallOption) (*ModifyItemResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ModifyItemResponse)\n\terr := c.cc.Invoke(ctx, DataStore_ModifyItem_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) GetItems(ctx context.Context, in *GetItemsRequest, opts ...grpc.CallOption) (*GetItemsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(GetItemsResponse)\n\terr := c.cc.Invoke(ctx, DataStore_GetItems_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) GetItemFeedback(ctx context.Context, in *GetItemFeedbackRequest, opts ...grpc.CallOption) (*GetFeedbackResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(GetFeedbackResponse)\n\terr := c.cc.Invoke(ctx, DataStore_GetItemFeedback_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) BatchInsertUsers(ctx context.Context, in *BatchInsertUsersRequest, opts ...grpc.CallOption) (*BatchInsertUsersResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(BatchInsertUsersResponse)\n\terr := c.cc.Invoke(ctx, DataStore_BatchInsertUsers_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*DeleteUserResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(DeleteUserResponse)\n\terr := c.cc.Invoke(ctx, DataStore_DeleteUser_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(GetUserResponse)\n\terr := c.cc.Invoke(ctx, DataStore_GetUser_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) ModifyUser(ctx context.Context, in *ModifyUserRequest, opts ...grpc.CallOption) (*ModifyUserResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ModifyUserResponse)\n\terr := c.cc.Invoke(ctx, DataStore_ModifyUser_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) GetUsers(ctx context.Context, in *GetUsersRequest, opts ...grpc.CallOption) (*GetUsersResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(GetUsersResponse)\n\terr := c.cc.Invoke(ctx, DataStore_GetUsers_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) GetUserFeedback(ctx context.Context, in *GetUserFeedbackRequest, opts ...grpc.CallOption) (*GetFeedbackResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(GetFeedbackResponse)\n\terr := c.cc.Invoke(ctx, DataStore_GetUserFeedback_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) GetUserItemFeedback(ctx context.Context, in *GetUserItemFeedbackRequest, opts ...grpc.CallOption) (*GetFeedbackResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(GetFeedbackResponse)\n\terr := c.cc.Invoke(ctx, DataStore_GetUserItemFeedback_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) DeleteUserItemFeedback(ctx context.Context, in *DeleteUserItemFeedbackRequest, opts ...grpc.CallOption) (*DeleteUserItemFeedbackResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(DeleteUserItemFeedbackResponse)\n\terr := c.cc.Invoke(ctx, DataStore_DeleteUserItemFeedback_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) BatchInsertFeedback(ctx context.Context, in *BatchInsertFeedbackRequest, opts ...grpc.CallOption) (*BatchInsertFeedbackResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(BatchInsertFeedbackResponse)\n\terr := c.cc.Invoke(ctx, DataStore_BatchInsertFeedback_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) GetFeedback(ctx context.Context, in *GetFeedbackRequest, opts ...grpc.CallOption) (*GetFeedbackResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(GetFeedbackResponse)\n\terr := c.cc.Invoke(ctx, DataStore_GetFeedback_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) GetUserStream(ctx context.Context, in *GetUserStreamRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GetUserStreamResponse], error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tstream, err := c.cc.NewStream(ctx, &DataStore_ServiceDesc.Streams[0], DataStore_GetUserStream_FullMethodName, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &grpc.GenericClientStream[GetUserStreamRequest, GetUserStreamResponse]{ClientStream: stream}\n\tif err := x.ClientStream.SendMsg(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := x.ClientStream.CloseSend(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn x, nil\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype DataStore_GetUserStreamClient = grpc.ServerStreamingClient[GetUserStreamResponse]\n\nfunc (c *dataStoreClient) GetItemStream(ctx context.Context, in *GetItemStreamRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GetItemStreamResponse], error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tstream, err := c.cc.NewStream(ctx, &DataStore_ServiceDesc.Streams[1], DataStore_GetItemStream_FullMethodName, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &grpc.GenericClientStream[GetItemStreamRequest, GetItemStreamResponse]{ClientStream: stream}\n\tif err := x.ClientStream.SendMsg(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := x.ClientStream.CloseSend(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn x, nil\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype DataStore_GetItemStreamClient = grpc.ServerStreamingClient[GetItemStreamResponse]\n\nfunc (c *dataStoreClient) GetFeedbackStream(ctx context.Context, in *GetFeedbackStreamRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GetFeedbackStreamResponse], error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tstream, err := c.cc.NewStream(ctx, &DataStore_ServiceDesc.Streams[2], DataStore_GetFeedbackStream_FullMethodName, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &grpc.GenericClientStream[GetFeedbackStreamRequest, GetFeedbackStreamResponse]{ClientStream: stream}\n\tif err := x.ClientStream.SendMsg(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := x.ClientStream.CloseSend(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn x, nil\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype DataStore_GetFeedbackStreamClient = grpc.ServerStreamingClient[GetFeedbackStreamResponse]\n\nfunc (c *dataStoreClient) CountUsers(ctx context.Context, in *CountUsersRequest, opts ...grpc.CallOption) (*CountUsersResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(CountUsersResponse)\n\terr := c.cc.Invoke(ctx, DataStore_CountUsers_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) CountItems(ctx context.Context, in *CountItemsRequest, opts ...grpc.CallOption) (*CountItemsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(CountItemsResponse)\n\terr := c.cc.Invoke(ctx, DataStore_CountItems_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) CountFeedback(ctx context.Context, in *CountFeedbackRequest, opts ...grpc.CallOption) (*CountFeedbackResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(CountFeedbackResponse)\n\terr := c.cc.Invoke(ctx, DataStore_CountFeedback_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *dataStoreClient) GetLatestItems(ctx context.Context, in *GetLatestItemsRequest, opts ...grpc.CallOption) (*GetLatestItemsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(GetLatestItemsResponse)\n\terr := c.cc.Invoke(ctx, DataStore_GetLatestItems_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// DataStoreServer is the server API for DataStore service.\n// All implementations must embed UnimplementedDataStoreServer\n// for forward compatibility.\ntype DataStoreServer interface {\n\tPing(context.Context, *PingRequest) (*PingResponse, error)\n\tBatchInsertItems(context.Context, *BatchInsertItemsRequest) (*BatchInsertItemsResponse, error)\n\tBatchGetItems(context.Context, *BatchGetItemsRequest) (*BatchGetItemsResponse, error)\n\tDeleteItem(context.Context, *DeleteItemRequest) (*DeleteItemResponse, error)\n\tGetItem(context.Context, *GetItemRequest) (*GetItemResponse, error)\n\tModifyItem(context.Context, *ModifyItemRequest) (*ModifyItemResponse, error)\n\tGetItems(context.Context, *GetItemsRequest) (*GetItemsResponse, error)\n\tGetItemFeedback(context.Context, *GetItemFeedbackRequest) (*GetFeedbackResponse, error)\n\tBatchInsertUsers(context.Context, *BatchInsertUsersRequest) (*BatchInsertUsersResponse, error)\n\tDeleteUser(context.Context, *DeleteUserRequest) (*DeleteUserResponse, error)\n\tGetUser(context.Context, *GetUserRequest) (*GetUserResponse, error)\n\tModifyUser(context.Context, *ModifyUserRequest) (*ModifyUserResponse, error)\n\tGetUsers(context.Context, *GetUsersRequest) (*GetUsersResponse, error)\n\tGetUserFeedback(context.Context, *GetUserFeedbackRequest) (*GetFeedbackResponse, error)\n\tGetUserItemFeedback(context.Context, *GetUserItemFeedbackRequest) (*GetFeedbackResponse, error)\n\tDeleteUserItemFeedback(context.Context, *DeleteUserItemFeedbackRequest) (*DeleteUserItemFeedbackResponse, error)\n\tBatchInsertFeedback(context.Context, *BatchInsertFeedbackRequest) (*BatchInsertFeedbackResponse, error)\n\tGetFeedback(context.Context, *GetFeedbackRequest) (*GetFeedbackResponse, error)\n\tGetUserStream(*GetUserStreamRequest, grpc.ServerStreamingServer[GetUserStreamResponse]) error\n\tGetItemStream(*GetItemStreamRequest, grpc.ServerStreamingServer[GetItemStreamResponse]) error\n\tGetFeedbackStream(*GetFeedbackStreamRequest, grpc.ServerStreamingServer[GetFeedbackStreamResponse]) error\n\tCountUsers(context.Context, *CountUsersRequest) (*CountUsersResponse, error)\n\tCountItems(context.Context, *CountItemsRequest) (*CountItemsResponse, error)\n\tCountFeedback(context.Context, *CountFeedbackRequest) (*CountFeedbackResponse, error)\n\tGetLatestItems(context.Context, *GetLatestItemsRequest) (*GetLatestItemsResponse, error)\n\tmustEmbedUnimplementedDataStoreServer()\n}\n\n// UnimplementedDataStoreServer must be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedDataStoreServer struct{}\n\nfunc (UnimplementedDataStoreServer) Ping(context.Context, *PingRequest) (*PingResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method Ping not implemented\")\n}\nfunc (UnimplementedDataStoreServer) BatchInsertItems(context.Context, *BatchInsertItemsRequest) (*BatchInsertItemsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method BatchInsertItems not implemented\")\n}\nfunc (UnimplementedDataStoreServer) BatchGetItems(context.Context, *BatchGetItemsRequest) (*BatchGetItemsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method BatchGetItems not implemented\")\n}\nfunc (UnimplementedDataStoreServer) DeleteItem(context.Context, *DeleteItemRequest) (*DeleteItemResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method DeleteItem not implemented\")\n}\nfunc (UnimplementedDataStoreServer) GetItem(context.Context, *GetItemRequest) (*GetItemResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetItem not implemented\")\n}\nfunc (UnimplementedDataStoreServer) ModifyItem(context.Context, *ModifyItemRequest) (*ModifyItemResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method ModifyItem not implemented\")\n}\nfunc (UnimplementedDataStoreServer) GetItems(context.Context, *GetItemsRequest) (*GetItemsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetItems not implemented\")\n}\nfunc (UnimplementedDataStoreServer) GetItemFeedback(context.Context, *GetItemFeedbackRequest) (*GetFeedbackResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetItemFeedback not implemented\")\n}\nfunc (UnimplementedDataStoreServer) BatchInsertUsers(context.Context, *BatchInsertUsersRequest) (*BatchInsertUsersResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method BatchInsertUsers not implemented\")\n}\nfunc (UnimplementedDataStoreServer) DeleteUser(context.Context, *DeleteUserRequest) (*DeleteUserResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method DeleteUser not implemented\")\n}\nfunc (UnimplementedDataStoreServer) GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetUser not implemented\")\n}\nfunc (UnimplementedDataStoreServer) ModifyUser(context.Context, *ModifyUserRequest) (*ModifyUserResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method ModifyUser not implemented\")\n}\nfunc (UnimplementedDataStoreServer) GetUsers(context.Context, *GetUsersRequest) (*GetUsersResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetUsers not implemented\")\n}\nfunc (UnimplementedDataStoreServer) GetUserFeedback(context.Context, *GetUserFeedbackRequest) (*GetFeedbackResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetUserFeedback not implemented\")\n}\nfunc (UnimplementedDataStoreServer) GetUserItemFeedback(context.Context, *GetUserItemFeedbackRequest) (*GetFeedbackResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetUserItemFeedback not implemented\")\n}\nfunc (UnimplementedDataStoreServer) DeleteUserItemFeedback(context.Context, *DeleteUserItemFeedbackRequest) (*DeleteUserItemFeedbackResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method DeleteUserItemFeedback not implemented\")\n}\nfunc (UnimplementedDataStoreServer) BatchInsertFeedback(context.Context, *BatchInsertFeedbackRequest) (*BatchInsertFeedbackResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method BatchInsertFeedback not implemented\")\n}\nfunc (UnimplementedDataStoreServer) GetFeedback(context.Context, *GetFeedbackRequest) (*GetFeedbackResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetFeedback not implemented\")\n}\nfunc (UnimplementedDataStoreServer) GetUserStream(*GetUserStreamRequest, grpc.ServerStreamingServer[GetUserStreamResponse]) error {\n\treturn status.Error(codes.Unimplemented, \"method GetUserStream not implemented\")\n}\nfunc (UnimplementedDataStoreServer) GetItemStream(*GetItemStreamRequest, grpc.ServerStreamingServer[GetItemStreamResponse]) error {\n\treturn status.Error(codes.Unimplemented, \"method GetItemStream not implemented\")\n}\nfunc (UnimplementedDataStoreServer) GetFeedbackStream(*GetFeedbackStreamRequest, grpc.ServerStreamingServer[GetFeedbackStreamResponse]) error {\n\treturn status.Error(codes.Unimplemented, \"method GetFeedbackStream not implemented\")\n}\nfunc (UnimplementedDataStoreServer) CountUsers(context.Context, *CountUsersRequest) (*CountUsersResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method CountUsers not implemented\")\n}\nfunc (UnimplementedDataStoreServer) CountItems(context.Context, *CountItemsRequest) (*CountItemsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method CountItems not implemented\")\n}\nfunc (UnimplementedDataStoreServer) CountFeedback(context.Context, *CountFeedbackRequest) (*CountFeedbackResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method CountFeedback not implemented\")\n}\nfunc (UnimplementedDataStoreServer) GetLatestItems(context.Context, *GetLatestItemsRequest) (*GetLatestItemsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetLatestItems not implemented\")\n}\nfunc (UnimplementedDataStoreServer) mustEmbedUnimplementedDataStoreServer() {}\nfunc (UnimplementedDataStoreServer) testEmbeddedByValue()                   {}\n\n// UnsafeDataStoreServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to DataStoreServer will\n// result in compilation errors.\ntype UnsafeDataStoreServer interface {\n\tmustEmbedUnimplementedDataStoreServer()\n}\n\nfunc RegisterDataStoreServer(s grpc.ServiceRegistrar, srv DataStoreServer) {\n\t// If the following call panics, it indicates UnimplementedDataStoreServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&DataStore_ServiceDesc, srv)\n}\n\nfunc _DataStore_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(PingRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).Ping(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_Ping_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).Ping(ctx, req.(*PingRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_BatchInsertItems_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(BatchInsertItemsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).BatchInsertItems(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_BatchInsertItems_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).BatchInsertItems(ctx, req.(*BatchInsertItemsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_BatchGetItems_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(BatchGetItemsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).BatchGetItems(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_BatchGetItems_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).BatchGetItems(ctx, req.(*BatchGetItemsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_DeleteItem_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DeleteItemRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).DeleteItem(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_DeleteItem_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).DeleteItem(ctx, req.(*DeleteItemRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_GetItem_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetItemRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).GetItem(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_GetItem_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).GetItem(ctx, req.(*GetItemRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_ModifyItem_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ModifyItemRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).ModifyItem(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_ModifyItem_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).ModifyItem(ctx, req.(*ModifyItemRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_GetItems_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetItemsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).GetItems(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_GetItems_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).GetItems(ctx, req.(*GetItemsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_GetItemFeedback_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetItemFeedbackRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).GetItemFeedback(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_GetItemFeedback_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).GetItemFeedback(ctx, req.(*GetItemFeedbackRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_BatchInsertUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(BatchInsertUsersRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).BatchInsertUsers(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_BatchInsertUsers_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).BatchInsertUsers(ctx, req.(*BatchInsertUsersRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_DeleteUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DeleteUserRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).DeleteUser(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_DeleteUser_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).DeleteUser(ctx, req.(*DeleteUserRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_GetUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetUserRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).GetUser(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_GetUser_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).GetUser(ctx, req.(*GetUserRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_ModifyUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ModifyUserRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).ModifyUser(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_ModifyUser_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).ModifyUser(ctx, req.(*ModifyUserRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_GetUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetUsersRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).GetUsers(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_GetUsers_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).GetUsers(ctx, req.(*GetUsersRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_GetUserFeedback_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetUserFeedbackRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).GetUserFeedback(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_GetUserFeedback_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).GetUserFeedback(ctx, req.(*GetUserFeedbackRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_GetUserItemFeedback_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetUserItemFeedbackRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).GetUserItemFeedback(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_GetUserItemFeedback_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).GetUserItemFeedback(ctx, req.(*GetUserItemFeedbackRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_DeleteUserItemFeedback_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DeleteUserItemFeedbackRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).DeleteUserItemFeedback(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_DeleteUserItemFeedback_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).DeleteUserItemFeedback(ctx, req.(*DeleteUserItemFeedbackRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_BatchInsertFeedback_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(BatchInsertFeedbackRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).BatchInsertFeedback(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_BatchInsertFeedback_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).BatchInsertFeedback(ctx, req.(*BatchInsertFeedbackRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_GetFeedback_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetFeedbackRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).GetFeedback(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_GetFeedback_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).GetFeedback(ctx, req.(*GetFeedbackRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_GetUserStream_Handler(srv interface{}, stream grpc.ServerStream) error {\n\tm := new(GetUserStreamRequest)\n\tif err := stream.RecvMsg(m); err != nil {\n\t\treturn err\n\t}\n\treturn srv.(DataStoreServer).GetUserStream(m, &grpc.GenericServerStream[GetUserStreamRequest, GetUserStreamResponse]{ServerStream: stream})\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype DataStore_GetUserStreamServer = grpc.ServerStreamingServer[GetUserStreamResponse]\n\nfunc _DataStore_GetItemStream_Handler(srv interface{}, stream grpc.ServerStream) error {\n\tm := new(GetItemStreamRequest)\n\tif err := stream.RecvMsg(m); err != nil {\n\t\treturn err\n\t}\n\treturn srv.(DataStoreServer).GetItemStream(m, &grpc.GenericServerStream[GetItemStreamRequest, GetItemStreamResponse]{ServerStream: stream})\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype DataStore_GetItemStreamServer = grpc.ServerStreamingServer[GetItemStreamResponse]\n\nfunc _DataStore_GetFeedbackStream_Handler(srv interface{}, stream grpc.ServerStream) error {\n\tm := new(GetFeedbackStreamRequest)\n\tif err := stream.RecvMsg(m); err != nil {\n\t\treturn err\n\t}\n\treturn srv.(DataStoreServer).GetFeedbackStream(m, &grpc.GenericServerStream[GetFeedbackStreamRequest, GetFeedbackStreamResponse]{ServerStream: stream})\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype DataStore_GetFeedbackStreamServer = grpc.ServerStreamingServer[GetFeedbackStreamResponse]\n\nfunc _DataStore_CountUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(CountUsersRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).CountUsers(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_CountUsers_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).CountUsers(ctx, req.(*CountUsersRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_CountItems_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(CountItemsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).CountItems(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_CountItems_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).CountItems(ctx, req.(*CountItemsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_CountFeedback_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(CountFeedbackRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).CountFeedback(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_CountFeedback_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).CountFeedback(ctx, req.(*CountFeedbackRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _DataStore_GetLatestItems_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetLatestItemsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DataStoreServer).GetLatestItems(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: DataStore_GetLatestItems_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DataStoreServer).GetLatestItems(ctx, req.(*GetLatestItemsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// DataStore_ServiceDesc is the grpc.ServiceDesc for DataStore service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar DataStore_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"protocol.DataStore\",\n\tHandlerType: (*DataStoreServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"Ping\",\n\t\t\tHandler:    _DataStore_Ping_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"BatchInsertItems\",\n\t\t\tHandler:    _DataStore_BatchInsertItems_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"BatchGetItems\",\n\t\t\tHandler:    _DataStore_BatchGetItems_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DeleteItem\",\n\t\t\tHandler:    _DataStore_DeleteItem_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetItem\",\n\t\t\tHandler:    _DataStore_GetItem_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"ModifyItem\",\n\t\t\tHandler:    _DataStore_ModifyItem_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetItems\",\n\t\t\tHandler:    _DataStore_GetItems_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetItemFeedback\",\n\t\t\tHandler:    _DataStore_GetItemFeedback_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"BatchInsertUsers\",\n\t\t\tHandler:    _DataStore_BatchInsertUsers_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DeleteUser\",\n\t\t\tHandler:    _DataStore_DeleteUser_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetUser\",\n\t\t\tHandler:    _DataStore_GetUser_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"ModifyUser\",\n\t\t\tHandler:    _DataStore_ModifyUser_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetUsers\",\n\t\t\tHandler:    _DataStore_GetUsers_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetUserFeedback\",\n\t\t\tHandler:    _DataStore_GetUserFeedback_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetUserItemFeedback\",\n\t\t\tHandler:    _DataStore_GetUserItemFeedback_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DeleteUserItemFeedback\",\n\t\t\tHandler:    _DataStore_DeleteUserItemFeedback_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"BatchInsertFeedback\",\n\t\t\tHandler:    _DataStore_BatchInsertFeedback_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetFeedback\",\n\t\t\tHandler:    _DataStore_GetFeedback_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"CountUsers\",\n\t\t\tHandler:    _DataStore_CountUsers_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"CountItems\",\n\t\t\tHandler:    _DataStore_CountItems_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"CountFeedback\",\n\t\t\tHandler:    _DataStore_CountFeedback_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetLatestItems\",\n\t\t\tHandler:    _DataStore_GetLatestItems_Handler,\n\t\t},\n\t},\n\tStreams: []grpc.StreamDesc{\n\t\t{\n\t\t\tStreamName:    \"GetUserStream\",\n\t\t\tHandler:       _DataStore_GetUserStream_Handler,\n\t\t\tServerStreams: true,\n\t\t},\n\t\t{\n\t\t\tStreamName:    \"GetItemStream\",\n\t\t\tHandler:       _DataStore_GetItemStream_Handler,\n\t\t\tServerStreams: true,\n\t\t},\n\t\t{\n\t\t\tStreamName:    \"GetFeedbackStream\",\n\t\t\tHandler:       _DataStore_GetFeedbackStream_Handler,\n\t\t\tServerStreams: true,\n\t\t},\n\t},\n\tMetadata: \"data_store.proto\",\n}\n"
  },
  {
    "path": "protocol/encoding.pb.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.10\n// \tprotoc        v6.33.1\n// source: encoding.proto\n\npackage protocol\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype Tensor struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tKey           []string               `protobuf:\"bytes,1,rep,name=key,proto3\" json:\"key,omitempty\"`\n\tShape         []int32                `protobuf:\"varint,2,rep,packed,name=shape,proto3\" json:\"shape,omitempty\"`\n\tData          []float32              `protobuf:\"fixed32,3,rep,packed,name=data,proto3\" json:\"data,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Tensor) Reset() {\n\t*x = Tensor{}\n\tmi := &file_encoding_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Tensor) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Tensor) ProtoMessage() {}\n\nfunc (x *Tensor) ProtoReflect() protoreflect.Message {\n\tmi := &file_encoding_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Tensor.ProtoReflect.Descriptor instead.\nfunc (*Tensor) Descriptor() ([]byte, []int) {\n\treturn file_encoding_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *Tensor) GetKey() []string {\n\tif x != nil {\n\t\treturn x.Key\n\t}\n\treturn nil\n}\n\nfunc (x *Tensor) GetShape() []int32 {\n\tif x != nil {\n\t\treturn x.Shape\n\t}\n\treturn nil\n}\n\nfunc (x *Tensor) GetData() []float32 {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\ntype LatentFactor struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tData          []float32              `protobuf:\"fixed32,2,rep,packed,name=data,proto3\" json:\"data,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *LatentFactor) Reset() {\n\t*x = LatentFactor{}\n\tmi := &file_encoding_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *LatentFactor) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*LatentFactor) ProtoMessage() {}\n\nfunc (x *LatentFactor) ProtoReflect() protoreflect.Message {\n\tmi := &file_encoding_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use LatentFactor.ProtoReflect.Descriptor instead.\nfunc (*LatentFactor) Descriptor() ([]byte, []int) {\n\treturn file_encoding_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *LatentFactor) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *LatentFactor) GetData() []float32 {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\nvar File_encoding_proto protoreflect.FileDescriptor\n\nconst file_encoding_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x0eencoding.proto\\x12\\bprotocol\\\"D\\n\" +\n\t\"\\x06Tensor\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x03(\\tR\\x03key\\x12\\x14\\n\" +\n\t\"\\x05shape\\x18\\x02 \\x03(\\x05R\\x05shape\\x12\\x12\\n\" +\n\t\"\\x04data\\x18\\x03 \\x03(\\x02R\\x04data\\\"2\\n\" +\n\t\"\\fLatentFactor\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x12\\n\" +\n\t\"\\x04data\\x18\\x02 \\x03(\\x02R\\x04dataB$Z\\\"github.com/gorse-io/gorse/protocolb\\x06proto3\"\n\nvar (\n\tfile_encoding_proto_rawDescOnce sync.Once\n\tfile_encoding_proto_rawDescData []byte\n)\n\nfunc file_encoding_proto_rawDescGZIP() []byte {\n\tfile_encoding_proto_rawDescOnce.Do(func() {\n\t\tfile_encoding_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_encoding_proto_rawDesc), len(file_encoding_proto_rawDesc)))\n\t})\n\treturn file_encoding_proto_rawDescData\n}\n\nvar file_encoding_proto_msgTypes = make([]protoimpl.MessageInfo, 2)\nvar file_encoding_proto_goTypes = []any{\n\t(*Tensor)(nil),       // 0: protocol.Tensor\n\t(*LatentFactor)(nil), // 1: protocol.LatentFactor\n}\nvar file_encoding_proto_depIdxs = []int32{\n\t0, // [0:0] is the sub-list for method output_type\n\t0, // [0:0] is the sub-list for method input_type\n\t0, // [0:0] is the sub-list for extension type_name\n\t0, // [0:0] is the sub-list for extension extendee\n\t0, // [0:0] is the sub-list for field type_name\n}\n\nfunc init() { file_encoding_proto_init() }\nfunc file_encoding_proto_init() {\n\tif File_encoding_proto != nil {\n\t\treturn\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_encoding_proto_rawDesc), len(file_encoding_proto_rawDesc)),\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   2,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_encoding_proto_goTypes,\n\t\tDependencyIndexes: file_encoding_proto_depIdxs,\n\t\tMessageInfos:      file_encoding_proto_msgTypes,\n\t}.Build()\n\tFile_encoding_proto = out.File\n\tfile_encoding_proto_goTypes = nil\n\tfile_encoding_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "protocol/encoding.proto",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nsyntax = \"proto3\";\n\noption go_package = \"github.com/gorse-io/gorse/protocol\";\n\npackage protocol;\n\nmessage Tensor {\n  repeated string key = 1;\n  repeated int32 shape = 2;\n  repeated float data = 3;\n}\n\nmessage LatentFactor {\n  string id = 1;\n  repeated float data = 2;\n}\n"
  },
  {
    "path": "protocol/generate.go",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage protocol\n\n//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative cache_store.proto\n//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative data_store.proto\n//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative vector_store.proto\n//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative encoding.proto\n//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative protocol.proto\n"
  },
  {
    "path": "protocol/protocol.pb.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.10\n// \tprotoc        v6.33.1\n// source: protocol.proto\n\npackage protocol\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\ttimestamppb \"google.golang.org/protobuf/types/known/timestamppb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype NodeType int32\n\nconst (\n\tNodeType_Server NodeType = 0\n\tNodeType_Worker NodeType = 1\n\tNodeType_Client NodeType = 2\n)\n\n// Enum value maps for NodeType.\nvar (\n\tNodeType_name = map[int32]string{\n\t\t0: \"Server\",\n\t\t1: \"Worker\",\n\t\t2: \"Client\",\n\t}\n\tNodeType_value = map[string]int32{\n\t\t\"Server\": 0,\n\t\t\"Worker\": 1,\n\t\t\"Client\": 2,\n\t}\n)\n\nfunc (x NodeType) Enum() *NodeType {\n\tp := new(NodeType)\n\t*p = x\n\treturn p\n}\n\nfunc (x NodeType) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (NodeType) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_protocol_proto_enumTypes[0].Descriptor()\n}\n\nfunc (NodeType) Type() protoreflect.EnumType {\n\treturn &file_protocol_proto_enumTypes[0]\n}\n\nfunc (x NodeType) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use NodeType.Descriptor instead.\nfunc (NodeType) EnumDescriptor() ([]byte, []int) {\n\treturn file_protocol_proto_rawDescGZIP(), []int{0}\n}\n\ntype User struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tUserId        string                 `protobuf:\"bytes,1,opt,name=user_id,json=userId,proto3\" json:\"user_id,omitempty\"`\n\tLabels        []byte                 `protobuf:\"bytes,2,opt,name=labels,proto3\" json:\"labels,omitempty\"`\n\tComment       string                 `protobuf:\"bytes,3,opt,name=comment,proto3\" json:\"comment,omitempty\"`\n\tSubscribe     []string               `protobuf:\"bytes,4,rep,name=subscribe,proto3\" json:\"subscribe,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *User) Reset() {\n\t*x = User{}\n\tmi := &file_protocol_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *User) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*User) ProtoMessage() {}\n\nfunc (x *User) ProtoReflect() protoreflect.Message {\n\tmi := &file_protocol_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use User.ProtoReflect.Descriptor instead.\nfunc (*User) Descriptor() ([]byte, []int) {\n\treturn file_protocol_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *User) GetUserId() string {\n\tif x != nil {\n\t\treturn x.UserId\n\t}\n\treturn \"\"\n}\n\nfunc (x *User) GetLabels() []byte {\n\tif x != nil {\n\t\treturn x.Labels\n\t}\n\treturn nil\n}\n\nfunc (x *User) GetComment() string {\n\tif x != nil {\n\t\treturn x.Comment\n\t}\n\treturn \"\"\n}\n\nfunc (x *User) GetSubscribe() []string {\n\tif x != nil {\n\t\treturn x.Subscribe\n\t}\n\treturn nil\n}\n\ntype Item struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tNamespace     string                 `protobuf:\"bytes,1,opt,name=namespace,proto3\" json:\"namespace,omitempty\"`\n\tItemId        string                 `protobuf:\"bytes,2,opt,name=item_id,json=itemId,proto3\" json:\"item_id,omitempty\"`\n\tIsHidden      bool                   `protobuf:\"varint,3,opt,name=is_hidden,json=isHidden,proto3\" json:\"is_hidden,omitempty\"`\n\tCategories    []string               `protobuf:\"bytes,4,rep,name=categories,proto3\" json:\"categories,omitempty\"`\n\tTimestamp     *timestamppb.Timestamp `protobuf:\"bytes,5,opt,name=timestamp,proto3\" json:\"timestamp,omitempty\"`\n\tLabels        []byte                 `protobuf:\"bytes,6,opt,name=labels,proto3\" json:\"labels,omitempty\"`\n\tComment       string                 `protobuf:\"bytes,7,opt,name=comment,proto3\" json:\"comment,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Item) Reset() {\n\t*x = Item{}\n\tmi := &file_protocol_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Item) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Item) ProtoMessage() {}\n\nfunc (x *Item) ProtoReflect() protoreflect.Message {\n\tmi := &file_protocol_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Item.ProtoReflect.Descriptor instead.\nfunc (*Item) Descriptor() ([]byte, []int) {\n\treturn file_protocol_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *Item) GetNamespace() string {\n\tif x != nil {\n\t\treturn x.Namespace\n\t}\n\treturn \"\"\n}\n\nfunc (x *Item) GetItemId() string {\n\tif x != nil {\n\t\treturn x.ItemId\n\t}\n\treturn \"\"\n}\n\nfunc (x *Item) GetIsHidden() bool {\n\tif x != nil {\n\t\treturn x.IsHidden\n\t}\n\treturn false\n}\n\nfunc (x *Item) GetCategories() []string {\n\tif x != nil {\n\t\treturn x.Categories\n\t}\n\treturn nil\n}\n\nfunc (x *Item) GetTimestamp() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.Timestamp\n\t}\n\treturn nil\n}\n\nfunc (x *Item) GetLabels() []byte {\n\tif x != nil {\n\t\treturn x.Labels\n\t}\n\treturn nil\n}\n\nfunc (x *Item) GetComment() string {\n\tif x != nil {\n\t\treturn x.Comment\n\t}\n\treturn \"\"\n}\n\ntype Feedback struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tNamespace     string                 `protobuf:\"bytes,1,opt,name=namespace,proto3\" json:\"namespace,omitempty\"`\n\tFeedbackType  string                 `protobuf:\"bytes,2,opt,name=feedback_type,json=feedbackType,proto3\" json:\"feedback_type,omitempty\"`\n\tUserId        string                 `protobuf:\"bytes,3,opt,name=user_id,json=userId,proto3\" json:\"user_id,omitempty\"`\n\tItemId        string                 `protobuf:\"bytes,4,opt,name=item_id,json=itemId,proto3\" json:\"item_id,omitempty\"`\n\tValue         float64                `protobuf:\"fixed64,5,opt,name=value,proto3\" json:\"value,omitempty\"`\n\tTimestamp     *timestamppb.Timestamp `protobuf:\"bytes,6,opt,name=timestamp,proto3\" json:\"timestamp,omitempty\"`\n\tComment       string                 `protobuf:\"bytes,7,opt,name=comment,proto3\" json:\"comment,omitempty\"`\n\tUpdated       *timestamppb.Timestamp `protobuf:\"bytes,8,opt,name=updated,proto3\" json:\"updated,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Feedback) Reset() {\n\t*x = Feedback{}\n\tmi := &file_protocol_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Feedback) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Feedback) ProtoMessage() {}\n\nfunc (x *Feedback) ProtoReflect() protoreflect.Message {\n\tmi := &file_protocol_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Feedback.ProtoReflect.Descriptor instead.\nfunc (*Feedback) Descriptor() ([]byte, []int) {\n\treturn file_protocol_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *Feedback) GetNamespace() string {\n\tif x != nil {\n\t\treturn x.Namespace\n\t}\n\treturn \"\"\n}\n\nfunc (x *Feedback) GetFeedbackType() string {\n\tif x != nil {\n\t\treturn x.FeedbackType\n\t}\n\treturn \"\"\n}\n\nfunc (x *Feedback) GetUserId() string {\n\tif x != nil {\n\t\treturn x.UserId\n\t}\n\treturn \"\"\n}\n\nfunc (x *Feedback) GetItemId() string {\n\tif x != nil {\n\t\treturn x.ItemId\n\t}\n\treturn \"\"\n}\n\nfunc (x *Feedback) GetValue() float64 {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn 0\n}\n\nfunc (x *Feedback) GetTimestamp() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.Timestamp\n\t}\n\treturn nil\n}\n\nfunc (x *Feedback) GetComment() string {\n\tif x != nil {\n\t\treturn x.Comment\n\t}\n\treturn \"\"\n}\n\nfunc (x *Feedback) GetUpdated() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.Updated\n\t}\n\treturn nil\n}\n\ntype Meta struct {\n\tstate                         protoimpl.MessageState `protogen:\"open.v1\"`\n\tConfig                        string                 `protobuf:\"bytes,1,opt,name=config,proto3\" json:\"config,omitempty\"`\n\tCollaborativeFilteringModelId int64                  `protobuf:\"varint,3,opt,name=collaborative_filtering_model_id,json=collaborativeFilteringModelId,proto3\" json:\"collaborative_filtering_model_id,omitempty\"`\n\tClickThroughRateModelId       int64                  `protobuf:\"varint,4,opt,name=click_through_rate_model_id,json=clickThroughRateModelId,proto3\" json:\"click_through_rate_model_id,omitempty\"`\n\tMe                            string                 `protobuf:\"bytes,5,opt,name=me,proto3\" json:\"me,omitempty\"`\n\tServers                       []string               `protobuf:\"bytes,6,rep,name=servers,proto3\" json:\"servers,omitempty\"`\n\tWorkers                       []string               `protobuf:\"bytes,7,rep,name=workers,proto3\" json:\"workers,omitempty\"`\n\tunknownFields                 protoimpl.UnknownFields\n\tsizeCache                     protoimpl.SizeCache\n}\n\nfunc (x *Meta) Reset() {\n\t*x = Meta{}\n\tmi := &file_protocol_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Meta) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Meta) ProtoMessage() {}\n\nfunc (x *Meta) ProtoReflect() protoreflect.Message {\n\tmi := &file_protocol_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Meta.ProtoReflect.Descriptor instead.\nfunc (*Meta) Descriptor() ([]byte, []int) {\n\treturn file_protocol_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *Meta) GetConfig() string {\n\tif x != nil {\n\t\treturn x.Config\n\t}\n\treturn \"\"\n}\n\nfunc (x *Meta) GetCollaborativeFilteringModelId() int64 {\n\tif x != nil {\n\t\treturn x.CollaborativeFilteringModelId\n\t}\n\treturn 0\n}\n\nfunc (x *Meta) GetClickThroughRateModelId() int64 {\n\tif x != nil {\n\t\treturn x.ClickThroughRateModelId\n\t}\n\treturn 0\n}\n\nfunc (x *Meta) GetMe() string {\n\tif x != nil {\n\t\treturn x.Me\n\t}\n\treturn \"\"\n}\n\nfunc (x *Meta) GetServers() []string {\n\tif x != nil {\n\t\treturn x.Servers\n\t}\n\treturn nil\n}\n\nfunc (x *Meta) GetWorkers() []string {\n\tif x != nil {\n\t\treturn x.Workers\n\t}\n\treturn nil\n}\n\ntype Fragment struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tData          []byte                 `protobuf:\"bytes,1,opt,name=data,proto3\" json:\"data,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Fragment) Reset() {\n\t*x = Fragment{}\n\tmi := &file_protocol_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Fragment) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Fragment) ProtoMessage() {}\n\nfunc (x *Fragment) ProtoReflect() protoreflect.Message {\n\tmi := &file_protocol_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Fragment.ProtoReflect.Descriptor instead.\nfunc (*Fragment) Descriptor() ([]byte, []int) {\n\treturn file_protocol_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *Fragment) GetData() []byte {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\ntype NodeInfo struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tNodeType      NodeType               `protobuf:\"varint,1,opt,name=node_type,json=nodeType,proto3,enum=protocol.NodeType\" json:\"node_type,omitempty\"`\n\tUuid          string                 `protobuf:\"bytes,2,opt,name=uuid,proto3\" json:\"uuid,omitempty\"`\n\tBinaryVersion string                 `protobuf:\"bytes,4,opt,name=binary_version,json=binaryVersion,proto3\" json:\"binary_version,omitempty\"`\n\tHostname      string                 `protobuf:\"bytes,5,opt,name=hostname,proto3\" json:\"hostname,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *NodeInfo) Reset() {\n\t*x = NodeInfo{}\n\tmi := &file_protocol_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *NodeInfo) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*NodeInfo) ProtoMessage() {}\n\nfunc (x *NodeInfo) ProtoReflect() protoreflect.Message {\n\tmi := &file_protocol_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use NodeInfo.ProtoReflect.Descriptor instead.\nfunc (*NodeInfo) Descriptor() ([]byte, []int) {\n\treturn file_protocol_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *NodeInfo) GetNodeType() NodeType {\n\tif x != nil {\n\t\treturn x.NodeType\n\t}\n\treturn NodeType_Server\n}\n\nfunc (x *NodeInfo) GetUuid() string {\n\tif x != nil {\n\t\treturn x.Uuid\n\t}\n\treturn \"\"\n}\n\nfunc (x *NodeInfo) GetBinaryVersion() string {\n\tif x != nil {\n\t\treturn x.BinaryVersion\n\t}\n\treturn \"\"\n}\n\nfunc (x *NodeInfo) GetHostname() string {\n\tif x != nil {\n\t\treturn x.Hostname\n\t}\n\treturn \"\"\n}\n\ntype Progress struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTracer        string                 `protobuf:\"bytes,1,opt,name=tracer,proto3\" json:\"tracer,omitempty\"`\n\tName          string                 `protobuf:\"bytes,2,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tStatus        string                 `protobuf:\"bytes,3,opt,name=status,proto3\" json:\"status,omitempty\"`\n\tError         string                 `protobuf:\"bytes,4,opt,name=error,proto3\" json:\"error,omitempty\"`\n\tCount         int64                  `protobuf:\"varint,5,opt,name=count,proto3\" json:\"count,omitempty\"`\n\tTotal         int64                  `protobuf:\"varint,6,opt,name=total,proto3\" json:\"total,omitempty\"`\n\tStartTime     int64                  `protobuf:\"varint,7,opt,name=start_time,json=startTime,proto3\" json:\"start_time,omitempty\"`\n\tFinishTime    int64                  `protobuf:\"varint,8,opt,name=finish_time,json=finishTime,proto3\" json:\"finish_time,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Progress) Reset() {\n\t*x = Progress{}\n\tmi := &file_protocol_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Progress) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Progress) ProtoMessage() {}\n\nfunc (x *Progress) ProtoReflect() protoreflect.Message {\n\tmi := &file_protocol_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Progress.ProtoReflect.Descriptor instead.\nfunc (*Progress) Descriptor() ([]byte, []int) {\n\treturn file_protocol_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *Progress) GetTracer() string {\n\tif x != nil {\n\t\treturn x.Tracer\n\t}\n\treturn \"\"\n}\n\nfunc (x *Progress) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *Progress) GetStatus() string {\n\tif x != nil {\n\t\treturn x.Status\n\t}\n\treturn \"\"\n}\n\nfunc (x *Progress) GetError() string {\n\tif x != nil {\n\t\treturn x.Error\n\t}\n\treturn \"\"\n}\n\nfunc (x *Progress) GetCount() int64 {\n\tif x != nil {\n\t\treturn x.Count\n\t}\n\treturn 0\n}\n\nfunc (x *Progress) GetTotal() int64 {\n\tif x != nil {\n\t\treturn x.Total\n\t}\n\treturn 0\n}\n\nfunc (x *Progress) GetStartTime() int64 {\n\tif x != nil {\n\t\treturn x.StartTime\n\t}\n\treturn 0\n}\n\nfunc (x *Progress) GetFinishTime() int64 {\n\tif x != nil {\n\t\treturn x.FinishTime\n\t}\n\treturn 0\n}\n\ntype PushProgressRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tProgress      []*Progress            `protobuf:\"bytes,1,rep,name=progress,proto3\" json:\"progress,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PushProgressRequest) Reset() {\n\t*x = PushProgressRequest{}\n\tmi := &file_protocol_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PushProgressRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PushProgressRequest) ProtoMessage() {}\n\nfunc (x *PushProgressRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_protocol_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PushProgressRequest.ProtoReflect.Descriptor instead.\nfunc (*PushProgressRequest) Descriptor() ([]byte, []int) {\n\treturn file_protocol_proto_rawDescGZIP(), []int{7}\n}\n\nfunc (x *PushProgressRequest) GetProgress() []*Progress {\n\tif x != nil {\n\t\treturn x.Progress\n\t}\n\treturn nil\n}\n\ntype PushProgressResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PushProgressResponse) Reset() {\n\t*x = PushProgressResponse{}\n\tmi := &file_protocol_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PushProgressResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PushProgressResponse) ProtoMessage() {}\n\nfunc (x *PushProgressResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_protocol_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PushProgressResponse.ProtoReflect.Descriptor instead.\nfunc (*PushProgressResponse) Descriptor() ([]byte, []int) {\n\treturn file_protocol_proto_rawDescGZIP(), []int{8}\n}\n\ntype PingRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PingRequest) Reset() {\n\t*x = PingRequest{}\n\tmi := &file_protocol_proto_msgTypes[9]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PingRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PingRequest) ProtoMessage() {}\n\nfunc (x *PingRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_protocol_proto_msgTypes[9]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PingRequest.ProtoReflect.Descriptor instead.\nfunc (*PingRequest) Descriptor() ([]byte, []int) {\n\treturn file_protocol_proto_rawDescGZIP(), []int{9}\n}\n\ntype PingResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PingResponse) Reset() {\n\t*x = PingResponse{}\n\tmi := &file_protocol_proto_msgTypes[10]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PingResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PingResponse) ProtoMessage() {}\n\nfunc (x *PingResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_protocol_proto_msgTypes[10]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PingResponse.ProtoReflect.Descriptor instead.\nfunc (*PingResponse) Descriptor() ([]byte, []int) {\n\treturn file_protocol_proto_rawDescGZIP(), []int{10}\n}\n\ntype UploadBlobRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tTimestamp     *timestamppb.Timestamp `protobuf:\"bytes,2,opt,name=timestamp,proto3\" json:\"timestamp,omitempty\"`\n\tData          []byte                 `protobuf:\"bytes,3,opt,name=data,proto3\" json:\"data,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UploadBlobRequest) Reset() {\n\t*x = UploadBlobRequest{}\n\tmi := &file_protocol_proto_msgTypes[11]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UploadBlobRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UploadBlobRequest) ProtoMessage() {}\n\nfunc (x *UploadBlobRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_protocol_proto_msgTypes[11]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UploadBlobRequest.ProtoReflect.Descriptor instead.\nfunc (*UploadBlobRequest) Descriptor() ([]byte, []int) {\n\treturn file_protocol_proto_rawDescGZIP(), []int{11}\n}\n\nfunc (x *UploadBlobRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *UploadBlobRequest) GetTimestamp() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.Timestamp\n\t}\n\treturn nil\n}\n\nfunc (x *UploadBlobRequest) GetData() []byte {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\ntype UploadBlobResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UploadBlobResponse) Reset() {\n\t*x = UploadBlobResponse{}\n\tmi := &file_protocol_proto_msgTypes[12]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UploadBlobResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UploadBlobResponse) ProtoMessage() {}\n\nfunc (x *UploadBlobResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_protocol_proto_msgTypes[12]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UploadBlobResponse.ProtoReflect.Descriptor instead.\nfunc (*UploadBlobResponse) Descriptor() ([]byte, []int) {\n\treturn file_protocol_proto_rawDescGZIP(), []int{12}\n}\n\ntype DownloadBlobRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DownloadBlobRequest) Reset() {\n\t*x = DownloadBlobRequest{}\n\tmi := &file_protocol_proto_msgTypes[13]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DownloadBlobRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DownloadBlobRequest) ProtoMessage() {}\n\nfunc (x *DownloadBlobRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_protocol_proto_msgTypes[13]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DownloadBlobRequest.ProtoReflect.Descriptor instead.\nfunc (*DownloadBlobRequest) Descriptor() ([]byte, []int) {\n\treturn file_protocol_proto_rawDescGZIP(), []int{13}\n}\n\nfunc (x *DownloadBlobRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\ntype DownloadBlobResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tData          []byte                 `protobuf:\"bytes,1,opt,name=data,proto3\" json:\"data,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DownloadBlobResponse) Reset() {\n\t*x = DownloadBlobResponse{}\n\tmi := &file_protocol_proto_msgTypes[14]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DownloadBlobResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DownloadBlobResponse) ProtoMessage() {}\n\nfunc (x *DownloadBlobResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_protocol_proto_msgTypes[14]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DownloadBlobResponse.ProtoReflect.Descriptor instead.\nfunc (*DownloadBlobResponse) Descriptor() ([]byte, []int) {\n\treturn file_protocol_proto_rawDescGZIP(), []int{14}\n}\n\nfunc (x *DownloadBlobResponse) GetData() []byte {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\ntype ListBlobsRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListBlobsRequest) Reset() {\n\t*x = ListBlobsRequest{}\n\tmi := &file_protocol_proto_msgTypes[15]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListBlobsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListBlobsRequest) ProtoMessage() {}\n\nfunc (x *ListBlobsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_protocol_proto_msgTypes[15]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListBlobsRequest.ProtoReflect.Descriptor instead.\nfunc (*ListBlobsRequest) Descriptor() ([]byte, []int) {\n\treturn file_protocol_proto_rawDescGZIP(), []int{15}\n}\n\ntype ListBlobsResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tNames         []string               `protobuf:\"bytes,1,rep,name=names,proto3\" json:\"names,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListBlobsResponse) Reset() {\n\t*x = ListBlobsResponse{}\n\tmi := &file_protocol_proto_msgTypes[16]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListBlobsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListBlobsResponse) ProtoMessage() {}\n\nfunc (x *ListBlobsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_protocol_proto_msgTypes[16]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListBlobsResponse.ProtoReflect.Descriptor instead.\nfunc (*ListBlobsResponse) Descriptor() ([]byte, []int) {\n\treturn file_protocol_proto_rawDescGZIP(), []int{16}\n}\n\nfunc (x *ListBlobsResponse) GetNames() []string {\n\tif x != nil {\n\t\treturn x.Names\n\t}\n\treturn nil\n}\n\ntype RemoveBlobRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RemoveBlobRequest) Reset() {\n\t*x = RemoveBlobRequest{}\n\tmi := &file_protocol_proto_msgTypes[17]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RemoveBlobRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RemoveBlobRequest) ProtoMessage() {}\n\nfunc (x *RemoveBlobRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_protocol_proto_msgTypes[17]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RemoveBlobRequest.ProtoReflect.Descriptor instead.\nfunc (*RemoveBlobRequest) Descriptor() ([]byte, []int) {\n\treturn file_protocol_proto_rawDescGZIP(), []int{17}\n}\n\nfunc (x *RemoveBlobRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\ntype RemoveBlobResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RemoveBlobResponse) Reset() {\n\t*x = RemoveBlobResponse{}\n\tmi := &file_protocol_proto_msgTypes[18]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RemoveBlobResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RemoveBlobResponse) ProtoMessage() {}\n\nfunc (x *RemoveBlobResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_protocol_proto_msgTypes[18]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RemoveBlobResponse.ProtoReflect.Descriptor instead.\nfunc (*RemoveBlobResponse) Descriptor() ([]byte, []int) {\n\treturn file_protocol_proto_rawDescGZIP(), []int{18}\n}\n\nvar File_protocol_proto protoreflect.FileDescriptor\n\nconst file_protocol_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x0eprotocol.proto\\x12\\bprotocol\\x1a\\x1fgoogle/protobuf/timestamp.proto\\\"o\\n\" +\n\t\"\\x04User\\x12\\x17\\n\" +\n\t\"\\auser_id\\x18\\x01 \\x01(\\tR\\x06userId\\x12\\x16\\n\" +\n\t\"\\x06labels\\x18\\x02 \\x01(\\fR\\x06labels\\x12\\x18\\n\" +\n\t\"\\acomment\\x18\\x03 \\x01(\\tR\\acomment\\x12\\x1c\\n\" +\n\t\"\\tsubscribe\\x18\\x04 \\x03(\\tR\\tsubscribe\\\"\\xe6\\x01\\n\" +\n\t\"\\x04Item\\x12\\x1c\\n\" +\n\t\"\\tnamespace\\x18\\x01 \\x01(\\tR\\tnamespace\\x12\\x17\\n\" +\n\t\"\\aitem_id\\x18\\x02 \\x01(\\tR\\x06itemId\\x12\\x1b\\n\" +\n\t\"\\tis_hidden\\x18\\x03 \\x01(\\bR\\bisHidden\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"categories\\x18\\x04 \\x03(\\tR\\n\" +\n\t\"categories\\x128\\n\" +\n\t\"\\ttimestamp\\x18\\x05 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\ttimestamp\\x12\\x16\\n\" +\n\t\"\\x06labels\\x18\\x06 \\x01(\\fR\\x06labels\\x12\\x18\\n\" +\n\t\"\\acomment\\x18\\a \\x01(\\tR\\acomment\\\"\\x9f\\x02\\n\" +\n\t\"\\bFeedback\\x12\\x1c\\n\" +\n\t\"\\tnamespace\\x18\\x01 \\x01(\\tR\\tnamespace\\x12#\\n\" +\n\t\"\\rfeedback_type\\x18\\x02 \\x01(\\tR\\ffeedbackType\\x12\\x17\\n\" +\n\t\"\\auser_id\\x18\\x03 \\x01(\\tR\\x06userId\\x12\\x17\\n\" +\n\t\"\\aitem_id\\x18\\x04 \\x01(\\tR\\x06itemId\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x05 \\x01(\\x01R\\x05value\\x128\\n\" +\n\t\"\\ttimestamp\\x18\\x06 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\ttimestamp\\x12\\x18\\n\" +\n\t\"\\acomment\\x18\\a \\x01(\\tR\\acomment\\x124\\n\" +\n\t\"\\aupdated\\x18\\b \\x01(\\v2\\x1a.google.protobuf.TimestampR\\aupdated\\\"\\xe9\\x01\\n\" +\n\t\"\\x04Meta\\x12\\x16\\n\" +\n\t\"\\x06config\\x18\\x01 \\x01(\\tR\\x06config\\x12G\\n\" +\n\t\" collaborative_filtering_model_id\\x18\\x03 \\x01(\\x03R\\x1dcollaborativeFilteringModelId\\x12<\\n\" +\n\t\"\\x1bclick_through_rate_model_id\\x18\\x04 \\x01(\\x03R\\x17clickThroughRateModelId\\x12\\x0e\\n\" +\n\t\"\\x02me\\x18\\x05 \\x01(\\tR\\x02me\\x12\\x18\\n\" +\n\t\"\\aservers\\x18\\x06 \\x03(\\tR\\aservers\\x12\\x18\\n\" +\n\t\"\\aworkers\\x18\\a \\x03(\\tR\\aworkers\\\"\\x1e\\n\" +\n\t\"\\bFragment\\x12\\x12\\n\" +\n\t\"\\x04data\\x18\\x01 \\x01(\\fR\\x04data\\\"\\x92\\x01\\n\" +\n\t\"\\bNodeInfo\\x12/\\n\" +\n\t\"\\tnode_type\\x18\\x01 \\x01(\\x0e2\\x12.protocol.NodeTypeR\\bnodeType\\x12\\x12\\n\" +\n\t\"\\x04uuid\\x18\\x02 \\x01(\\tR\\x04uuid\\x12%\\n\" +\n\t\"\\x0ebinary_version\\x18\\x04 \\x01(\\tR\\rbinaryVersion\\x12\\x1a\\n\" +\n\t\"\\bhostname\\x18\\x05 \\x01(\\tR\\bhostname\\\"\\xd0\\x01\\n\" +\n\t\"\\bProgress\\x12\\x16\\n\" +\n\t\"\\x06tracer\\x18\\x01 \\x01(\\tR\\x06tracer\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x02 \\x01(\\tR\\x04name\\x12\\x16\\n\" +\n\t\"\\x06status\\x18\\x03 \\x01(\\tR\\x06status\\x12\\x14\\n\" +\n\t\"\\x05error\\x18\\x04 \\x01(\\tR\\x05error\\x12\\x14\\n\" +\n\t\"\\x05count\\x18\\x05 \\x01(\\x03R\\x05count\\x12\\x14\\n\" +\n\t\"\\x05total\\x18\\x06 \\x01(\\x03R\\x05total\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"start_time\\x18\\a \\x01(\\x03R\\tstartTime\\x12\\x1f\\n\" +\n\t\"\\vfinish_time\\x18\\b \\x01(\\x03R\\n\" +\n\t\"finishTime\\\"E\\n\" +\n\t\"\\x13PushProgressRequest\\x12.\\n\" +\n\t\"\\bprogress\\x18\\x01 \\x03(\\v2\\x12.protocol.ProgressR\\bprogress\\\"\\x16\\n\" +\n\t\"\\x14PushProgressResponse\\\"\\r\\n\" +\n\t\"\\vPingRequest\\\"\\x0e\\n\" +\n\t\"\\fPingResponse\\\"u\\n\" +\n\t\"\\x11UploadBlobRequest\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x128\\n\" +\n\t\"\\ttimestamp\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\ttimestamp\\x12\\x12\\n\" +\n\t\"\\x04data\\x18\\x03 \\x01(\\fR\\x04data\\\"\\x14\\n\" +\n\t\"\\x12UploadBlobResponse\\\")\\n\" +\n\t\"\\x13DownloadBlobRequest\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\\"*\\n\" +\n\t\"\\x14DownloadBlobResponse\\x12\\x12\\n\" +\n\t\"\\x04data\\x18\\x01 \\x01(\\fR\\x04data\\\"\\x12\\n\" +\n\t\"\\x10ListBlobsRequest\\\")\\n\" +\n\t\"\\x11ListBlobsResponse\\x12\\x14\\n\" +\n\t\"\\x05names\\x18\\x01 \\x03(\\tR\\x05names\\\"'\\n\" +\n\t\"\\x11RemoveBlobRequest\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\\"\\x14\\n\" +\n\t\"\\x12RemoveBlobResponse*.\\n\" +\n\t\"\\bNodeType\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06Server\\x10\\x00\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06Worker\\x10\\x01\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06Client\\x10\\x022\\x8a\\x01\\n\" +\n\t\"\\x06Master\\x12/\\n\" +\n\t\"\\aGetMeta\\x12\\x12.protocol.NodeInfo\\x1a\\x0e.protocol.Meta\\\"\\x00\\x12O\\n\" +\n\t\"\\fPushProgress\\x12\\x1d.protocol.PushProgressRequest\\x1a\\x1e.protocol.PushProgressResponse\\\"\\x002\\xbe\\x02\\n\" +\n\t\"\\tBlobStore\\x12K\\n\" +\n\t\"\\n\" +\n\t\"UploadBlob\\x12\\x1b.protocol.UploadBlobRequest\\x1a\\x1c.protocol.UploadBlobResponse\\\"\\x00(\\x01\\x12Q\\n\" +\n\t\"\\fDownloadBlob\\x12\\x1d.protocol.DownloadBlobRequest\\x1a\\x1e.protocol.DownloadBlobResponse\\\"\\x000\\x01\\x12F\\n\" +\n\t\"\\tListBlobs\\x12\\x1a.protocol.ListBlobsRequest\\x1a\\x1b.protocol.ListBlobsResponse\\\"\\x00\\x12I\\n\" +\n\t\"\\n\" +\n\t\"RemoveBlob\\x12\\x1b.protocol.RemoveBlobRequest\\x1a\\x1c.protocol.RemoveBlobResponse\\\"\\x00B$Z\\\"github.com/gorse-io/gorse/protocolb\\x06proto3\"\n\nvar (\n\tfile_protocol_proto_rawDescOnce sync.Once\n\tfile_protocol_proto_rawDescData []byte\n)\n\nfunc file_protocol_proto_rawDescGZIP() []byte {\n\tfile_protocol_proto_rawDescOnce.Do(func() {\n\t\tfile_protocol_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_protocol_proto_rawDesc), len(file_protocol_proto_rawDesc)))\n\t})\n\treturn file_protocol_proto_rawDescData\n}\n\nvar file_protocol_proto_enumTypes = make([]protoimpl.EnumInfo, 1)\nvar file_protocol_proto_msgTypes = make([]protoimpl.MessageInfo, 19)\nvar file_protocol_proto_goTypes = []any{\n\t(NodeType)(0),                 // 0: protocol.NodeType\n\t(*User)(nil),                  // 1: protocol.User\n\t(*Item)(nil),                  // 2: protocol.Item\n\t(*Feedback)(nil),              // 3: protocol.Feedback\n\t(*Meta)(nil),                  // 4: protocol.Meta\n\t(*Fragment)(nil),              // 5: protocol.Fragment\n\t(*NodeInfo)(nil),              // 6: protocol.NodeInfo\n\t(*Progress)(nil),              // 7: protocol.Progress\n\t(*PushProgressRequest)(nil),   // 8: protocol.PushProgressRequest\n\t(*PushProgressResponse)(nil),  // 9: protocol.PushProgressResponse\n\t(*PingRequest)(nil),           // 10: protocol.PingRequest\n\t(*PingResponse)(nil),          // 11: protocol.PingResponse\n\t(*UploadBlobRequest)(nil),     // 12: protocol.UploadBlobRequest\n\t(*UploadBlobResponse)(nil),    // 13: protocol.UploadBlobResponse\n\t(*DownloadBlobRequest)(nil),   // 14: protocol.DownloadBlobRequest\n\t(*DownloadBlobResponse)(nil),  // 15: protocol.DownloadBlobResponse\n\t(*ListBlobsRequest)(nil),      // 16: protocol.ListBlobsRequest\n\t(*ListBlobsResponse)(nil),     // 17: protocol.ListBlobsResponse\n\t(*RemoveBlobRequest)(nil),     // 18: protocol.RemoveBlobRequest\n\t(*RemoveBlobResponse)(nil),    // 19: protocol.RemoveBlobResponse\n\t(*timestamppb.Timestamp)(nil), // 20: google.protobuf.Timestamp\n}\nvar file_protocol_proto_depIdxs = []int32{\n\t20, // 0: protocol.Item.timestamp:type_name -> google.protobuf.Timestamp\n\t20, // 1: protocol.Feedback.timestamp:type_name -> google.protobuf.Timestamp\n\t20, // 2: protocol.Feedback.updated:type_name -> google.protobuf.Timestamp\n\t0,  // 3: protocol.NodeInfo.node_type:type_name -> protocol.NodeType\n\t7,  // 4: protocol.PushProgressRequest.progress:type_name -> protocol.Progress\n\t20, // 5: protocol.UploadBlobRequest.timestamp:type_name -> google.protobuf.Timestamp\n\t6,  // 6: protocol.Master.GetMeta:input_type -> protocol.NodeInfo\n\t8,  // 7: protocol.Master.PushProgress:input_type -> protocol.PushProgressRequest\n\t12, // 8: protocol.BlobStore.UploadBlob:input_type -> protocol.UploadBlobRequest\n\t14, // 9: protocol.BlobStore.DownloadBlob:input_type -> protocol.DownloadBlobRequest\n\t16, // 10: protocol.BlobStore.ListBlobs:input_type -> protocol.ListBlobsRequest\n\t18, // 11: protocol.BlobStore.RemoveBlob:input_type -> protocol.RemoveBlobRequest\n\t4,  // 12: protocol.Master.GetMeta:output_type -> protocol.Meta\n\t9,  // 13: protocol.Master.PushProgress:output_type -> protocol.PushProgressResponse\n\t13, // 14: protocol.BlobStore.UploadBlob:output_type -> protocol.UploadBlobResponse\n\t15, // 15: protocol.BlobStore.DownloadBlob:output_type -> protocol.DownloadBlobResponse\n\t17, // 16: protocol.BlobStore.ListBlobs:output_type -> protocol.ListBlobsResponse\n\t19, // 17: protocol.BlobStore.RemoveBlob:output_type -> protocol.RemoveBlobResponse\n\t12, // [12:18] is the sub-list for method output_type\n\t6,  // [6:12] is the sub-list for method input_type\n\t6,  // [6:6] is the sub-list for extension type_name\n\t6,  // [6:6] is the sub-list for extension extendee\n\t0,  // [0:6] is the sub-list for field type_name\n}\n\nfunc init() { file_protocol_proto_init() }\nfunc file_protocol_proto_init() {\n\tif File_protocol_proto != nil {\n\t\treturn\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_protocol_proto_rawDesc), len(file_protocol_proto_rawDesc)),\n\t\t\tNumEnums:      1,\n\t\t\tNumMessages:   19,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   2,\n\t\t},\n\t\tGoTypes:           file_protocol_proto_goTypes,\n\t\tDependencyIndexes: file_protocol_proto_depIdxs,\n\t\tEnumInfos:         file_protocol_proto_enumTypes,\n\t\tMessageInfos:      file_protocol_proto_msgTypes,\n\t}.Build()\n\tFile_protocol_proto = out.File\n\tfile_protocol_proto_goTypes = nil\n\tfile_protocol_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "protocol/protocol.proto",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nsyntax = \"proto3\";\n\noption go_package = \"github.com/gorse-io/gorse/protocol\";\n\npackage protocol;\n\nimport \"google/protobuf/timestamp.proto\";\n\nmessage User {\n  string user_id = 1;\n  bytes labels = 2;\n  string comment = 3;\n  repeated string subscribe = 4;\n}\n\nmessage Item {\n  string namespace = 1;\n  string item_id = 2;\n  bool is_hidden = 3;\n  repeated string categories = 4;\n  google.protobuf.Timestamp timestamp = 5;\n  bytes labels = 6;\n  string comment = 7;\n}\n\nmessage Feedback {\n  string namespace = 1;\n  string feedback_type = 2;\n  string user_id = 3;\n  string item_id = 4;\n  double value = 5;\n  google.protobuf.Timestamp timestamp = 6;\n  string comment = 7;\n  google.protobuf.Timestamp updated = 8;\n}\n\nenum NodeType {\n  Server = 0;\n  Worker = 1;\n  Client = 2;\n}\n\nservice Master {\n\n  /* meta distribute */\n  rpc GetMeta(NodeInfo) returns (Meta) {}\n  rpc PushProgress(PushProgressRequest) returns (PushProgressResponse) {}\n}\n\nmessage Meta {\n  string config = 1;\n  int64 collaborative_filtering_model_id = 3;\n  int64 click_through_rate_model_id = 4;\n  string me = 5;\n  repeated string servers = 6;\n  repeated string workers = 7;\n}\n\nmessage Fragment { bytes data = 1; }\n\nmessage NodeInfo {\n  NodeType node_type = 1;\n  string uuid = 2;\n  string binary_version = 4;\n  string hostname = 5;\n}\n\nmessage Progress {\n  string tracer = 1;\n  string name = 2;\n  string status = 3;\n  string error = 4;\n  int64 count = 5;\n  int64 total = 6;\n  int64 start_time = 7;\n  int64 finish_time = 8;\n}\n\nmessage PushProgressRequest { repeated Progress progress = 1; }\n\nmessage PushProgressResponse {}\n\nmessage PingRequest {}\n\nmessage PingResponse {}\n\nmessage UploadBlobRequest {\n  string name = 1;\n  google.protobuf.Timestamp timestamp = 2;\n  bytes data = 3;\n}\n\nmessage UploadBlobResponse {}\n\nmessage DownloadBlobRequest { string name = 1; }\n\nmessage DownloadBlobResponse { bytes data = 1; }\n\nmessage ListBlobsRequest {}\n\nmessage ListBlobsResponse {\n  repeated string names = 1;\n}\n\nmessage RemoveBlobRequest {\n  string name = 1;\n}\n\nmessage RemoveBlobResponse {}\n\nservice BlobStore {\n  rpc UploadBlob(stream UploadBlobRequest) returns (UploadBlobResponse) {}\n  rpc DownloadBlob(DownloadBlobRequest) returns (stream DownloadBlobResponse) {}\n  rpc ListBlobs(ListBlobsRequest) returns (ListBlobsResponse) {}\n  rpc RemoveBlob(RemoveBlobRequest) returns (RemoveBlobResponse) {}\n}\n"
  },
  {
    "path": "protocol/protocol_grpc.pb.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.6.0\n// - protoc             v6.33.1\n// source: protocol.proto\n\npackage protocol\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.64.0 or later.\nconst _ = grpc.SupportPackageIsVersion9\n\nconst (\n\tMaster_GetMeta_FullMethodName      = \"/protocol.Master/GetMeta\"\n\tMaster_PushProgress_FullMethodName = \"/protocol.Master/PushProgress\"\n)\n\n// MasterClient is the client API for Master service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype MasterClient interface {\n\t// meta distribute\n\tGetMeta(ctx context.Context, in *NodeInfo, opts ...grpc.CallOption) (*Meta, error)\n\tPushProgress(ctx context.Context, in *PushProgressRequest, opts ...grpc.CallOption) (*PushProgressResponse, error)\n}\n\ntype masterClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewMasterClient(cc grpc.ClientConnInterface) MasterClient {\n\treturn &masterClient{cc}\n}\n\nfunc (c *masterClient) GetMeta(ctx context.Context, in *NodeInfo, opts ...grpc.CallOption) (*Meta, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Meta)\n\terr := c.cc.Invoke(ctx, Master_GetMeta_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *masterClient) PushProgress(ctx context.Context, in *PushProgressRequest, opts ...grpc.CallOption) (*PushProgressResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(PushProgressResponse)\n\terr := c.cc.Invoke(ctx, Master_PushProgress_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// MasterServer is the server API for Master service.\n// All implementations must embed UnimplementedMasterServer\n// for forward compatibility.\ntype MasterServer interface {\n\t// meta distribute\n\tGetMeta(context.Context, *NodeInfo) (*Meta, error)\n\tPushProgress(context.Context, *PushProgressRequest) (*PushProgressResponse, error)\n\tmustEmbedUnimplementedMasterServer()\n}\n\n// UnimplementedMasterServer must be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedMasterServer struct{}\n\nfunc (UnimplementedMasterServer) GetMeta(context.Context, *NodeInfo) (*Meta, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetMeta not implemented\")\n}\nfunc (UnimplementedMasterServer) PushProgress(context.Context, *PushProgressRequest) (*PushProgressResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method PushProgress not implemented\")\n}\nfunc (UnimplementedMasterServer) mustEmbedUnimplementedMasterServer() {}\nfunc (UnimplementedMasterServer) testEmbeddedByValue()                {}\n\n// UnsafeMasterServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to MasterServer will\n// result in compilation errors.\ntype UnsafeMasterServer interface {\n\tmustEmbedUnimplementedMasterServer()\n}\n\nfunc RegisterMasterServer(s grpc.ServiceRegistrar, srv MasterServer) {\n\t// If the following call panics, it indicates UnimplementedMasterServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&Master_ServiceDesc, srv)\n}\n\nfunc _Master_GetMeta_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(NodeInfo)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(MasterServer).GetMeta(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Master_GetMeta_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(MasterServer).GetMeta(ctx, req.(*NodeInfo))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Master_PushProgress_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(PushProgressRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(MasterServer).PushProgress(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Master_PushProgress_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(MasterServer).PushProgress(ctx, req.(*PushProgressRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// Master_ServiceDesc is the grpc.ServiceDesc for Master service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar Master_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"protocol.Master\",\n\tHandlerType: (*MasterServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"GetMeta\",\n\t\t\tHandler:    _Master_GetMeta_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"PushProgress\",\n\t\t\tHandler:    _Master_PushProgress_Handler,\n\t\t},\n\t},\n\tStreams:  []grpc.StreamDesc{},\n\tMetadata: \"protocol.proto\",\n}\n\nconst (\n\tBlobStore_UploadBlob_FullMethodName   = \"/protocol.BlobStore/UploadBlob\"\n\tBlobStore_DownloadBlob_FullMethodName = \"/protocol.BlobStore/DownloadBlob\"\n\tBlobStore_ListBlobs_FullMethodName    = \"/protocol.BlobStore/ListBlobs\"\n\tBlobStore_RemoveBlob_FullMethodName   = \"/protocol.BlobStore/RemoveBlob\"\n)\n\n// BlobStoreClient is the client API for BlobStore service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype BlobStoreClient interface {\n\tUploadBlob(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[UploadBlobRequest, UploadBlobResponse], error)\n\tDownloadBlob(ctx context.Context, in *DownloadBlobRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[DownloadBlobResponse], error)\n\tListBlobs(ctx context.Context, in *ListBlobsRequest, opts ...grpc.CallOption) (*ListBlobsResponse, error)\n\tRemoveBlob(ctx context.Context, in *RemoveBlobRequest, opts ...grpc.CallOption) (*RemoveBlobResponse, error)\n}\n\ntype blobStoreClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewBlobStoreClient(cc grpc.ClientConnInterface) BlobStoreClient {\n\treturn &blobStoreClient{cc}\n}\n\nfunc (c *blobStoreClient) UploadBlob(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[UploadBlobRequest, UploadBlobResponse], error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tstream, err := c.cc.NewStream(ctx, &BlobStore_ServiceDesc.Streams[0], BlobStore_UploadBlob_FullMethodName, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &grpc.GenericClientStream[UploadBlobRequest, UploadBlobResponse]{ClientStream: stream}\n\treturn x, nil\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype BlobStore_UploadBlobClient = grpc.ClientStreamingClient[UploadBlobRequest, UploadBlobResponse]\n\nfunc (c *blobStoreClient) DownloadBlob(ctx context.Context, in *DownloadBlobRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[DownloadBlobResponse], error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tstream, err := c.cc.NewStream(ctx, &BlobStore_ServiceDesc.Streams[1], BlobStore_DownloadBlob_FullMethodName, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &grpc.GenericClientStream[DownloadBlobRequest, DownloadBlobResponse]{ClientStream: stream}\n\tif err := x.ClientStream.SendMsg(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := x.ClientStream.CloseSend(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn x, nil\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype BlobStore_DownloadBlobClient = grpc.ServerStreamingClient[DownloadBlobResponse]\n\nfunc (c *blobStoreClient) ListBlobs(ctx context.Context, in *ListBlobsRequest, opts ...grpc.CallOption) (*ListBlobsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ListBlobsResponse)\n\terr := c.cc.Invoke(ctx, BlobStore_ListBlobs_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *blobStoreClient) RemoveBlob(ctx context.Context, in *RemoveBlobRequest, opts ...grpc.CallOption) (*RemoveBlobResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(RemoveBlobResponse)\n\terr := c.cc.Invoke(ctx, BlobStore_RemoveBlob_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// BlobStoreServer is the server API for BlobStore service.\n// All implementations must embed UnimplementedBlobStoreServer\n// for forward compatibility.\ntype BlobStoreServer interface {\n\tUploadBlob(grpc.ClientStreamingServer[UploadBlobRequest, UploadBlobResponse]) error\n\tDownloadBlob(*DownloadBlobRequest, grpc.ServerStreamingServer[DownloadBlobResponse]) error\n\tListBlobs(context.Context, *ListBlobsRequest) (*ListBlobsResponse, error)\n\tRemoveBlob(context.Context, *RemoveBlobRequest) (*RemoveBlobResponse, error)\n\tmustEmbedUnimplementedBlobStoreServer()\n}\n\n// UnimplementedBlobStoreServer must be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedBlobStoreServer struct{}\n\nfunc (UnimplementedBlobStoreServer) UploadBlob(grpc.ClientStreamingServer[UploadBlobRequest, UploadBlobResponse]) error {\n\treturn status.Error(codes.Unimplemented, \"method UploadBlob not implemented\")\n}\nfunc (UnimplementedBlobStoreServer) DownloadBlob(*DownloadBlobRequest, grpc.ServerStreamingServer[DownloadBlobResponse]) error {\n\treturn status.Error(codes.Unimplemented, \"method DownloadBlob not implemented\")\n}\nfunc (UnimplementedBlobStoreServer) ListBlobs(context.Context, *ListBlobsRequest) (*ListBlobsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method ListBlobs not implemented\")\n}\nfunc (UnimplementedBlobStoreServer) RemoveBlob(context.Context, *RemoveBlobRequest) (*RemoveBlobResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method RemoveBlob not implemented\")\n}\nfunc (UnimplementedBlobStoreServer) mustEmbedUnimplementedBlobStoreServer() {}\nfunc (UnimplementedBlobStoreServer) testEmbeddedByValue()                   {}\n\n// UnsafeBlobStoreServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to BlobStoreServer will\n// result in compilation errors.\ntype UnsafeBlobStoreServer interface {\n\tmustEmbedUnimplementedBlobStoreServer()\n}\n\nfunc RegisterBlobStoreServer(s grpc.ServiceRegistrar, srv BlobStoreServer) {\n\t// If the following call panics, it indicates UnimplementedBlobStoreServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&BlobStore_ServiceDesc, srv)\n}\n\nfunc _BlobStore_UploadBlob_Handler(srv interface{}, stream grpc.ServerStream) error {\n\treturn srv.(BlobStoreServer).UploadBlob(&grpc.GenericServerStream[UploadBlobRequest, UploadBlobResponse]{ServerStream: stream})\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype BlobStore_UploadBlobServer = grpc.ClientStreamingServer[UploadBlobRequest, UploadBlobResponse]\n\nfunc _BlobStore_DownloadBlob_Handler(srv interface{}, stream grpc.ServerStream) error {\n\tm := new(DownloadBlobRequest)\n\tif err := stream.RecvMsg(m); err != nil {\n\t\treturn err\n\t}\n\treturn srv.(BlobStoreServer).DownloadBlob(m, &grpc.GenericServerStream[DownloadBlobRequest, DownloadBlobResponse]{ServerStream: stream})\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype BlobStore_DownloadBlobServer = grpc.ServerStreamingServer[DownloadBlobResponse]\n\nfunc _BlobStore_ListBlobs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ListBlobsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(BlobStoreServer).ListBlobs(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: BlobStore_ListBlobs_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(BlobStoreServer).ListBlobs(ctx, req.(*ListBlobsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _BlobStore_RemoveBlob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(RemoveBlobRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(BlobStoreServer).RemoveBlob(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: BlobStore_RemoveBlob_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(BlobStoreServer).RemoveBlob(ctx, req.(*RemoveBlobRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// BlobStore_ServiceDesc is the grpc.ServiceDesc for BlobStore service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar BlobStore_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"protocol.BlobStore\",\n\tHandlerType: (*BlobStoreServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"ListBlobs\",\n\t\t\tHandler:    _BlobStore_ListBlobs_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"RemoveBlob\",\n\t\t\tHandler:    _BlobStore_RemoveBlob_Handler,\n\t\t},\n\t},\n\tStreams: []grpc.StreamDesc{\n\t\t{\n\t\t\tStreamName:    \"UploadBlob\",\n\t\t\tHandler:       _BlobStore_UploadBlob_Handler,\n\t\t\tClientStreams: true,\n\t\t},\n\t\t{\n\t\t\tStreamName:    \"DownloadBlob\",\n\t\t\tHandler:       _BlobStore_DownloadBlob_Handler,\n\t\t\tServerStreams: true,\n\t\t},\n\t},\n\tMetadata: \"protocol.proto\",\n}\n"
  },
  {
    "path": "protocol/vector_store.pb.go",
    "content": "// Copyright 2026 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        v7.34.0--rc2\n// source: vector_store.proto\n\npackage protocol\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\ttimestamppb \"google.golang.org/protobuf/types/known/timestamppb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype Distance int32\n\nconst (\n\tDistance_Unknown   Distance = 0\n\tDistance_Cosine    Distance = 1\n\tDistance_Euclidean Distance = 2\n\tDistance_Dot       Distance = 3\n)\n\n// Enum value maps for Distance.\nvar (\n\tDistance_name = map[int32]string{\n\t\t0: \"Unknown\",\n\t\t1: \"Cosine\",\n\t\t2: \"Euclidean\",\n\t\t3: \"Dot\",\n\t}\n\tDistance_value = map[string]int32{\n\t\t\"Unknown\":   0,\n\t\t\"Cosine\":    1,\n\t\t\"Euclidean\": 2,\n\t\t\"Dot\":       3,\n\t}\n)\n\nfunc (x Distance) Enum() *Distance {\n\tp := new(Distance)\n\t*p = x\n\treturn p\n}\n\nfunc (x Distance) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (Distance) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_vector_store_proto_enumTypes[0].Descriptor()\n}\n\nfunc (Distance) Type() protoreflect.EnumType {\n\treturn &file_vector_store_proto_enumTypes[0]\n}\n\nfunc (x Distance) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use Distance.Descriptor instead.\nfunc (Distance) EnumDescriptor() ([]byte, []int) {\n\treturn file_vector_store_proto_rawDescGZIP(), []int{0}\n}\n\ntype Vector struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tValues        []float32              `protobuf:\"fixed32,2,rep,packed,name=values,proto3\" json:\"values,omitempty\"`\n\tCategories    []string               `protobuf:\"bytes,3,rep,name=categories,proto3\" json:\"categories,omitempty\"`\n\tTimestamp     *timestamppb.Timestamp `protobuf:\"bytes,4,opt,name=timestamp,proto3\" json:\"timestamp,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Vector) Reset() {\n\t*x = Vector{}\n\tmi := &file_vector_store_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Vector) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Vector) ProtoMessage() {}\n\nfunc (x *Vector) ProtoReflect() protoreflect.Message {\n\tmi := &file_vector_store_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Vector.ProtoReflect.Descriptor instead.\nfunc (*Vector) Descriptor() ([]byte, []int) {\n\treturn file_vector_store_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *Vector) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *Vector) GetValues() []float32 {\n\tif x != nil {\n\t\treturn x.Values\n\t}\n\treturn nil\n}\n\nfunc (x *Vector) GetCategories() []string {\n\tif x != nil {\n\t\treturn x.Categories\n\t}\n\treturn nil\n}\n\nfunc (x *Vector) GetTimestamp() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.Timestamp\n\t}\n\treturn nil\n}\n\ntype ListCollectionsRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListCollectionsRequest) Reset() {\n\t*x = ListCollectionsRequest{}\n\tmi := &file_vector_store_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListCollectionsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListCollectionsRequest) ProtoMessage() {}\n\nfunc (x *ListCollectionsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_vector_store_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListCollectionsRequest.ProtoReflect.Descriptor instead.\nfunc (*ListCollectionsRequest) Descriptor() ([]byte, []int) {\n\treturn file_vector_store_proto_rawDescGZIP(), []int{1}\n}\n\ntype ListCollectionsResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCollections   []string               `protobuf:\"bytes,1,rep,name=collections,proto3\" json:\"collections,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListCollectionsResponse) Reset() {\n\t*x = ListCollectionsResponse{}\n\tmi := &file_vector_store_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListCollectionsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListCollectionsResponse) ProtoMessage() {}\n\nfunc (x *ListCollectionsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_vector_store_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListCollectionsResponse.ProtoReflect.Descriptor instead.\nfunc (*ListCollectionsResponse) Descriptor() ([]byte, []int) {\n\treturn file_vector_store_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *ListCollectionsResponse) GetCollections() []string {\n\tif x != nil {\n\t\treturn x.Collections\n\t}\n\treturn nil\n}\n\ntype AddCollectionRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tDimensions    int32                  `protobuf:\"varint,2,opt,name=dimensions,proto3\" json:\"dimensions,omitempty\"`\n\tDistance      Distance               `protobuf:\"varint,3,opt,name=distance,proto3,enum=protocol.Distance\" json:\"distance,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *AddCollectionRequest) Reset() {\n\t*x = AddCollectionRequest{}\n\tmi := &file_vector_store_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *AddCollectionRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*AddCollectionRequest) ProtoMessage() {}\n\nfunc (x *AddCollectionRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_vector_store_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use AddCollectionRequest.ProtoReflect.Descriptor instead.\nfunc (*AddCollectionRequest) Descriptor() ([]byte, []int) {\n\treturn file_vector_store_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *AddCollectionRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *AddCollectionRequest) GetDimensions() int32 {\n\tif x != nil {\n\t\treturn x.Dimensions\n\t}\n\treturn 0\n}\n\nfunc (x *AddCollectionRequest) GetDistance() Distance {\n\tif x != nil {\n\t\treturn x.Distance\n\t}\n\treturn Distance_Unknown\n}\n\ntype AddCollectionResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *AddCollectionResponse) Reset() {\n\t*x = AddCollectionResponse{}\n\tmi := &file_vector_store_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *AddCollectionResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*AddCollectionResponse) ProtoMessage() {}\n\nfunc (x *AddCollectionResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_vector_store_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use AddCollectionResponse.ProtoReflect.Descriptor instead.\nfunc (*AddCollectionResponse) Descriptor() ([]byte, []int) {\n\treturn file_vector_store_proto_rawDescGZIP(), []int{4}\n}\n\ntype DeleteCollectionRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteCollectionRequest) Reset() {\n\t*x = DeleteCollectionRequest{}\n\tmi := &file_vector_store_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteCollectionRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteCollectionRequest) ProtoMessage() {}\n\nfunc (x *DeleteCollectionRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_vector_store_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteCollectionRequest.ProtoReflect.Descriptor instead.\nfunc (*DeleteCollectionRequest) Descriptor() ([]byte, []int) {\n\treturn file_vector_store_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *DeleteCollectionRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\ntype DeleteCollectionResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteCollectionResponse) Reset() {\n\t*x = DeleteCollectionResponse{}\n\tmi := &file_vector_store_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteCollectionResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteCollectionResponse) ProtoMessage() {}\n\nfunc (x *DeleteCollectionResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_vector_store_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteCollectionResponse.ProtoReflect.Descriptor instead.\nfunc (*DeleteCollectionResponse) Descriptor() ([]byte, []int) {\n\treturn file_vector_store_proto_rawDescGZIP(), []int{6}\n}\n\ntype AddVectorsRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCollection    string                 `protobuf:\"bytes,1,opt,name=collection,proto3\" json:\"collection,omitempty\"`\n\tVectors       []*Vector              `protobuf:\"bytes,2,rep,name=vectors,proto3\" json:\"vectors,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *AddVectorsRequest) Reset() {\n\t*x = AddVectorsRequest{}\n\tmi := &file_vector_store_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *AddVectorsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*AddVectorsRequest) ProtoMessage() {}\n\nfunc (x *AddVectorsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_vector_store_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use AddVectorsRequest.ProtoReflect.Descriptor instead.\nfunc (*AddVectorsRequest) Descriptor() ([]byte, []int) {\n\treturn file_vector_store_proto_rawDescGZIP(), []int{7}\n}\n\nfunc (x *AddVectorsRequest) GetCollection() string {\n\tif x != nil {\n\t\treturn x.Collection\n\t}\n\treturn \"\"\n}\n\nfunc (x *AddVectorsRequest) GetVectors() []*Vector {\n\tif x != nil {\n\t\treturn x.Vectors\n\t}\n\treturn nil\n}\n\ntype AddVectorsResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *AddVectorsResponse) Reset() {\n\t*x = AddVectorsResponse{}\n\tmi := &file_vector_store_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *AddVectorsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*AddVectorsResponse) ProtoMessage() {}\n\nfunc (x *AddVectorsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_vector_store_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use AddVectorsResponse.ProtoReflect.Descriptor instead.\nfunc (*AddVectorsResponse) Descriptor() ([]byte, []int) {\n\treturn file_vector_store_proto_rawDescGZIP(), []int{8}\n}\n\ntype DeleteVectorsRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCollection    string                 `protobuf:\"bytes,1,opt,name=collection,proto3\" json:\"collection,omitempty\"`\n\tTimestamp     *timestamppb.Timestamp `protobuf:\"bytes,2,opt,name=timestamp,proto3\" json:\"timestamp,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteVectorsRequest) Reset() {\n\t*x = DeleteVectorsRequest{}\n\tmi := &file_vector_store_proto_msgTypes[9]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteVectorsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteVectorsRequest) ProtoMessage() {}\n\nfunc (x *DeleteVectorsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_vector_store_proto_msgTypes[9]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteVectorsRequest.ProtoReflect.Descriptor instead.\nfunc (*DeleteVectorsRequest) Descriptor() ([]byte, []int) {\n\treturn file_vector_store_proto_rawDescGZIP(), []int{9}\n}\n\nfunc (x *DeleteVectorsRequest) GetCollection() string {\n\tif x != nil {\n\t\treturn x.Collection\n\t}\n\treturn \"\"\n}\n\nfunc (x *DeleteVectorsRequest) GetTimestamp() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.Timestamp\n\t}\n\treturn nil\n}\n\ntype DeleteVectorsResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteVectorsResponse) Reset() {\n\t*x = DeleteVectorsResponse{}\n\tmi := &file_vector_store_proto_msgTypes[10]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteVectorsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteVectorsResponse) ProtoMessage() {}\n\nfunc (x *DeleteVectorsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_vector_store_proto_msgTypes[10]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteVectorsResponse.ProtoReflect.Descriptor instead.\nfunc (*DeleteVectorsResponse) Descriptor() ([]byte, []int) {\n\treturn file_vector_store_proto_rawDescGZIP(), []int{10}\n}\n\ntype QueryVectorsRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCollection    string                 `protobuf:\"bytes,1,opt,name=collection,proto3\" json:\"collection,omitempty\"`\n\tQuery         []float32              `protobuf:\"fixed32,2,rep,packed,name=query,proto3\" json:\"query,omitempty\"`\n\tCategories    []string               `protobuf:\"bytes,3,rep,name=categories,proto3\" json:\"categories,omitempty\"`\n\tTopK          int32                  `protobuf:\"varint,4,opt,name=top_k,json=topK,proto3\" json:\"top_k,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *QueryVectorsRequest) Reset() {\n\t*x = QueryVectorsRequest{}\n\tmi := &file_vector_store_proto_msgTypes[11]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *QueryVectorsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*QueryVectorsRequest) ProtoMessage() {}\n\nfunc (x *QueryVectorsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_vector_store_proto_msgTypes[11]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use QueryVectorsRequest.ProtoReflect.Descriptor instead.\nfunc (*QueryVectorsRequest) Descriptor() ([]byte, []int) {\n\treturn file_vector_store_proto_rawDescGZIP(), []int{11}\n}\n\nfunc (x *QueryVectorsRequest) GetCollection() string {\n\tif x != nil {\n\t\treturn x.Collection\n\t}\n\treturn \"\"\n}\n\nfunc (x *QueryVectorsRequest) GetQuery() []float32 {\n\tif x != nil {\n\t\treturn x.Query\n\t}\n\treturn nil\n}\n\nfunc (x *QueryVectorsRequest) GetCategories() []string {\n\tif x != nil {\n\t\treturn x.Categories\n\t}\n\treturn nil\n}\n\nfunc (x *QueryVectorsRequest) GetTopK() int32 {\n\tif x != nil {\n\t\treturn x.TopK\n\t}\n\treturn 0\n}\n\ntype QueryVectorsResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tVectors       []*Vector              `protobuf:\"bytes,1,rep,name=vectors,proto3\" json:\"vectors,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *QueryVectorsResponse) Reset() {\n\t*x = QueryVectorsResponse{}\n\tmi := &file_vector_store_proto_msgTypes[12]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *QueryVectorsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*QueryVectorsResponse) ProtoMessage() {}\n\nfunc (x *QueryVectorsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_vector_store_proto_msgTypes[12]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use QueryVectorsResponse.ProtoReflect.Descriptor instead.\nfunc (*QueryVectorsResponse) Descriptor() ([]byte, []int) {\n\treturn file_vector_store_proto_rawDescGZIP(), []int{12}\n}\n\nfunc (x *QueryVectorsResponse) GetVectors() []*Vector {\n\tif x != nil {\n\t\treturn x.Vectors\n\t}\n\treturn nil\n}\n\nvar File_vector_store_proto protoreflect.FileDescriptor\n\nconst file_vector_store_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x12vector_store.proto\\x12\\bprotocol\\x1a\\x1fgoogle/protobuf/timestamp.proto\\\"\\x8a\\x01\\n\" +\n\t\"\\x06Vector\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x16\\n\" +\n\t\"\\x06values\\x18\\x02 \\x03(\\x02R\\x06values\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"categories\\x18\\x03 \\x03(\\tR\\n\" +\n\t\"categories\\x128\\n\" +\n\t\"\\ttimestamp\\x18\\x04 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\ttimestamp\\\"\\x18\\n\" +\n\t\"\\x16ListCollectionsRequest\\\";\\n\" +\n\t\"\\x17ListCollectionsResponse\\x12 \\n\" +\n\t\"\\vcollections\\x18\\x01 \\x03(\\tR\\vcollections\\\"z\\n\" +\n\t\"\\x14AddCollectionRequest\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"dimensions\\x18\\x02 \\x01(\\x05R\\n\" +\n\t\"dimensions\\x12.\\n\" +\n\t\"\\bdistance\\x18\\x03 \\x01(\\x0e2\\x12.protocol.DistanceR\\bdistance\\\"\\x17\\n\" +\n\t\"\\x15AddCollectionResponse\\\"-\\n\" +\n\t\"\\x17DeleteCollectionRequest\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\\"\\x1a\\n\" +\n\t\"\\x18DeleteCollectionResponse\\\"_\\n\" +\n\t\"\\x11AddVectorsRequest\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"collection\\x18\\x01 \\x01(\\tR\\n\" +\n\t\"collection\\x12*\\n\" +\n\t\"\\avectors\\x18\\x02 \\x03(\\v2\\x10.protocol.VectorR\\avectors\\\"\\x14\\n\" +\n\t\"\\x12AddVectorsResponse\\\"p\\n\" +\n\t\"\\x14DeleteVectorsRequest\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"collection\\x18\\x01 \\x01(\\tR\\n\" +\n\t\"collection\\x128\\n\" +\n\t\"\\ttimestamp\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\ttimestamp\\\"\\x17\\n\" +\n\t\"\\x15DeleteVectorsResponse\\\"\\x80\\x01\\n\" +\n\t\"\\x13QueryVectorsRequest\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"collection\\x18\\x01 \\x01(\\tR\\n\" +\n\t\"collection\\x12\\x14\\n\" +\n\t\"\\x05query\\x18\\x02 \\x03(\\x02R\\x05query\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"categories\\x18\\x03 \\x03(\\tR\\n\" +\n\t\"categories\\x12\\x13\\n\" +\n\t\"\\x05top_k\\x18\\x04 \\x01(\\x05R\\x04topK\\\"B\\n\" +\n\t\"\\x14QueryVectorsResponse\\x12*\\n\" +\n\t\"\\avectors\\x18\\x01 \\x03(\\v2\\x10.protocol.VectorR\\avectors*;\\n\" +\n\t\"\\bDistance\\x12\\v\\n\" +\n\t\"\\aUnknown\\x10\\x00\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06Cosine\\x10\\x01\\x12\\r\\n\" +\n\t\"\\tEuclidean\\x10\\x02\\x12\\a\\n\" +\n\t\"\\x03Dot\\x10\\x032\\x88\\x04\\n\" +\n\t\"\\vVectorStore\\x12X\\n\" +\n\t\"\\x0fListCollections\\x12 .protocol.ListCollectionsRequest\\x1a!.protocol.ListCollectionsResponse\\\"\\x00\\x12R\\n\" +\n\t\"\\rAddCollection\\x12\\x1e.protocol.AddCollectionRequest\\x1a\\x1f.protocol.AddCollectionResponse\\\"\\x00\\x12[\\n\" +\n\t\"\\x10DeleteCollection\\x12!.protocol.DeleteCollectionRequest\\x1a\\\".protocol.DeleteCollectionResponse\\\"\\x00\\x12I\\n\" +\n\t\"\\n\" +\n\t\"AddVectors\\x12\\x1b.protocol.AddVectorsRequest\\x1a\\x1c.protocol.AddVectorsResponse\\\"\\x00\\x12R\\n\" +\n\t\"\\rDeleteVectors\\x12\\x1e.protocol.DeleteVectorsRequest\\x1a\\x1f.protocol.DeleteVectorsResponse\\\"\\x00\\x12O\\n\" +\n\t\"\\fQueryVectors\\x12\\x1d.protocol.QueryVectorsRequest\\x1a\\x1e.protocol.QueryVectorsResponse\\\"\\x00B$Z\\\"github.com/gorse-io/gorse/protocolb\\x06proto3\"\n\nvar (\n\tfile_vector_store_proto_rawDescOnce sync.Once\n\tfile_vector_store_proto_rawDescData []byte\n)\n\nfunc file_vector_store_proto_rawDescGZIP() []byte {\n\tfile_vector_store_proto_rawDescOnce.Do(func() {\n\t\tfile_vector_store_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_vector_store_proto_rawDesc), len(file_vector_store_proto_rawDesc)))\n\t})\n\treturn file_vector_store_proto_rawDescData\n}\n\nvar file_vector_store_proto_enumTypes = make([]protoimpl.EnumInfo, 1)\nvar file_vector_store_proto_msgTypes = make([]protoimpl.MessageInfo, 13)\nvar file_vector_store_proto_goTypes = []any{\n\t(Distance)(0),                    // 0: protocol.Distance\n\t(*Vector)(nil),                   // 1: protocol.Vector\n\t(*ListCollectionsRequest)(nil),   // 2: protocol.ListCollectionsRequest\n\t(*ListCollectionsResponse)(nil),  // 3: protocol.ListCollectionsResponse\n\t(*AddCollectionRequest)(nil),     // 4: protocol.AddCollectionRequest\n\t(*AddCollectionResponse)(nil),    // 5: protocol.AddCollectionResponse\n\t(*DeleteCollectionRequest)(nil),  // 6: protocol.DeleteCollectionRequest\n\t(*DeleteCollectionResponse)(nil), // 7: protocol.DeleteCollectionResponse\n\t(*AddVectorsRequest)(nil),        // 8: protocol.AddVectorsRequest\n\t(*AddVectorsResponse)(nil),       // 9: protocol.AddVectorsResponse\n\t(*DeleteVectorsRequest)(nil),     // 10: protocol.DeleteVectorsRequest\n\t(*DeleteVectorsResponse)(nil),    // 11: protocol.DeleteVectorsResponse\n\t(*QueryVectorsRequest)(nil),      // 12: protocol.QueryVectorsRequest\n\t(*QueryVectorsResponse)(nil),     // 13: protocol.QueryVectorsResponse\n\t(*timestamppb.Timestamp)(nil),    // 14: google.protobuf.Timestamp\n}\nvar file_vector_store_proto_depIdxs = []int32{\n\t14, // 0: protocol.Vector.timestamp:type_name -> google.protobuf.Timestamp\n\t0,  // 1: protocol.AddCollectionRequest.distance:type_name -> protocol.Distance\n\t1,  // 2: protocol.AddVectorsRequest.vectors:type_name -> protocol.Vector\n\t14, // 3: protocol.DeleteVectorsRequest.timestamp:type_name -> google.protobuf.Timestamp\n\t1,  // 4: protocol.QueryVectorsResponse.vectors:type_name -> protocol.Vector\n\t2,  // 5: protocol.VectorStore.ListCollections:input_type -> protocol.ListCollectionsRequest\n\t4,  // 6: protocol.VectorStore.AddCollection:input_type -> protocol.AddCollectionRequest\n\t6,  // 7: protocol.VectorStore.DeleteCollection:input_type -> protocol.DeleteCollectionRequest\n\t8,  // 8: protocol.VectorStore.AddVectors:input_type -> protocol.AddVectorsRequest\n\t10, // 9: protocol.VectorStore.DeleteVectors:input_type -> protocol.DeleteVectorsRequest\n\t12, // 10: protocol.VectorStore.QueryVectors:input_type -> protocol.QueryVectorsRequest\n\t3,  // 11: protocol.VectorStore.ListCollections:output_type -> protocol.ListCollectionsResponse\n\t5,  // 12: protocol.VectorStore.AddCollection:output_type -> protocol.AddCollectionResponse\n\t7,  // 13: protocol.VectorStore.DeleteCollection:output_type -> protocol.DeleteCollectionResponse\n\t9,  // 14: protocol.VectorStore.AddVectors:output_type -> protocol.AddVectorsResponse\n\t11, // 15: protocol.VectorStore.DeleteVectors:output_type -> protocol.DeleteVectorsResponse\n\t13, // 16: protocol.VectorStore.QueryVectors:output_type -> protocol.QueryVectorsResponse\n\t11, // [11:17] is the sub-list for method output_type\n\t5,  // [5:11] is the sub-list for method input_type\n\t5,  // [5:5] is the sub-list for extension type_name\n\t5,  // [5:5] is the sub-list for extension extendee\n\t0,  // [0:5] is the sub-list for field type_name\n}\n\nfunc init() { file_vector_store_proto_init() }\nfunc file_vector_store_proto_init() {\n\tif File_vector_store_proto != nil {\n\t\treturn\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_vector_store_proto_rawDesc), len(file_vector_store_proto_rawDesc)),\n\t\t\tNumEnums:      1,\n\t\t\tNumMessages:   13,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_vector_store_proto_goTypes,\n\t\tDependencyIndexes: file_vector_store_proto_depIdxs,\n\t\tEnumInfos:         file_vector_store_proto_enumTypes,\n\t\tMessageInfos:      file_vector_store_proto_msgTypes,\n\t}.Build()\n\tFile_vector_store_proto = out.File\n\tfile_vector_store_proto_goTypes = nil\n\tfile_vector_store_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "protocol/vector_store.proto",
    "content": "// Copyright 2026 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nsyntax = \"proto3\";\n\noption go_package = \"github.com/gorse-io/gorse/protocol\";\n\npackage protocol;\n\nimport \"google/protobuf/timestamp.proto\";\n\nenum Distance {\n  Unknown = 0;\n  Cosine = 1;\n  Euclidean = 2;\n  Dot = 3;\n}\n\nmessage Vector {\n  string id = 1;\n  repeated float values = 2;\n  repeated string categories = 3;\n  google.protobuf.Timestamp timestamp = 4;\n}\n\nmessage ListCollectionsRequest {}\n\nmessage ListCollectionsResponse { repeated string collections = 1; }\n\nmessage AddCollectionRequest {\n  string name = 1;\n  int32 dimensions = 2;\n  Distance distance = 3;\n}\n\nmessage AddCollectionResponse {}\n\nmessage DeleteCollectionRequest { string name = 1; }\n\nmessage DeleteCollectionResponse {}\n\nmessage AddVectorsRequest {\n  string collection = 1;\n  repeated Vector vectors = 2;\n}\n\nmessage AddVectorsResponse {}\n\nmessage DeleteVectorsRequest {\n  string collection = 1;\n  google.protobuf.Timestamp timestamp = 2;\n}\n\nmessage DeleteVectorsResponse {}\n\nmessage QueryVectorsRequest {\n  string collection = 1;\n  repeated float query = 2;\n  repeated string categories = 3;\n  int32 top_k = 4;\n}\n\nmessage QueryVectorsResponse { repeated Vector vectors = 1; }\n\nservice VectorStore {\n  rpc ListCollections(ListCollectionsRequest) returns (ListCollectionsResponse) {}\n  rpc AddCollection(AddCollectionRequest) returns (AddCollectionResponse) {}\n  rpc DeleteCollection(DeleteCollectionRequest) returns (DeleteCollectionResponse) {}\n  rpc AddVectors(AddVectorsRequest) returns (AddVectorsResponse) {}\n  rpc DeleteVectors(DeleteVectorsRequest) returns (DeleteVectorsResponse) {}\n  rpc QueryVectors(QueryVectorsRequest) returns (QueryVectorsResponse) {}\n}\n"
  },
  {
    "path": "protocol/vector_store_grpc.pb.go",
    "content": "// Copyright 2026 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.6.1\n// - protoc             v7.34.0--rc2\n// source: vector_store.proto\n\npackage protocol\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.64.0 or later.\nconst _ = grpc.SupportPackageIsVersion9\n\nconst (\n\tVectorStore_ListCollections_FullMethodName  = \"/protocol.VectorStore/ListCollections\"\n\tVectorStore_AddCollection_FullMethodName    = \"/protocol.VectorStore/AddCollection\"\n\tVectorStore_DeleteCollection_FullMethodName = \"/protocol.VectorStore/DeleteCollection\"\n\tVectorStore_AddVectors_FullMethodName       = \"/protocol.VectorStore/AddVectors\"\n\tVectorStore_DeleteVectors_FullMethodName    = \"/protocol.VectorStore/DeleteVectors\"\n\tVectorStore_QueryVectors_FullMethodName     = \"/protocol.VectorStore/QueryVectors\"\n)\n\n// VectorStoreClient is the client API for VectorStore service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype VectorStoreClient interface {\n\tListCollections(ctx context.Context, in *ListCollectionsRequest, opts ...grpc.CallOption) (*ListCollectionsResponse, error)\n\tAddCollection(ctx context.Context, in *AddCollectionRequest, opts ...grpc.CallOption) (*AddCollectionResponse, error)\n\tDeleteCollection(ctx context.Context, in *DeleteCollectionRequest, opts ...grpc.CallOption) (*DeleteCollectionResponse, error)\n\tAddVectors(ctx context.Context, in *AddVectorsRequest, opts ...grpc.CallOption) (*AddVectorsResponse, error)\n\tDeleteVectors(ctx context.Context, in *DeleteVectorsRequest, opts ...grpc.CallOption) (*DeleteVectorsResponse, error)\n\tQueryVectors(ctx context.Context, in *QueryVectorsRequest, opts ...grpc.CallOption) (*QueryVectorsResponse, error)\n}\n\ntype vectorStoreClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewVectorStoreClient(cc grpc.ClientConnInterface) VectorStoreClient {\n\treturn &vectorStoreClient{cc}\n}\n\nfunc (c *vectorStoreClient) ListCollections(ctx context.Context, in *ListCollectionsRequest, opts ...grpc.CallOption) (*ListCollectionsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ListCollectionsResponse)\n\terr := c.cc.Invoke(ctx, VectorStore_ListCollections_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *vectorStoreClient) AddCollection(ctx context.Context, in *AddCollectionRequest, opts ...grpc.CallOption) (*AddCollectionResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(AddCollectionResponse)\n\terr := c.cc.Invoke(ctx, VectorStore_AddCollection_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *vectorStoreClient) DeleteCollection(ctx context.Context, in *DeleteCollectionRequest, opts ...grpc.CallOption) (*DeleteCollectionResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(DeleteCollectionResponse)\n\terr := c.cc.Invoke(ctx, VectorStore_DeleteCollection_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *vectorStoreClient) AddVectors(ctx context.Context, in *AddVectorsRequest, opts ...grpc.CallOption) (*AddVectorsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(AddVectorsResponse)\n\terr := c.cc.Invoke(ctx, VectorStore_AddVectors_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *vectorStoreClient) DeleteVectors(ctx context.Context, in *DeleteVectorsRequest, opts ...grpc.CallOption) (*DeleteVectorsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(DeleteVectorsResponse)\n\terr := c.cc.Invoke(ctx, VectorStore_DeleteVectors_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *vectorStoreClient) QueryVectors(ctx context.Context, in *QueryVectorsRequest, opts ...grpc.CallOption) (*QueryVectorsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(QueryVectorsResponse)\n\terr := c.cc.Invoke(ctx, VectorStore_QueryVectors_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// VectorStoreServer is the server API for VectorStore service.\n// All implementations must embed UnimplementedVectorStoreServer\n// for forward compatibility.\ntype VectorStoreServer interface {\n\tListCollections(context.Context, *ListCollectionsRequest) (*ListCollectionsResponse, error)\n\tAddCollection(context.Context, *AddCollectionRequest) (*AddCollectionResponse, error)\n\tDeleteCollection(context.Context, *DeleteCollectionRequest) (*DeleteCollectionResponse, error)\n\tAddVectors(context.Context, *AddVectorsRequest) (*AddVectorsResponse, error)\n\tDeleteVectors(context.Context, *DeleteVectorsRequest) (*DeleteVectorsResponse, error)\n\tQueryVectors(context.Context, *QueryVectorsRequest) (*QueryVectorsResponse, error)\n\tmustEmbedUnimplementedVectorStoreServer()\n}\n\n// UnimplementedVectorStoreServer must be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedVectorStoreServer struct{}\n\nfunc (UnimplementedVectorStoreServer) ListCollections(context.Context, *ListCollectionsRequest) (*ListCollectionsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method ListCollections not implemented\")\n}\nfunc (UnimplementedVectorStoreServer) AddCollection(context.Context, *AddCollectionRequest) (*AddCollectionResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method AddCollection not implemented\")\n}\nfunc (UnimplementedVectorStoreServer) DeleteCollection(context.Context, *DeleteCollectionRequest) (*DeleteCollectionResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method DeleteCollection not implemented\")\n}\nfunc (UnimplementedVectorStoreServer) AddVectors(context.Context, *AddVectorsRequest) (*AddVectorsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method AddVectors not implemented\")\n}\nfunc (UnimplementedVectorStoreServer) DeleteVectors(context.Context, *DeleteVectorsRequest) (*DeleteVectorsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method DeleteVectors not implemented\")\n}\nfunc (UnimplementedVectorStoreServer) QueryVectors(context.Context, *QueryVectorsRequest) (*QueryVectorsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method QueryVectors not implemented\")\n}\nfunc (UnimplementedVectorStoreServer) mustEmbedUnimplementedVectorStoreServer() {}\nfunc (UnimplementedVectorStoreServer) testEmbeddedByValue()                     {}\n\n// UnsafeVectorStoreServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to VectorStoreServer will\n// result in compilation errors.\ntype UnsafeVectorStoreServer interface {\n\tmustEmbedUnimplementedVectorStoreServer()\n}\n\nfunc RegisterVectorStoreServer(s grpc.ServiceRegistrar, srv VectorStoreServer) {\n\t// If the following call panics, it indicates UnimplementedVectorStoreServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&VectorStore_ServiceDesc, srv)\n}\n\nfunc _VectorStore_ListCollections_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ListCollectionsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(VectorStoreServer).ListCollections(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: VectorStore_ListCollections_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(VectorStoreServer).ListCollections(ctx, req.(*ListCollectionsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _VectorStore_AddCollection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(AddCollectionRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(VectorStoreServer).AddCollection(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: VectorStore_AddCollection_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(VectorStoreServer).AddCollection(ctx, req.(*AddCollectionRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _VectorStore_DeleteCollection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DeleteCollectionRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(VectorStoreServer).DeleteCollection(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: VectorStore_DeleteCollection_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(VectorStoreServer).DeleteCollection(ctx, req.(*DeleteCollectionRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _VectorStore_AddVectors_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(AddVectorsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(VectorStoreServer).AddVectors(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: VectorStore_AddVectors_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(VectorStoreServer).AddVectors(ctx, req.(*AddVectorsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _VectorStore_DeleteVectors_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DeleteVectorsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(VectorStoreServer).DeleteVectors(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: VectorStore_DeleteVectors_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(VectorStoreServer).DeleteVectors(ctx, req.(*DeleteVectorsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _VectorStore_QueryVectors_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(QueryVectorsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(VectorStoreServer).QueryVectors(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: VectorStore_QueryVectors_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(VectorStoreServer).QueryVectors(ctx, req.(*QueryVectorsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// VectorStore_ServiceDesc is the grpc.ServiceDesc for VectorStore service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar VectorStore_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"protocol.VectorStore\",\n\tHandlerType: (*VectorStoreServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"ListCollections\",\n\t\t\tHandler:    _VectorStore_ListCollections_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"AddCollection\",\n\t\t\tHandler:    _VectorStore_AddCollection_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DeleteCollection\",\n\t\t\tHandler:    _VectorStore_DeleteCollection_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"AddVectors\",\n\t\t\tHandler:    _VectorStore_AddVectors_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DeleteVectors\",\n\t\t\tHandler:    _VectorStore_DeleteVectors_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"QueryVectors\",\n\t\t\tHandler:    _VectorStore_QueryVectors_Handler,\n\t\t},\n\t},\n\tStreams:  []grpc.StreamDesc{},\n\tMetadata: \"vector_store.proto\",\n}\n"
  },
  {
    "path": "server/metrics.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage server\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n)\n\nvar (\n\tRestAPIRequestSecondsVec = promauto.NewHistogramVec(prometheus.HistogramOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"server\",\n\t\tName:      \"rest_api_request_seconds\",\n\t}, []string{\"api\"})\n)\n"
  },
  {
    "path": "server/rest.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage server\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/pprof\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/araddon/dateparse\"\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\trestfulspec \"github.com/emicklei/go-restful-openapi/v2\"\n\t\"github.com/emicklei/go-restful/v3\"\n\t\"github.com/google/uuid\"\n\t\"github.com/gorse-io/gorse/common/expression\"\n\t\"github.com/gorse-io/gorse/common/heap\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/logics\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/juju/errors\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n\t\"github.com/samber/lo\"\n\t\"github.com/swaggest/swgui/v5emb\"\n\t\"go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful\"\n\t\"go.uber.org/zap\"\n)\n\nconst (\n\tHealthAPITag         = \"health\"\n\tUsersAPITag          = \"users\"\n\tItemsAPITag          = \"items\"\n\tFeedbackAPITag       = \"feedback\"\n\tRecommendationAPITag = \"recommendation\"\n\tMeasurementsAPITag   = \"measurements\"\n\tDetractedAPITag      = \"deprecated\"\n\tapiDocsPath          = \"/apidocs/\"\n)\n\n// RestServer implements a REST-ful API server.\ntype RestServer struct {\n\tConfig      *config.Config\n\tCacheClient cache.Database\n\tDataClient  data.Database\n\n\tHttpHost string\n\tHttpPort int\n\n\tDisableLog bool\n\tWebService *restful.WebService\n\tHttpServer *http.Server\n}\n\n// StartHttpServer starts the REST-ful API server.\nfunc (s *RestServer) StartHttpServer(container *restful.Container) {\n\t// register restful APIs\n\ts.CreateWebService()\n\tcontainer.Add(s.WebService)\n\t// register swagger UI\n\tspecConfig := restfulspec.Config{\n\t\tWebServices: []*restful.WebService{s.WebService},\n\t\tAPIPath:     \"/apidocs.json\",\n\t}\n\tcontainer.Add(restfulspec.NewOpenAPIService(specConfig))\n\tcontainer.Handle(apiDocsPath, v5emb.New(\n\t\t\"Gorse REST API\",\n\t\t\"/apidocs.json\",\n\t\tapiDocsPath,\n\t))\n\t// register prometheus\n\tcontainer.Handle(\"/metrics\", promhttp.Handler())\n\t// register pprof\n\tcontainer.Handle(\"/debug/pprof/\", http.HandlerFunc(pprof.Index))\n\tcontainer.Handle(\"/debug/pprof/cmdline\", http.HandlerFunc(pprof.Cmdline))\n\tcontainer.Handle(\"/debug/pprof/profile\", http.HandlerFunc(pprof.Profile))\n\tcontainer.Handle(\"/debug/pprof/symbol\", http.HandlerFunc(pprof.Symbol))\n\tcontainer.Handle(\"/debug/pprof/trace\", http.HandlerFunc(pprof.Trace))\n\tcontainer.Handle(\"/debug/pprof/allocs\", pprof.Handler(\"allocs\"))\n\tcontainer.Handle(\"/debug/pprof/block\", pprof.Handler(\"block\"))\n\tcontainer.Handle(\"/debug/pprof/goroutine\", pprof.Handler(\"goroutine\"))\n\tcontainer.Handle(\"/debug/pprof/heap\", pprof.Handler(\"heap\"))\n\tcontainer.Handle(\"/debug/pprof/mutex\", pprof.Handler(\"mutex\"))\n\tcontainer.Handle(\"/debug/pprof/threadcreate\", pprof.Handler(\"threadcreate\"))\n\n\t// Add container filter to enable CORS\n\tcors := restful.CrossOriginResourceSharing{\n\t\tAllowedHeaders: []string{\"Content-Type\", \"Accept\", \"X-API-Key\"},\n\t\tAllowedDomains: s.Config.Master.HttpCorsDomains,\n\t\tAllowedMethods: s.Config.Master.HttpCorsMethods,\n\t\tCookiesAllowed: false,\n\t\tContainer:      container}\n\tcontainer.Filter(cors.Filter)\n\n\tlog.Logger().Info(\"start http server\",\n\t\tzap.String(\"url\", fmt.Sprintf(\"http://%s:%d\", s.HttpHost, s.HttpPort)),\n\t\tzap.Strings(\"cors_methods\", s.Config.Master.HttpCorsMethods),\n\t\tzap.Strings(\"cors_domains\", s.Config.Master.HttpCorsDomains),\n\t)\n\ts.HttpServer = &http.Server{\n\t\tAddr:    fmt.Sprintf(\"%s:%d\", s.HttpHost, s.HttpPort),\n\t\tHandler: container,\n\t}\n\tif err := s.HttpServer.ListenAndServe(); err != http.ErrServerClosed {\n\t\tlog.Logger().Fatal(\"failed to start http server\", zap.Error(err))\n\t}\n}\n\nfunc (s *RestServer) LogFilter(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {\n\t// generate request id\n\trequestId := uuid.New().String()\n\tresp.AddHeader(\"X-Request-ID\", requestId)\n\n\tstart := time.Now()\n\tchain.ProcessFilter(req, resp)\n\tresponseTime := time.Since(start)\n\tif !s.DisableLog && req.Request.URL.Path != \"/api/dashboard/cluster\" &&\n\t\treq.Request.URL.Path != \"/api/dashboard/tasks\" {\n\t\tlog.ResponseLogger(resp).Info(fmt.Sprintf(\"%s %s\", req.Request.Method, req.Request.URL),\n\t\t\tzap.Int(\"status_code\", resp.StatusCode()),\n\t\t\tzap.Duration(\"response_time\", responseTime))\n\t}\n}\n\nfunc (s *RestServer) AuthFilter(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {\n\tif strings.HasPrefix(req.SelectedRoute().Path(), \"/api/health/\") {\n\t\t// Health check APIs don't need API key,\n\t\tchain.ProcessFilter(req, resp)\n\t\treturn\n\t}\n\tif s.Config.Server.APIKey == \"\" {\n\t\tchain.ProcessFilter(req, resp)\n\t\treturn\n\t}\n\tapikey := req.HeaderParameter(\"X-API-Key\")\n\tif apikey == s.Config.Server.APIKey {\n\t\tchain.ProcessFilter(req, resp)\n\t\treturn\n\t}\n\tlog.ResponseLogger(resp).Error(\"unauthorized\",\n\t\tzap.String(\"api_key\", s.Config.Server.APIKey),\n\t\tzap.String(\"X-API-Key\", apikey))\n\tif err := resp.WriteError(http.StatusUnauthorized, fmt.Errorf(\"unauthorized\")); err != nil {\n\t\tlog.ResponseLogger(resp).Error(\"failed to write error\", zap.Error(err))\n\t}\n}\n\nfunc (s *RestServer) MetricsFilter(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {\n\tstartTime := time.Now()\n\tchain.ProcessFilter(req, resp)\n\tif req.SelectedRoute() != nil && resp.StatusCode() == http.StatusOK {\n\t\troutePath := req.SelectedRoutePath()\n\t\tif !strings.HasPrefix(routePath, \"/api/dashboard\") {\n\t\t\tRestAPIRequestSecondsVec.WithLabelValues(fmt.Sprintf(\"%s %s\", req.Request.Method, routePath)).\n\t\t\t\tObserve(time.Since(startTime).Seconds())\n\t\t}\n\t}\n}\n\n// CreateWebService creates web service.\nfunc (s *RestServer) CreateWebService() {\n\t// Create a server\n\tws := s.WebService\n\tws.Path(\"/api/\").\n\t\tProduces(restful.MIME_JSON).\n\t\tFilter(s.LogFilter).\n\t\tFilter(s.AuthFilter).\n\t\tFilter(s.MetricsFilter).\n\t\tFilter(otelrestful.OTelFilter(\"gorse\"))\n\n\t/* Health check */\n\tws.Route(ws.GET(\"/health/live\").To(s.checkLive).\n\t\tDoc(\"Probe the liveness of this node. Return OK once the server starts.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{HealthAPITag}).\n\t\tReturns(http.StatusOK, \"OK\", HealthStatus{}).\n\t\tWrites(HealthStatus{}))\n\tws.Route(ws.GET(\"/health/ready\").To(s.checkReady).\n\t\tDoc(\"Probe the readiness of this node. Return OK if the server is able to handle requests.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{HealthAPITag}).\n\t\tReturns(http.StatusOK, \"OK\", HealthStatus{}).\n\t\tWrites(HealthStatus{}))\n\n\t// Insert a user\n\tws.Route(ws.POST(\"/user\").To(s.insertUser).\n\t\tDoc(\"Insert a user.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{UsersAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tReads(data.User{}).\n\t\tReturns(http.StatusOK, \"OK\", Success{}).\n\t\tWrites(Success{}))\n\t// Modify a user\n\tws.Route(ws.PATCH(\"/user/{user-id}\").To(s.modifyUser).\n\t\tDoc(\"Modify a user.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{UsersAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"user-id\", \"User ID\").DataType(\"string\")).\n\t\tReads(data.UserPatch{}).\n\t\tReturns(http.StatusOK, \"OK\", Success{}).\n\t\tWrites(Success{}))\n\t// Get a user\n\tws.Route(ws.GET(\"/user/{user-id}\").To(s.getUser).\n\t\tDoc(\"Get a user.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{UsersAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"user-id\", \"User ID\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", data.User{}).\n\t\tWrites(data.User{}))\n\t// Insert users\n\tws.Route(ws.POST(\"/users\").To(s.insertUsers).\n\t\tDoc(\"Insert users.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{UsersAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tReads([]data.User{}).\n\t\tReturns(http.StatusOK, \"OK\", Success{}).\n\t\tWrites(Success{}))\n\t// Get users\n\tws.Route(ws.GET(\"/users\").To(s.getUsers).\n\t\tDoc(\"List users.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{UsersAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"Number of returned users\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"cursor\", \"Cursor for the next page\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", UserIterator{}).\n\t\tWrites(UserIterator{}))\n\t// Delete a user\n\tws.Route(ws.DELETE(\"/user/{user-id}\").To(s.deleteUser).\n\t\tDoc(\"Delete a user. His or her feedback will also be deleted.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{UsersAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"user-id\", \"User ID\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", Success{}).\n\t\tWrites(Success{}))\n\n\t// Insert an item\n\tws.Route(ws.POST(\"/item\").To(s.insertItem).\n\t\tDoc(\"Insert an item.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{ItemsAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tReads(data.Item{}).\n\t\tReturns(http.StatusOK, \"OK\", Success{}).\n\t\tWrites(Success{}))\n\t// Modify an item\n\tws.Route(ws.PATCH(\"/item/{item-id}\").To(s.modifyItem).\n\t\tDoc(\"Modify an item.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{ItemsAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"item-id\", \"Item ID\").DataType(\"string\")).\n\t\tReads(data.ItemPatch{}).\n\t\tReturns(http.StatusOK, \"OK\", Success{}).\n\t\tWrites(Success{}))\n\t// Get items\n\tws.Route(ws.GET(\"/items\").To(s.getItems).\n\t\tDoc(\"List items.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{ItemsAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"Number of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"cursor\", \"Cursor for the next page\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", ItemIterator{}).\n\t\tWrites(ItemIterator{}))\n\t// Get item\n\tws.Route(ws.GET(\"/item/{item-id}\").To(s.getItem).\n\t\tDoc(\"Get an item.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{ItemsAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"item-id\", \"Item ID.\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", data.Item{}).\n\t\tWrites(data.Item{}))\n\t// Insert items\n\tws.Route(ws.POST(\"/items\").To(s.insertItems).\n\t\tDoc(\"Insert items.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{ItemsAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tReads([]data.Item{}).\n\t\tReturns(http.StatusOK, \"OK\", Success{}).\n\t\tWrites(Success{}))\n\t// Delete item\n\tws.Route(ws.DELETE(\"/item/{item-id}\").To(s.deleteItem).\n\t\tDoc(\"Delete an item and its feedback.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{ItemsAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"item-id\", \"Item ID\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", Success{}).\n\t\tWrites(Success{}))\n\t// Insert category\n\tws.Route(ws.PUT(\"/item/{item-id}/category/{category}\").To(s.insertItemCategory).\n\t\tDoc(\"Insert a category for a item.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{ItemsAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"item-id\", \"Item ID\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"category\", \"Category to insert\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", Success{}).\n\t\tWrites(Success{}))\n\t// Delete category\n\tws.Route(ws.DELETE(\"/item/{item-id}/category/{category}\").To(s.deleteItemCategory).\n\t\tDoc(\"Delete a category from a item.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{ItemsAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"item-id\", \"Item ID\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"category\", \"Category to delete\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", Success{}).\n\t\tWrites(Success{}))\n\n\t// Insert feedback\n\tws.Route(ws.POST(\"/feedback\").To(s.insertFeedback(false)).\n\t\tDoc(\"Insert feedbacks. Accumulate value if feedback already exists.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{FeedbackAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tReads([]data.Feedback{}).\n\t\tReturns(http.StatusOK, \"OK\", Success{}).\n\t\tWrites(Success{}))\n\tws.Route(ws.PUT(\"/feedback\").To(s.insertFeedback(true)).\n\t\tDoc(\"Insert feedbacks. Existed feedback will be overwritten.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{FeedbackAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tReads([]data.Feedback{}).\n\t\tReturns(http.StatusOK, \"OK\", Success{}).\n\t\tWrites(Success{}))\n\t// Get feedback\n\tws.Route(ws.GET(\"/feedback\").To(s.getFeedback).\n\t\tDoc(\"List feedbacks.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{FeedbackAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"cursor\", \"Cursor for the next page\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"Number of returned feedback\").DataType(\"integer\")).\n\t\tReturns(http.StatusOK, \"OK\", FeedbackIterator{}).\n\t\tWrites(FeedbackIterator{}))\n\tws.Route(ws.GET(\"/feedback/{user-id}/{item-id}\").To(s.getUserItemFeedback).\n\t\tDoc(\"List feedbacks between a user and a item.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{FeedbackAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"user-id\", \"User ID\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"item-id\", \"Item ID\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", []data.Feedback{}).\n\t\tWrites([]data.Feedback{}))\n\tws.Route(ws.DELETE(\"/feedback/{user-id}/{item-id}\").To(s.deleteUserItemFeedback).\n\t\tDoc(\"Delete feedbacks between a user and a item.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{FeedbackAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"user-id\", \"User ID\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"item-id\", \"Item ID\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", Success{}).\n\t\tWrites(Success{}))\n\tws.Route(ws.GET(\"/feedback/{feedback-type}\").To(s.getTypedFeedback).\n\t\tDoc(\"List feedbacks with feedback type.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{FeedbackAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"feedback-type\", \"Feedback type\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"cursor\", \"Cursor for the next page\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"Number of returned feedbacks\").DataType(\"integer\")).\n\t\tReturns(http.StatusOK, \"OK\", FeedbackIterator{}).\n\t\tWrites(FeedbackIterator{}))\n\tws.Route(ws.GET(\"/feedback/{feedback-type}/{user-id}/{item-id}\").To(s.getTypedUserItemFeedback).\n\t\tDoc(\"Get feedbacks between a user and a item with feedback type.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{FeedbackAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"feedback-type\", \"Feedback type\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"user-id\", \"User ID\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"item-id\", \"Item ID\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", data.Feedback{}).\n\t\tWrites(data.Feedback{}))\n\tws.Route(ws.DELETE(\"/feedback/{feedback-type}/{user-id}/{item-id}\").To(s.deleteTypedUserItemFeedback).\n\t\tDoc(\"Delete feedbacks between a user and a item with feedback type.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{FeedbackAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"feedback-type\", \"Feedback type\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"user-id\", \"User ID\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"item-id\", \"Item ID\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", Success{}).\n\t\tWrites(Success{}))\n\t// Get feedback by user id\n\tws.Route(ws.GET(\"/user/{user-id}/feedback/{feedback-type}\").To(s.getTypedFeedbackByUser).\n\t\tDoc(\"Get feedbacks by user id with feedback type.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{FeedbackAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"user-id\", \"User ID\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"feedback-type\", \"Feedback type\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", []data.Feedback{}).\n\t\tWrites([]data.Feedback{}))\n\tws.Route(ws.GET(\"/user/{user-id}/feedback\").To(s.getFeedbackByUser).\n\t\tDoc(\"Get feedbacks by user id.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{FeedbackAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"user-id\", \"User ID\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", []data.Feedback{}).\n\t\tWrites([]data.Feedback{}))\n\t// Get feedback by item-id\n\tws.Route(ws.GET(\"/item/{item-id}/feedback/{feedback-type}\").To(s.getTypedFeedbackByItem).\n\t\tDoc(\"Get feedbacks by item id with feedback type.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{FeedbackAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"item-id\", \"Item ID\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"feedback-type\", \"Feedback type\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", []data.Feedback{}).\n\t\tWrites([]data.Feedback{}))\n\tws.Route(ws.GET(\"/item/{item-id}/feedback/\").To(s.getFeedbackByItem).\n\t\tDoc(\"Get feedbacks by item id.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{FeedbackAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"item-id\", \"Item ID\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", []data.Feedback{}).\n\t\tWrites([]data.Feedback{}))\n\n\t// Get collaborative filtering recommendation by user id\n\tws.Route(ws.GET(\"/collaborative-filtering/{user-id}\").To(s.getCollaborativeFiltering).\n\t\tDoc(\"Get collaborative filtering recommendation for a user.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{RecommendationAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"user-id\", \"User ID\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"Number of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"offset\", \"Offset of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"category\", \"Category of returned items.\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"user-id\", \"Remove read items of a user\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", []cache.Score{}).\n\t\tWrites([]cache.Score{}))\n\tws.Route(ws.GET(\"/collaborative-filtering/{user-id}/{category}\").To(s.getCollaborativeFiltering).\n\t\tDeprecate().Doc(\"Get the collaborative filtering recommendation for a user.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{RecommendationAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"user-id\", \"User ID\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"category\", \"Category of returned items.\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"Number of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"offset\", \"Offset of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"user-id\", \"Remove read items of a user\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", []cache.Score{}).\n\t\tWrites([]cache.Score{}))\n\t// Get latest items\n\tws.Route(ws.GET(\"/latest\").To(s.getLatest).\n\t\tDoc(\"Get latest items.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{RecommendationAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"category\", \"Category of returned items\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"Number of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"offset\", \"Offset of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"user-id\", \"Remove read items of a user\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", []cache.Score{}).\n\t\tWrites([]cache.Score{}))\n\tws.Route(ws.GET(\"/latest/{category}\").To(s.getLatest).\n\t\tDeprecate().Doc(\"Get the latest items in category.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{RecommendationAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"category\", \"Category of returned items.\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"Number of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"offset\", \"Offset of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"user-id\", \"Remove read items of a user\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", []cache.Score{}).\n\t\tWrites([]cache.Score{}))\n\t// Get non-personalized\n\tws.Route(ws.GET(\"/non-personalized/{name}\").To(s.getNonPersonalized).\n\t\tDoc(\"Get non-personalized recommendations.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{RecommendationAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"category\", \"Category of returned items.\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"Number of returned users\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"offset\", \"Offset of returned users\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"user-id\", \"Remove read items of a user\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", []cache.Score{}).\n\t\tWrites([]cache.Score{}))\n\t// Get item-to-item recommendation\n\tws.Route(ws.GET(\"/item-to-item/{name}/{item-id}\").To(s.getItemToItem).\n\t\tDoc(\"Get item-to-item recommendation.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{RecommendationAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"name\", \"Name of the item-to-item recommendation\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"item-id\", \"Item ID\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"Number of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"offset\", \"Offset of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"category\", \"Category of returned items\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"user-id\", \"Remove read items of a user\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", []cache.Score{}).\n\t\tWrites([]cache.Score{}))\n\t// Get user-to-user recommendation\n\tws.Route(ws.GET(\"/user-to-user/{name}/{user-id}\").To(s.getUserToUser).\n\t\tDoc(\"Get user-to-user recommendation.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{RecommendationAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"name\", \"Name of the user-to-user recommendation\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"user-id\", \"User ID\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"Number of returned users\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"offset\", \"Offset of returned users\").DataType(\"integer\")).\n\t\tReturns(http.StatusOK, \"OK\", []cache.Score{}).\n\t\tWrites([]cache.Score{}))\n\t// Get neighbors\n\tws.Route(ws.GET(\"/item/{item-id}/neighbors/\").To(s.getItemNeighbors).\n\t\tDoc(\"Get neighbors of a item\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{RecommendationAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"item-id\", \"Item ID\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"Number of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"offset\", \"Offset of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"category\", \"Category of returned items\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"user-id\", \"Remove read items of a user\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", []cache.Score{}).\n\t\tWrites([]cache.Score{}))\n\tws.Route(ws.GET(\"/item/{item-id}/neighbors/{category}\").To(s.getItemNeighbors).\n\t\tDeprecate().Doc(\"Get neighbors of a item in category.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{RecommendationAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"item-id\", \"Item ID\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"category\", \"Category of returned items\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"Number of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"offset\", \"Offset of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"user-id\", \"Remove read items of a user\").DataType(\"string\")).\n\t\tReturns(http.StatusOK, \"OK\", []cache.Score{}).\n\t\tWrites([]cache.Score{}))\n\tws.Route(ws.GET(\"/user/{user-id}/neighbors/\").To(s.getUserNeighbors).\n\t\tDoc(\"Get neighbors of a user.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{RecommendationAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"user-id\", \"User ID\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"Number of returned users\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"offset\", \"Offset of returned users\").DataType(\"integer\")).\n\t\tReturns(http.StatusOK, \"OK\", []cache.Score{}).\n\t\tWrites([]cache.Score{}))\n\tws.Route(ws.GET(\"/recommend/{user-id}\").To(s.getRecommend).\n\t\tDoc(\"Get recommendation for user. Set X-API-Version: 2 to return scores.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{RecommendationAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.HeaderParameter(\"X-API-Version\", \"API version (set to 2 to return scores)\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"user-id\", \"User ID\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"category\", \"Category of the returned items (support multi-categories filtering)\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"write-back-type\", \"Type of write back feedback\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"write-back-delay\", \"Timestamp delay of write back feedback (format 0h0m0s)\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"Number of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"offset\", \"Offset of returned items\").DataType(\"integer\")).\n\t\tReturns(http.StatusOK, \"OK\", []string{}).\n\t\tWrites([]string{}))\n\tws.Route(ws.GET(\"/recommend/{user-id}/{category}\").To(s.getRecommend).\n\t\tDeprecate().Doc(\"Get recommendation for user. Set X-API-Version: 2 to return scores.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{RecommendationAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.HeaderParameter(\"X-API-Version\", \"API version (set to 2 to return scores)\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"user-id\", \"User ID\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"category\", \"Category of the returned items\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"write-back-type\", \"Type of write back feedback\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"write-back-delay\", \"Timestamp delay of write back feedback (format 0h0m0s)\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"Number of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"offset\", \"Offset of returned items\").DataType(\"integer\")).\n\t\tReturns(http.StatusOK, \"OK\", []string{}).\n\t\tWrites([]string{}))\n\tws.Route(ws.POST(\"/session/recommend\").To(s.sessionRecommend).\n\t\tDoc(\"Get recommendation for session.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{RecommendationAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"Number of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"offset\", \"Offset of returned items\").DataType(\"integer\")).\n\t\tReads([]Feedback{}).\n\t\tReturns(http.StatusOK, \"OK\", []cache.Score{}).\n\t\tWrites([]cache.Score{}))\n\tws.Route(ws.POST(\"/session/recommend/{category}\").To(s.sessionRecommend).\n\t\tDeprecate().Doc(\"Get recommendation for session.\").\n\t\tMetadata(restfulspec.KeyOpenAPITags, []string{RecommendationAPITag}).\n\t\tParam(ws.HeaderParameter(\"X-API-Key\", \"API key\").DataType(\"string\")).\n\t\tParam(ws.PathParameter(\"category\", \"Category of the returned items\").DataType(\"string\")).\n\t\tParam(ws.QueryParameter(\"n\", \"Number of returned items\").DataType(\"integer\")).\n\t\tParam(ws.QueryParameter(\"offset\", \"Offset of returned items\").DataType(\"integer\")).\n\t\tReads([]Feedback{}).\n\t\tReturns(http.StatusOK, \"OK\", []cache.Score{}).\n\t\tWrites([]cache.Score{}))\n}\n\n// ParseInt parses integers from the query parameter.\nfunc ParseInt(request *restful.Request, name string, fallback int) (value int, err error) {\n\tvalueString := request.QueryParameter(name)\n\tvalue, err = strconv.Atoi(valueString)\n\tif err != nil && valueString == \"\" {\n\t\tvalue = fallback\n\t\terr = nil\n\t}\n\treturn\n}\n\n// ParseDuration parses duration from the query parameter.\nfunc ParseDuration(request *restful.Request, name string) (time.Duration, error) {\n\tvalueString := request.QueryParameter(name)\n\tif valueString == \"\" {\n\t\treturn 0, nil\n\t}\n\treturn time.ParseDuration(valueString)\n}\n\nfunc (s *RestServer) SearchDocuments(collection, subset string, categories []string,\n\titeratee func(item cache.Score) (any, error),\n\trequest *restful.Request, response *restful.Response,\n) {\n\tvar (\n\t\tctx    = request.Request.Context()\n\t\tn      int\n\t\toffset int\n\t\tuserId string\n\t\terr    error\n\t)\n\n\t// parse arguments\n\tif offset, err = ParseInt(request, \"offset\", 0); err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\tif n, err = ParseInt(request, \"n\", s.Config.Server.DefaultN); err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\tuserId = request.QueryParameter(\"user-id\")\n\n\treadItems := mapset.NewSet[string]()\n\tif userId != \"\" {\n\t\tfeedback, err := s.DataClient.GetUserFeedback(ctx, userId, s.Config.Now())\n\t\tif err != nil {\n\t\t\tInternalServerError(response, err)\n\t\t\treturn\n\t\t}\n\t\tfor _, f := range feedback {\n\t\t\treadItems.Add(f.ItemId)\n\t\t}\n\t}\n\n\tend := offset + n\n\tif end > 0 && readItems.Cardinality() > 0 {\n\t\tend += readItems.Cardinality()\n\t}\n\n\t// Get the sorted list\n\titems, err := s.CacheClient.SearchScores(ctx, collection, subset, categories, offset, end)\n\tif err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\n\t// Remove read items\n\tif userId != \"\" {\n\t\tprunedItems := make([]cache.Score, 0, len(items))\n\t\tfor _, item := range items {\n\t\t\tif !readItems.Contains(item.Id) {\n\t\t\t\tprunedItems = append(prunedItems, item)\n\t\t\t}\n\t\t}\n\t\titems = prunedItems\n\t}\n\n\t// Send result\n\tif n > 0 && len(items) > n {\n\t\titems = items[:n]\n\t}\n\tif iteratee != nil {\n\t\tvar results []any\n\t\tfor _, item := range items {\n\t\t\tresult, err := iteratee(item)\n\t\t\tif err != nil {\n\t\t\t\tInternalServerError(response, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tresults = append(results, result)\n\t\t}\n\t\tOk(response, results)\n\t} else {\n\t\tOk(response, items)\n\t}\n}\n\nfunc (s *RestServer) getLatest(request *restful.Request, response *restful.Response) {\n\tvar (\n\t\toffset int\n\t\tn      int\n\t\terr    error\n\t)\n\tctx := request.Request.Context()\n\tif offset, err = ParseInt(request, \"offset\", 0); err != nil {\n\t\tBadRequest(response, errors.Errorf(\"invalid offset parameter: %v\", err))\n\t\treturn\n\t}\n\tif n, err = ParseInt(request, \"n\", s.Config.Server.DefaultN); err != nil {\n\t\tBadRequest(response, errors.Errorf(\"invalid n parameter: %v\", err))\n\t\treturn\n\t}\n\tcategories := ReadCategories(request, nil)\n\tuserId := request.QueryParameter(\"user-id\")\n\n\treadItems := mapset.NewSet[string]()\n\tif userId != \"\" {\n\t\tfeedback, err := s.DataClient.GetUserFeedback(ctx, userId, s.Config.Now())\n\t\tif err != nil {\n\t\t\tInternalServerError(response, err)\n\t\t\treturn\n\t\t}\n\t\tfor _, f := range feedback {\n\t\t\treadItems.Add(f.ItemId)\n\t\t}\n\t}\n\n\tlimit := offset + n\n\tif readItems.Cardinality() > 0 {\n\t\tlimit += readItems.Cardinality()\n\t}\n\n\titems, err := s.DataClient.GetLatestItems(ctx, limit, categories)\n\tif err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\n\tif readItems.Cardinality() > 0 {\n\t\tfiltered := make([]data.Item, 0, len(items))\n\t\tfor _, item := range items {\n\t\t\tif !readItems.Contains(item.ItemId) {\n\t\t\t\tfiltered = append(filtered, item)\n\t\t\t}\n\t\t}\n\t\titems = filtered\n\t}\n\n\titems = items[min(offset, len(items)):]\n\tif n > 0 && len(items) > n {\n\t\titems = items[:n]\n\t}\n\n\tOk(response, lo.Map(items, func(item data.Item, _ int) cache.Score {\n\t\treturn cache.Score{\n\t\t\tId:    item.ItemId,\n\t\t\tScore: float64(item.Timestamp.Unix()),\n\t\t}\n\t}))\n}\n\nfunc (s *RestServer) getNonPersonalized(request *restful.Request, response *restful.Response) {\n\tname := request.PathParameter(\"name\")\n\tcategories := ReadCategories(request, []string{\"\"})\n\tlog.ResponseLogger(response).Debug(\"get leaderboard\", zap.String(\"name\", name))\n\ts.SetLastModified(request, response, cache.Key(cache.NonPersonalizedUpdateTime, name))\n\ts.SearchDocuments(cache.NonPersonalized, name, categories, nil, request, response)\n}\n\nfunc (s *RestServer) getItemToItem(request *restful.Request, response *restful.Response) {\n\tname := request.PathParameter(\"name\")\n\titemId := request.PathParameter(\"item-id\")\n\tcategories := request.QueryParameters(\"category\")\n\ts.SetLastModified(request, response, cache.Key(cache.ItemToItemUpdateTime, name, itemId))\n\ts.SearchDocuments(cache.ItemToItem, cache.Key(name, itemId), categories, nil, request, response)\n}\n\nfunc (s *RestServer) getUserToUser(request *restful.Request, response *restful.Response) {\n\tname := request.PathParameter(\"name\")\n\tuserId := request.PathParameter(\"user-id\")\n\ts.SetLastModified(request, response, cache.Key(cache.UserToUserUpdateTime, name, userId))\n\ts.SearchDocuments(cache.UserToUser, cache.Key(name, userId), nil, nil, request, response)\n}\n\nfunc (s *RestServer) SetLastModified(request *restful.Request, response *restful.Response, key string) {\n\tlastModified, err := s.CacheClient.Get(request.Request.Context(), key).Time()\n\tif err != nil {\n\t\tlog.ResponseLogger(response).Error(\"failed to get last modified time\", zap.Error(err))\n\t\treturn\n\t}\n\tresponse.AddHeader(\"Last-Modified\", lastModified.Format(time.RFC1123))\n}\n\n// get feedback by item-id with feedback type\nfunc (s *RestServer) getTypedFeedbackByItem(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\tfeedbackType := request.PathParameter(\"feedback-type\")\n\titemId := request.PathParameter(\"item-id\")\n\tfeedback, err := s.DataClient.GetItemFeedback(ctx, itemId, feedbackType)\n\tif err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\tOk(response, feedback)\n}\n\n// get feedback by item-id\nfunc (s *RestServer) getFeedbackByItem(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\titemId := request.PathParameter(\"item-id\")\n\tfeedback, err := s.DataClient.GetItemFeedback(ctx, itemId)\n\tif err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\tOk(response, feedback)\n}\n\n// getItemNeighbors gets neighbors of a item from database.\nfunc (s *RestServer) getItemNeighbors(request *restful.Request, response *restful.Response) {\n\t// Get item id\n\titemId := request.PathParameter(\"item-id\")\n\tcategories := ReadCategories(request, nil)\n\tif len(s.Config.Recommend.ItemToItem) == 0 {\n\t\tPageNotFound(response, errors.New(\"item-to-item recommendation is not enabled\"))\n\t\treturn\n\t} else {\n\t\tname := s.Config.Recommend.ItemToItem[0].Name\n\t\ts.SetLastModified(request, response, cache.Key(cache.ItemToItemUpdateTime, name, itemId))\n\t\ts.SearchDocuments(cache.ItemToItem, cache.Key(name, itemId), categories, nil, request, response)\n\t}\n}\n\n// getUserNeighbors gets neighbors of a user from database.\nfunc (s *RestServer) getUserNeighbors(request *restful.Request, response *restful.Response) {\n\t// Get item id\n\tuserId := request.PathParameter(\"user-id\")\n\tif len(s.Config.Recommend.UserToUser) == 0 {\n\t\tPageNotFound(response, errors.New(\"user-to-user recommendation is not enabled\"))\n\t\treturn\n\t} else {\n\t\tname := s.Config.Recommend.UserToUser[0].Name\n\t\ts.SetLastModified(request, response, cache.Key(cache.UserToUserUpdateTime, name, userId))\n\t\ts.SearchDocuments(cache.UserToUser, cache.Key(name, userId), nil, nil, request, response)\n\t}\n}\n\n// getCollaborativeFiltering gets cached recommended items from database.\nfunc (s *RestServer) getCollaborativeFiltering(request *restful.Request, response *restful.Response) {\n\tif strings.EqualFold(s.Config.Recommend.Collaborative.Type, \"none\") {\n\t\tPageNotFound(response, errors.New(\"collaborative filtering recommendation is disabled\"))\n\t\treturn\n\t}\n\t// Get user id\n\tuserId := request.PathParameter(\"user-id\")\n\tcategories := ReadCategories(request, nil)\n\ts.SetLastModified(request, response, cache.Key(cache.RecommendUpdateTime, userId))\n\ts.SearchDocuments(cache.Recommend, userId, categories, nil, request, response)\n}\n\nfunc (s *RestServer) getRecommend(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\t// parse arguments\n\tuserId := request.PathParameter(\"user-id\")\n\tapiVersion := strings.TrimSpace(request.HeaderParameter(\"X-API-Version\"))\n\tn, err := ParseInt(request, \"n\", s.Config.Server.DefaultN)\n\tif err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\tcategories := ReadCategories(request, nil)\n\toffset, err := ParseInt(request, \"offset\", 0)\n\tif err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\twriteBackFeedback := request.QueryParameter(\"write-back-type\")\n\twriteBackDelay, err := ParseDuration(request, \"write-back-delay\")\n\tif err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\t// online recommendation\n\trecommender, err := logics.NewRecommender(s.Config.Recommend, s.CacheClient, s.DataClient, true, userId, categories)\n\tif err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\tscores, err := recommender.Recommend(ctx, n+offset)\n\tif err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\tif len(scores) > offset {\n\t\tscores = scores[offset:]\n\t} else {\n\t\tscores = []cache.Score{}\n\t}\n\tresults := lo.Map(scores, func(item cache.Score, index int) string {\n\t\treturn item.Id\n\t})\n\t// write back\n\tif writeBackFeedback != \"\" {\n\t\tstartTime := time.Now()\n\t\tfor _, itemId := range results {\n\t\t\t// insert to data store\n\t\t\tfeedback := data.Feedback{\n\t\t\t\tFeedbackKey: data.FeedbackKey{\n\t\t\t\t\tUserId:       userId,\n\t\t\t\t\tItemId:       itemId,\n\t\t\t\t\tFeedbackType: writeBackFeedback,\n\t\t\t\t},\n\t\t\t\tTimestamp: startTime.Add(writeBackDelay),\n\t\t\t}\n\t\t\terr = s.DataClient.BatchInsertFeedback(ctx, []data.Feedback{feedback}, false, false, false)\n\t\t\tif err != nil {\n\t\t\t\tInternalServerError(response, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\t// Send result\n\tif apiVersion == \"2\" {\n\t\tOk(response, scores)\n\t\treturn\n\t}\n\tOk(response, results)\n}\n\nfunc (s *RestServer) sessionRecommend(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif len(s.Config.Recommend.ItemToItem) == 0 {\n\t\tPageNotFound(response, errors.New(\"item-to-item recommendation is not enabled\"))\n\t\treturn\n\t}\n\tname := s.Config.Recommend.ItemToItem[0].Name\n\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\t// parse arguments\n\tvar feedbacks []Feedback\n\tif err := request.ReadEntity(&feedbacks); err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\tn, err := ParseInt(request, \"n\", s.Config.Server.DefaultN)\n\tif err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\tcategory := request.PathParameter(\"category\")\n\toffset, err := ParseInt(request, \"offset\", 0)\n\tif err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\n\t// pre-process feedback\n\tdataFeedback := make([]data.Feedback, len(feedbacks))\n\tfor i := range dataFeedback {\n\t\tvar err error\n\t\tdataFeedback[i], err = feedbacks[i].ToDataFeedback()\n\t\tif err != nil {\n\t\t\tBadRequest(response, err)\n\t\t\treturn\n\t\t}\n\t}\n\tdata.SortFeedbacks(dataFeedback)\n\n\t// item-based recommendation\n\tvar excludeSet = mapset.NewSet[string]()\n\tvar userFeedback []data.Feedback\n\tfor _, feedback := range dataFeedback {\n\t\texcludeSet.Add(feedback.ItemId)\n\t\tif expression.MatchFeedbackTypeExpressions(s.Config.Recommend.DataSource.PositiveFeedbackTypes, feedback.FeedbackType, feedback.Value) {\n\t\t\tuserFeedback = append(userFeedback, feedback)\n\t\t}\n\t}\n\t// collect candidates\n\tcandidates := make(map[string]float64)\n\tusedFeedbackCount := 0\n\tfor _, feedback := range userFeedback {\n\t\t// load similar items\n\t\tsimilarItems, err := s.CacheClient.SearchScores(ctx, cache.ItemToItem, cache.Key(name, feedback.ItemId), []string{category}, 0, s.Config.Recommend.CacheSize)\n\t\tif err != nil {\n\t\t\tBadRequest(response, err)\n\t\t\treturn\n\t\t}\n\t\t// add unseen items\n\t\t// similarItems = s.FilterOutHiddenScores(response, similarItems, \"\")\n\t\tfor _, item := range similarItems {\n\t\t\tif !excludeSet.Contains(item.Id) {\n\t\t\t\tcandidates[item.Id] += item.Score\n\t\t\t}\n\t\t}\n\t\t// finish recommendation if the number of used feedbacks is enough\n\t\tif len(similarItems) > 0 {\n\t\t\tusedFeedbackCount++\n\t\t\tif usedFeedbackCount >= s.Config.Recommend.ContextSize {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\t// collect top k\n\tfilter := heap.NewTopKFilter[string, float64](n + offset)\n\tfor id, score := range candidates {\n\t\tfilter.Push(id, score)\n\t}\n\tscores := filter.PopAll()\n\tresult := lo.Map(scores, func(score heap.Elem[string, float64], _ int) cache.Score {\n\t\treturn cache.Score{\n\t\t\tId:    score.Value,\n\t\t\tScore: score.Weight,\n\t\t}\n\t})\n\tif len(result) > offset {\n\t\tresult = result[offset:]\n\t} else {\n\t\tresult = nil\n\t}\n\tresult = result[:lo.Min([]int{len(result), n})]\n\t// Send result\n\tOk(response, result)\n}\n\n// Success is the returned data structure for data insert operations.\ntype Success struct {\n\tRowAffected int\n}\n\nfunc (s *RestServer) insertUser(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\ttemp := data.User{}\n\t// get userInfo from request and put into temp\n\tif err := request.ReadEntity(&temp); err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\t// validate labels\n\tif err := data.ValidateLabels(temp.Labels); err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\tif err := s.DataClient.BatchInsertUsers(ctx, []data.User{temp}); err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\t// insert modify timestamp\n\tif err := s.CacheClient.Set(ctx, cache.Time(cache.Key(cache.LastModifyUserTime, temp.UserId), time.Now())); err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\tOk(response, Success{RowAffected: 1})\n}\n\nfunc (s *RestServer) modifyUser(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\t// get user id\n\tuserId := request.PathParameter(\"user-id\")\n\t// modify user\n\tvar patch data.UserPatch\n\tif err := request.ReadEntity(&patch); err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\t// validate labels\n\tif err := data.ValidateLabels(patch.Labels); err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\tif err := s.DataClient.ModifyUser(ctx, userId, patch); err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\t// insert modify timestamp\n\tif err := s.CacheClient.Set(ctx, cache.Time(cache.Key(cache.LastModifyUserTime, userId), time.Now())); err != nil {\n\t\treturn\n\t}\n\tOk(response, Success{RowAffected: 1})\n}\n\nfunc (s *RestServer) getUser(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\t// get user id\n\tuserId := request.PathParameter(\"user-id\")\n\t// get user\n\tuser, err := s.DataClient.GetUser(ctx, userId)\n\tif err != nil {\n\t\tif errors.Is(err, errors.NotFound) {\n\t\t\tPageNotFound(response, err)\n\t\t} else {\n\t\t\tInternalServerError(response, err)\n\t\t}\n\t\treturn\n\t}\n\tOk(response, user)\n}\n\nfunc (s *RestServer) insertUsers(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\tvar temp []data.User\n\t// get param from request and put into temp\n\tif err := request.ReadEntity(&temp); err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\t// validate labels\n\tfor _, user := range temp {\n\t\tif err := data.ValidateLabels(user.Labels); err != nil {\n\t\t\tBadRequest(response, err)\n\t\t\treturn\n\t\t}\n\t}\n\t// range temp and achieve user\n\tif err := s.DataClient.BatchInsertUsers(ctx, temp); err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\t// insert modify timestamp\n\tvalues := make([]cache.Value, len(temp))\n\tfor i, user := range temp {\n\t\tvalues[i] = cache.Time(cache.Key(cache.LastModifyUserTime, user.UserId), time.Now())\n\t}\n\tif err := s.CacheClient.Set(ctx, values...); err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\tOk(response, Success{RowAffected: len(temp)})\n}\n\ntype UserIterator struct {\n\tCursor string\n\tUsers  []data.User\n}\n\nfunc (s *RestServer) getUsers(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\tcursor := request.QueryParameter(\"cursor\")\n\tn, err := ParseInt(request, \"n\", s.Config.Server.DefaultN)\n\tif err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\t// get all users\n\tcursor, users, err := s.DataClient.GetUsers(ctx, cursor, n)\n\tif err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\tOk(response, UserIterator{Cursor: cursor, Users: users})\n}\n\n// delete a user by user-id\nfunc (s *RestServer) deleteUser(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\t// get user-id and put into temp\n\tuserId := request.PathParameter(\"user-id\")\n\tif err := s.DataClient.DeleteUser(ctx, userId); err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\tOk(response, Success{RowAffected: 1})\n}\n\n// get feedback by user-id with feedback type\nfunc (s *RestServer) getTypedFeedbackByUser(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\tfeedbackType := request.PathParameter(\"feedback-type\")\n\tvar feednackTypeExpr expression.FeedbackTypeExpression\n\tif err := feednackTypeExpr.FromString(feedbackType); err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\tuserId := request.PathParameter(\"user-id\")\n\tfeedback, err := s.DataClient.GetUserFeedback(ctx, userId, s.Config.Now(), feednackTypeExpr)\n\tif err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\tOk(response, feedback)\n}\n\n// get feedback by user-id\nfunc (s *RestServer) getFeedbackByUser(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\tuserId := request.PathParameter(\"user-id\")\n\tfeedback, err := s.DataClient.GetUserFeedback(ctx, userId, s.Config.Now())\n\tif err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\tOk(response, feedback)\n}\n\n// Item is the data structure for the item but stores the timestamp using string.\ntype Item struct {\n\tItemId     string\n\tIsHidden   bool\n\tCategories []string\n\tTimestamp  string\n\tLabels     any\n\tComment    string\n}\n\nfunc (s *RestServer) batchInsertItems(ctx context.Context, response *restful.Response, temp []Item) {\n\tvar (\n\t\tcount                int\n\t\titems                = make([]data.Item, 0, len(temp))\n\t\tloadExistedItemsTime time.Duration\n\t\tparseTimesatmpTime   time.Duration\n\t\tinsertItemsTime      time.Duration\n\t\tinsertCacheTime      time.Duration\n\t)\n\t// load existed items\n\tstart := time.Now()\n\texistedItems, err := s.DataClient.BatchGetItems(ctx, lo.Map(temp, func(t Item, i int) string {\n\t\treturn t.ItemId\n\t}))\n\tif err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\texistedItemsSet := make(map[string]data.Item)\n\tfor _, item := range existedItems {\n\t\texistedItemsSet[item.ItemId] = item\n\t}\n\tloadExistedItemsTime = time.Since(start)\n\n\tstart = time.Now()\n\tfor _, item := range temp {\n\t\t// parse datetime\n\t\tvar timestamp time.Time\n\t\tvar err error\n\t\tif item.Timestamp != \"\" {\n\t\t\tif timestamp, err = dateparse.ParseAny(item.Timestamp); err != nil {\n\t\t\t\tBadRequest(response, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\titems = append(items, data.Item{\n\t\t\tItemId:     item.ItemId,\n\t\t\tIsHidden:   item.IsHidden,\n\t\t\tCategories: item.Categories,\n\t\t\tTimestamp:  timestamp,\n\t\t\tLabels:     item.Labels,\n\t\t\tComment:    item.Comment,\n\t\t})\n\t\t// update items cache\n\t\tif err = s.CacheClient.UpdateScores(ctx, cache.ItemCache, nil, item.ItemId, cache.ScorePatch{\n\t\t\tCategories: withWildCard(item.Categories),\n\t\t\tIsHidden:   &item.IsHidden,\n\t\t}); err != nil {\n\t\t\tInternalServerError(response, err)\n\t\t\treturn\n\t\t}\n\t\tcount++\n\t}\n\tparseTimesatmpTime = time.Since(start)\n\n\t// insert items\n\tstart = time.Now()\n\tif err = s.DataClient.BatchInsertItems(ctx, items); err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\tinsertItemsTime = time.Since(start)\n\n\t// insert modify timestamp\n\tstart = time.Now()\n\tvalues := make([]cache.Value, len(items))\n\tfor i, item := range items {\n\t\tvalues[i] = cache.Time(cache.Key(cache.LastModifyItemTime, item.ItemId), time.Now())\n\t}\n\tif err = s.CacheClient.Set(ctx, values...); err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\n\tinsertCacheTime = time.Since(start)\n\tlog.ResponseLogger(response).Info(\"batch insert items\",\n\t\tzap.Duration(\"load_existed_items_time\", loadExistedItemsTime),\n\t\tzap.Duration(\"parse_timestamp_time\", parseTimesatmpTime),\n\t\tzap.Duration(\"insert_items_time\", insertItemsTime),\n\t\tzap.Duration(\"insert_cache_time\", insertCacheTime))\n\tOk(response, Success{RowAffected: count})\n}\n\nfunc (s *RestServer) insertItems(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\tvar items []Item\n\tif err := request.ReadEntity(&items); err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\t// validate labels\n\tfor _, user := range items {\n\t\tif err := data.ValidateLabels(user.Labels); err != nil {\n\t\t\tBadRequest(response, err)\n\t\t\treturn\n\t\t}\n\t}\n\t// Insert items\n\ts.batchInsertItems(ctx, response, items)\n}\n\nfunc (s *RestServer) insertItem(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\tvar item Item\n\tvar err error\n\tif err = request.ReadEntity(&item); err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\t// validate labels\n\tif err := data.ValidateLabels(item.Labels); err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\ts.batchInsertItems(ctx, response, []Item{item})\n}\n\nfunc (s *RestServer) modifyItem(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\titemId := request.PathParameter(\"item-id\")\n\tvar patch data.ItemPatch\n\tif err := request.ReadEntity(&patch); err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\t// validate labels\n\tif err := data.ValidateLabels(patch.Labels); err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\t// remove hidden item from cache\n\tif patch.IsHidden != nil {\n\t\tif err := s.CacheClient.UpdateScores(ctx, cache.ItemCache, nil, itemId, cache.ScorePatch{IsHidden: patch.IsHidden}); err != nil {\n\t\t\tInternalServerError(response, err)\n\t\t\treturn\n\t\t}\n\t}\n\t// update categories in cache\n\tif patch.Categories != nil {\n\t\tif err := s.CacheClient.UpdateScores(ctx, cache.ItemCache, nil, itemId, cache.ScorePatch{Categories: withWildCard(patch.Categories)}); err != nil {\n\t\t\tInternalServerError(response, err)\n\t\t\treturn\n\t\t}\n\t}\n\t// modify item\n\tif err := s.DataClient.ModifyItem(ctx, itemId, patch); err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\t// insert modify timestamp\n\tif err := s.CacheClient.Set(ctx, cache.Time(cache.Key(cache.LastModifyItemTime, itemId), time.Now())); err != nil {\n\t\treturn\n\t}\n\tOk(response, Success{RowAffected: 1})\n}\n\n// ItemIterator is the iterator for items.\ntype ItemIterator struct {\n\tCursor string\n\tItems  []data.Item\n}\n\nfunc (s *RestServer) getItems(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\tcursor := request.QueryParameter(\"cursor\")\n\tn, err := ParseInt(request, \"n\", s.Config.Server.DefaultN)\n\tif err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\tcursor, items, err := s.DataClient.GetItems(ctx, cursor, n, nil)\n\tif err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\tOk(response, ItemIterator{Cursor: cursor, Items: items})\n}\n\nfunc (s *RestServer) getItem(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\t// Get item id\n\titemId := request.PathParameter(\"item-id\")\n\t// Get item\n\titem, err := s.DataClient.GetItem(ctx, itemId)\n\tif err != nil {\n\t\tif errors.Is(err, errors.NotFound) {\n\t\t\tPageNotFound(response, err)\n\t\t} else {\n\t\t\tInternalServerError(response, err)\n\t\t}\n\t\treturn\n\t}\n\tOk(response, item)\n}\n\nfunc (s *RestServer) deleteItem(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\titemId := request.PathParameter(\"item-id\")\n\t// delete item from database\n\tif err := s.DataClient.DeleteItem(ctx, itemId); err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\t// delete item from cache\n\tif err := s.CacheClient.DeleteScores(ctx, cache.ItemCache, cache.ScoreCondition{Id: &itemId}); err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\tOk(response, Success{RowAffected: 1})\n}\n\nfunc (s *RestServer) insertItemCategory(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\t// fetch item id and category\n\titemId := request.PathParameter(\"item-id\")\n\tcategory := request.PathParameter(\"category\")\n\t// fetch item\n\titem, err := s.DataClient.GetItem(ctx, itemId)\n\tif err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\tif !lo.Contains(item.Categories, category) {\n\t\titem.Categories = append(item.Categories, category)\n\t}\n\t// insert category to database\n\tif err = s.DataClient.BatchInsertItems(ctx, []data.Item{item}); err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\t// insert category to cache\n\tif err = s.CacheClient.UpdateScores(ctx, cache.ItemCache, nil, itemId, cache.ScorePatch{Categories: withWildCard(item.Categories)}); err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\tOk(response, Success{RowAffected: 1})\n}\n\nfunc (s *RestServer) deleteItemCategory(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\t// fetch item id and category\n\titemId := request.PathParameter(\"item-id\")\n\tcategory := request.PathParameter(\"category\")\n\t// fetch item\n\titem, err := s.DataClient.GetItem(ctx, itemId)\n\tif err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\tcategories := make([]string, 0, len(item.Categories))\n\tfor _, cat := range item.Categories {\n\t\tif cat != category {\n\t\t\tcategories = append(categories, cat)\n\t\t}\n\t}\n\titem.Categories = categories\n\t// delete category from cache\n\tif err = s.CacheClient.UpdateScores(ctx, cache.ItemCache, nil, itemId, cache.ScorePatch{Categories: withWildCard(categories)}); err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\t// delete category from database\n\tif err = s.DataClient.BatchInsertItems(ctx, []data.Item{item}); err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\tOk(response, Success{RowAffected: 1})\n}\n\n// Feedback is the data structure for the feedback but stores the timestamp using string.\ntype Feedback struct {\n\tdata.FeedbackKey\n\tValue     float64\n\tTimestamp string\n\tComment   string\n}\n\nfunc (f Feedback) ToDataFeedback() (data.Feedback, error) {\n\tvar feedback data.Feedback\n\tfeedback.FeedbackKey = f.FeedbackKey\n\tfeedback.Value = f.Value\n\tfeedback.Comment = f.Comment\n\tif f.Timestamp != \"\" {\n\t\tvar err error\n\t\tfeedback.Timestamp, err = dateparse.ParseAny(f.Timestamp)\n\t\tif err != nil {\n\t\t\treturn data.Feedback{}, err\n\t\t}\n\t}\n\treturn feedback, nil\n}\n\nfunc (s *RestServer) insertFeedback(overwrite bool) func(request *restful.Request, response *restful.Response) {\n\treturn func(request *restful.Request, response *restful.Response) {\n\t\tctx := context.Background()\n\t\tif request != nil && request.Request != nil {\n\t\t\tctx = request.Request.Context()\n\t\t}\n\t\t// add ratings\n\t\tvar feedbackLiterTime []Feedback\n\t\tif err := request.ReadEntity(&feedbackLiterTime); err != nil {\n\t\t\tBadRequest(response, err)\n\t\t\treturn\n\t\t}\n\t\t// parse datetime\n\t\tvar err error\n\t\tfeedback := make([]data.Feedback, len(feedbackLiterTime))\n\t\tusers := mapset.NewSet[string]()\n\t\titems := mapset.NewSet[string]()\n\t\tfor i := range feedback {\n\t\t\tusers.Add(feedbackLiterTime[i].UserId)\n\t\t\titems.Add(feedbackLiterTime[i].ItemId)\n\t\t\tfeedback[i], err = feedbackLiterTime[i].ToDataFeedback()\n\t\t\tif err != nil {\n\t\t\t\tBadRequest(response, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\t// insert feedback to data store\n\t\terr = s.DataClient.BatchInsertFeedback(ctx, feedback,\n\t\t\ts.Config.Server.AutoInsertUser,\n\t\t\ts.Config.Server.AutoInsertItem, overwrite)\n\t\tif err != nil {\n\t\t\tInternalServerError(response, err)\n\t\t\treturn\n\t\t}\n\t\tvalues := make([]cache.Value, 0, users.Cardinality()+items.Cardinality())\n\t\tfor _, userId := range users.ToSlice() {\n\t\t\tvalues = append(values, cache.Time(cache.Key(cache.LastModifyUserTime, userId), time.Now()))\n\t\t}\n\t\tfor _, itemId := range items.ToSlice() {\n\t\t\tvalues = append(values, cache.Time(cache.Key(cache.LastModifyItemTime, itemId), time.Now()))\n\t\t}\n\t\tif err = s.CacheClient.Set(ctx, values...); err != nil {\n\t\t\tInternalServerError(response, err)\n\t\t\treturn\n\t\t}\n\t\tlog.ResponseLogger(response).Info(\"Insert feedback successfully\", zap.Int(\"num_feedback\", len(feedback)))\n\t\tOk(response, Success{RowAffected: len(feedback)})\n\t}\n}\n\n// FeedbackIterator is the iterator for feedback.\ntype FeedbackIterator struct {\n\tCursor   string\n\tFeedback []data.Feedback\n}\n\nfunc (s *RestServer) getFeedback(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\t// Parse parameters\n\tcursor := request.QueryParameter(\"cursor\")\n\tn, err := ParseInt(request, \"n\", s.Config.Server.DefaultN)\n\tif err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\tcursor, feedback, err := s.DataClient.GetFeedback(ctx, cursor, n, nil, s.Config.Now())\n\tif err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\tOk(response, FeedbackIterator{Cursor: cursor, Feedback: feedback})\n}\n\nfunc (s *RestServer) getTypedFeedback(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\t// Parse parameters\n\tfeedbackType := request.PathParameter(\"feedback-type\")\n\tcursor := request.QueryParameter(\"cursor\")\n\tn, err := ParseInt(request, \"n\", s.Config.Server.DefaultN)\n\tif err != nil {\n\t\tBadRequest(response, err)\n\t\treturn\n\t}\n\tcursor, feedback, err := s.DataClient.GetFeedback(ctx, cursor, n, nil, s.Config.Now(), feedbackType)\n\tif err != nil {\n\t\tInternalServerError(response, err)\n\t\treturn\n\t}\n\tOk(response, FeedbackIterator{Cursor: cursor, Feedback: feedback})\n}\n\nfunc (s *RestServer) getUserItemFeedback(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\t// Parse parameters\n\tuserId := request.PathParameter(\"user-id\")\n\titemId := request.PathParameter(\"item-id\")\n\tif feedback, err := s.DataClient.GetUserItemFeedback(ctx, userId, itemId); err != nil {\n\t\tInternalServerError(response, err)\n\t} else {\n\t\tOk(response, feedback)\n\t}\n}\n\nfunc (s *RestServer) deleteUserItemFeedback(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\t// Parse parameters\n\tuserId := request.PathParameter(\"user-id\")\n\titemId := request.PathParameter(\"item-id\")\n\tif deleteCount, err := s.DataClient.DeleteUserItemFeedback(ctx, userId, itemId); err != nil {\n\t\tInternalServerError(response, err)\n\t} else {\n\t\tOk(response, Success{RowAffected: deleteCount})\n\t}\n}\n\nfunc (s *RestServer) getTypedUserItemFeedback(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\t// Parse parameters\n\tfeedbackType := request.PathParameter(\"feedback-type\")\n\tuserId := request.PathParameter(\"user-id\")\n\titemId := request.PathParameter(\"item-id\")\n\tif feedback, err := s.DataClient.GetUserItemFeedback(ctx, userId, itemId, feedbackType); err != nil {\n\t\tInternalServerError(response, err)\n\t} else if feedbackType == \"\" {\n\t\tText(response, \"{}\")\n\t} else {\n\t\tOk(response, feedback[0])\n\t}\n}\n\nfunc (s *RestServer) deleteTypedUserItemFeedback(request *restful.Request, response *restful.Response) {\n\tctx := context.Background()\n\tif request != nil && request.Request != nil {\n\t\tctx = request.Request.Context()\n\t}\n\t// Parse parameters\n\tfeedbackType := request.PathParameter(\"feedback-type\")\n\tuserId := request.PathParameter(\"user-id\")\n\titemId := request.PathParameter(\"item-id\")\n\tif deleteCount, err := s.DataClient.DeleteUserItemFeedback(ctx, userId, itemId, feedbackType); err != nil {\n\t\tInternalServerError(response, err)\n\t} else {\n\t\tOk(response, Success{deleteCount})\n\t}\n}\n\ntype HealthStatus struct {\n\tReady               bool\n\tDataStoreError      error\n\tCacheStoreError     error\n\tDataStoreConnected  bool\n\tCacheStoreConnected bool\n}\n\nfunc (s *RestServer) checkHealth() HealthStatus {\n\thealthStatus := HealthStatus{}\n\thealthStatus.DataStoreError = s.DataClient.Ping()\n\thealthStatus.CacheStoreError = s.CacheClient.Ping()\n\thealthStatus.DataStoreConnected = healthStatus.DataStoreError == nil\n\thealthStatus.CacheStoreConnected = healthStatus.CacheStoreError == nil\n\thealthStatus.Ready = healthStatus.DataStoreConnected && healthStatus.CacheStoreConnected\n\treturn healthStatus\n}\n\nfunc (s *RestServer) checkReady(_ *restful.Request, response *restful.Response) {\n\thealthStatus := s.checkHealth()\n\tif healthStatus.Ready {\n\t\tOk(response, healthStatus)\n\t} else {\n\t\terrReason, err := json.Marshal(healthStatus)\n\t\tif err != nil {\n\t\t\tError(response, http.StatusInternalServerError, err)\n\t\t} else {\n\t\t\tError(response, http.StatusServiceUnavailable, errors.New(string(errReason)))\n\t\t}\n\t}\n}\n\nfunc (s *RestServer) checkLive(_ *restful.Request, response *restful.Response) {\n\thealthStatus := s.checkHealth()\n\tOk(response, healthStatus)\n}\n\n// BadRequest returns a bad request error.\nfunc BadRequest(response *restful.Response, err error) {\n\tresponse.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\tlog.ResponseLogger(response).Error(\"bad request\", zap.Error(err))\n\tif err = response.WriteError(http.StatusBadRequest, err); err != nil {\n\t\tlog.ResponseLogger(response).Error(\"failed to write error\", zap.Error(err))\n\t}\n}\n\n// InternalServerError returns a internal server error.\nfunc InternalServerError(response *restful.Response, err error) {\n\tresponse.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\tlog.ResponseLogger(response).Error(\"internal server error\", zap.Error(err))\n\tif err = response.WriteError(http.StatusInternalServerError, err); err != nil {\n\t\tlog.ResponseLogger(response).Error(\"failed to write error\", zap.Error(err))\n\t}\n}\n\n// PageNotFound returns a not found error.\nfunc PageNotFound(response *restful.Response, err error) {\n\tresponse.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\tif err := response.WriteError(http.StatusNotFound, err); err != nil {\n\t\tlog.ResponseLogger(response).Error(\"failed to write error\", zap.Error(err))\n\t}\n}\n\n// Ok sends the content as JSON to the client.\nfunc Ok(response *restful.Response, content interface{}) {\n\tresponse.AddHeader(\"Access-Control-Allow-Origin\", \"*\")\n\tif err := response.WriteAsJson(content); err != nil {\n\t\tlog.ResponseLogger(response).Error(\"failed to write json\", zap.Error(err))\n\t}\n}\n\nfunc Error(response *restful.Response, httpStatus int, responseError error) {\n\tresponse.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\tif err := response.WriteError(httpStatus, responseError); err != nil {\n\t\tlog.ResponseLogger(response).Error(\"failed to write error\", zap.Error(err))\n\t}\n}\n\n// Text returns a plain text.\nfunc Text(response *restful.Response, content string) {\n\tresponse.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\tif _, err := response.Write([]byte(content)); err != nil {\n\t\tlog.ResponseLogger(response).Error(\"failed to write text\", zap.Error(err))\n\t}\n}\n\nfunc withWildCard(categories []string) []string {\n\tresult := make([]string, len(categories), len(categories)+1)\n\tcopy(result, categories)\n\tresult = append(result, \"\")\n\treturn result\n}\n\n// ReadCategories tries to read categories from the request. If the category is not found, it returns an empty string.\nfunc ReadCategories(request *restful.Request, defaultCategories []string) []string {\n\tif pathValue := request.PathParameter(\"category\"); pathValue != \"\" {\n\t\treturn []string{pathValue}\n\t} else if queryValues := request.QueryParameters(\"category\"); len(queryValues) > 0 {\n\t\treturn lo.Filter(queryValues, func(cat string, _ int) bool {\n\t\t\treturn len(cat) > 0\n\t\t})\n\t} else {\n\t\treturn defaultCategories\n\t}\n}\n"
  },
  {
    "path": "server/rest_test.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage server\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/emicklei/go-restful/v3\"\n\t\"github.com/gorse-io/gorse/common/expression\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/samber/lo/mutable\"\n\t\"github.com/steinfletcher/apitest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\nconst apiKey = \"test_api_key\"\n\ntype ServerTestSuite struct {\n\tsuite.Suite\n\tRestServer\n\thandler *restful.Container\n}\n\nfunc (suite *ServerTestSuite) SetupSuite() {\n\t// create mock redis server\n\tvar err error\n\t// open database\n\tsuite.Config = config.GetDefaultConfig()\n\tsuite.DataClient, err = data.Open(fmt.Sprintf(\"sqlite://%s/data.db\", suite.T().TempDir()), \"\")\n\tsuite.NoError(err)\n\tsuite.CacheClient, err = cache.Open(fmt.Sprintf(\"sqlite://%s/cache.db\", suite.T().TempDir()), \"\")\n\tsuite.NoError(err)\n\t// init database\n\terr = suite.DataClient.Init()\n\tsuite.NoError(err)\n\terr = suite.CacheClient.Init()\n\tsuite.NoError(err)\n\n\tsuite.WebService = new(restful.WebService)\n\tsuite.CreateWebService()\n\t// create handler\n\tsuite.handler = restful.NewContainer()\n\tsuite.handler.Add(suite.WebService)\n}\n\nfunc (suite *ServerTestSuite) TearDownSuite() {\n\terr := suite.DataClient.Close()\n\tsuite.NoError(err)\n\terr = suite.CacheClient.Close()\n\tsuite.NoError(err)\n}\n\nfunc (suite *ServerTestSuite) SetupTest() {\n\terr := suite.DataClient.Purge()\n\tsuite.NoError(err)\n\terr = suite.CacheClient.Purge()\n\tsuite.NoError(err)\n\t// configuration\n\tsuite.Config = config.GetDefaultConfig()\n\tsuite.Config.Server.APIKey = apiKey\n\tsuite.Config.Recommend.Collaborative.Type = \"mf\"\n\tsuite.Config.Recommend.Ranker.Type = \"fm\"\n\tsuite.Config.Recommend.Fallback.Recommenders = []string{\"latest\"}\n}\n\nfunc (suite *ServerTestSuite) marshal(v interface{}) string {\n\ts, err := json.Marshal(v)\n\tsuite.NoError(err)\n\treturn string(s)\n}\n\nfunc (suite *ServerTestSuite) TestUsers() {\n\tt := suite.T()\n\tusers := []data.User{\n\t\t{UserId: \"0\"},\n\t\t{UserId: \"1\"},\n\t\t{UserId: \"2\"},\n\t\t{UserId: \"3\"},\n\t\t{UserId: \"4\"},\n\t}\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/user\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(users[0]).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\":1}`).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/user/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(users[0])).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/users\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(users[1:]).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\":4}`).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/users\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"cursor\": \"\",\n\t\t\t\"n\":      \"100\",\n\t\t}).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(UserIterator{\n\t\t\tCursor: \"\",\n\t\t\tUsers:  users,\n\t\t})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tDelete(\"/api/user/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\": 1}`).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/user/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tStatus(http.StatusNotFound).\n\t\tEnd()\n\t// test modify\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPatch(\"/api/user/1\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(data.UserPatch{Labels: []string{\"a\", \"b\", \"c\"}, Comment: new(\"modified\")}).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\": 1}`).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/user/1\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(data.User{\n\t\t\tUserId:  \"1\",\n\t\t\tComment: \"modified\",\n\t\t\tLabels:  []string{\"a\", \"b\", \"c\"},\n\t\t})).\n\t\tEnd()\n\n\t// malicious labels\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/user\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(data.User{UserId: \"malicious\", Labels: []any{\"price\", 100}}).\n\t\tExpect(t).\n\t\tStatus(http.StatusBadRequest).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/users\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON([]data.User{{UserId: \"malicious\", Labels: []any{\"price\", 100}}}).\n\t\tExpect(t).\n\t\tStatus(http.StatusBadRequest).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPatch(\"/api/user/malicious\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(data.UserPatch{Labels: []any{\"price\", 100}}).\n\t\tExpect(t).\n\t\tStatus(http.StatusBadRequest).\n\t\tEnd()\n}\n\nfunc (suite *ServerTestSuite) TestItems() {\n\tt := suite.T()\n\t// Items\n\titems := []data.Item{\n\t\t{\n\t\t\tItemId:    \"0\",\n\t\t\tIsHidden:  true,\n\t\t\tTimestamp: time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC),\n\t\t\tLabels:    []string{\"a\"},\n\t\t\tComment:   \"comment_0\",\n\t\t},\n\t\t{\n\t\t\tItemId:     \"2\",\n\t\t\tCategories: []string{\"*\"},\n\t\t\tTimestamp:  time.Date(1997, 3, 15, 0, 0, 0, 0, time.UTC),\n\t\t\tLabels:     []string{\"a\"},\n\t\t\tComment:    \"comment_2\",\n\t\t},\n\t\t{\n\t\t\tItemId:    \"4\",\n\t\t\tIsHidden:  true,\n\t\t\tTimestamp: time.Date(1998, 3, 15, 0, 0, 0, 0, time.UTC),\n\t\t\tLabels:    []string{\"a\", \"b\"},\n\t\t\tComment:   \"comment_4\",\n\t\t},\n\t\t{\n\t\t\tItemId:     \"6\",\n\t\t\tCategories: []string{\"*\"},\n\t\t\tTimestamp:  time.Date(1999, 3, 15, 0, 0, 0, 0, time.UTC),\n\t\t\tLabels:     []string{\"b\"},\n\t\t\tComment:    \"comment_6\",\n\t\t},\n\t\t{\n\t\t\tItemId:    \"8\",\n\t\t\tIsHidden:  true,\n\t\t\tTimestamp: time.Date(2000, 3, 15, 0, 0, 0, 0, time.UTC),\n\t\t\tLabels:    []string{\"b\"},\n\t\t\tComment:   \"comment_8\",\n\t\t},\n\t}\n\t// insert items\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/item\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(items[0]).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\": 1}`).\n\t\tEnd()\n\t// batch insert items\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/items\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(items[1:]).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\": 4}`).\n\t\tEnd()\n\t// get items\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/items\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"cursor\": \"\",\n\t\t\t\"n\":      \"100\",\n\t\t}).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(ItemIterator{\n\t\t\tCursor: \"\",\n\t\t\tItems:  items,\n\t\t})).\n\t\tEnd()\n\t// get latest items\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/latest\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"3\",\n\t\t}).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]cache.Score{\n\t\t\t{Id: items[3].ItemId, Score: float64(items[3].Timestamp.Unix())},\n\t\t\t{Id: items[1].ItemId, Score: float64(items[1].Timestamp.Unix())},\n\t\t})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/latest/\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\":      \"3\",\n\t\t\t\"offset\": \"1\",\n\t\t}).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]cache.Score{\n\t\t\t{Id: items[1].ItemId, Score: float64(items[1].Timestamp.Unix())},\n\t\t})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/latest/*\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"3\",\n\t\t}).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]cache.Score{\n\t\t\t{Id: items[3].ItemId, Score: float64(items[3].Timestamp.Unix())},\n\t\t\t{Id: items[1].ItemId, Score: float64(items[1].Timestamp.Unix())},\n\t\t})).\n\t\tEnd()\n\terr := suite.DataClient.BatchInsertFeedback(suite.T().Context(), []data.Feedback{{\n\t\tFeedbackKey: data.FeedbackKey{FeedbackType: \"read\", UserId: \"0\", ItemId: \"6\"},\n\t\tTimestamp:   time.Now().Truncate(time.Hour),\n\t}}, true, true, true)\n\tsuite.NoError(err)\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/latest\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\":       \"3\",\n\t\t\t\"user-id\": \"0\",\n\t\t}).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]cache.Score{\n\t\t\t{Id: items[1].ItemId, Score: float64(items[1].Timestamp.Unix())},\n\t\t})).\n\t\tEnd()\n\n\t// delete item\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tDelete(\"/api/item/6\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\": 1}`).\n\t\tEnd()\n\t// get item\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/item/6\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tStatus(http.StatusNotFound).\n\t\tEnd()\n\t// get latest items\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/latest\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"3\",\n\t\t}).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]cache.Score{\n\t\t\t{Id: items[1].ItemId, Score: float64(items[1].Timestamp.Unix())},\n\t\t})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/latest/*\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"3\",\n\t\t}).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]cache.Score{\n\t\t\t{Id: items[1].ItemId, Score: float64(items[1].Timestamp.Unix())},\n\t\t})).\n\t\tEnd()\n\n\t// test modify\n\ttimestamp := time.Date(2010, 1, 1, 1, 1, 1, 0, time.UTC)\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPatch(\"/api/item/2\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(data.ItemPatch{\n\t\t\tIsHidden:   new(true),\n\t\t\tCategories: []string{\"-\"},\n\t\t\tLabels:     []string{\"a\", \"b\", \"c\"},\n\t\t\tComment:    new(\"modified\"),\n\t\t\tTimestamp:  &timestamp,\n\t\t}).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\": 1}`).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/item/2\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(data.Item{\n\t\t\tItemId:     \"2\",\n\t\t\tIsHidden:   true,\n\t\t\tCategories: []string{\"-\"},\n\t\t\tComment:    \"modified\",\n\t\t\tLabels:     []string{\"a\", \"b\", \"c\"},\n\t\t\tTimestamp:  timestamp,\n\t\t})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPatch(\"/api/item/2\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(data.ItemPatch{\n\t\t\tIsHidden: new(false),\n\t\t}).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\": 1}`).\n\t\tEnd()\n\t// get latest items\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/latest/-\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"1\",\n\t\t}).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]cache.Score{\n\t\t\t{Id: \"2\", Score: float64(timestamp.Unix())},\n\t\t})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/latest/*\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"3\",\n\t\t}).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]cache.Score{})).\n\t\tEnd()\n\n\t// insert category\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPut(\"/api/item/2/category/@\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(Success{RowAffected: 1})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/item/2\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(data.Item{\n\t\t\tItemId:     \"2\",\n\t\t\tIsHidden:   false,\n\t\t\tCategories: []string{\"-\", \"@\"},\n\t\t\tComment:    \"modified\",\n\t\t\tLabels:     []string{\"a\", \"b\", \"c\"},\n\t\t\tTimestamp:  timestamp,\n\t\t})).\n\t\tEnd()\n\t// get latest items\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/latest/@\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"1\",\n\t\t}).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]cache.Score{\n\t\t\t{Id: \"2\", Score: float64(timestamp.Unix())},\n\t\t})).\n\t\tEnd()\n\n\t// delete category\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tDelete(\"/api/item/2/category/@\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(Success{RowAffected: 1})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/item/2\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(data.Item{\n\t\t\tItemId:     \"2\",\n\t\t\tIsHidden:   false,\n\t\t\tCategories: []string{\"-\"},\n\t\t\tComment:    \"modified\",\n\t\t\tLabels:     []string{\"a\", \"b\", \"c\"},\n\t\t\tTimestamp:  timestamp,\n\t\t})).\n\t\tEnd()\n\t// get latest items\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/latest/@\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"1\",\n\t\t}).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]cache.Score{})).\n\t\tEnd()\n\n\t// insert items without timestamp\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/item\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(Item{ItemId: \"256\"}).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\": 1}`).\n\t\tEnd()\n\n\t// malicious labels\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/item\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(Item{ItemId: \"malicious\", Labels: []any{\"price\", 1}}).\n\t\tExpect(t).\n\t\tStatus(http.StatusBadRequest).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/items\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON([]Item{{ItemId: \"malicious\", Labels: []any{\"price\", 1}}}).\n\t\tExpect(t).\n\t\tStatus(http.StatusBadRequest).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPatch(\"/api/item/malicious\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(data.ItemPatch{Labels: []any{\"price\", 1}}).\n\t\tExpect(t).\n\t\tStatus(http.StatusBadRequest).\n\t\tEnd()\n}\n\nfunc (suite *ServerTestSuite) TestFeedback() {\n\tctx := suite.T().Context()\n\tt := suite.T()\n\t// Insert ret\n\tfeedback := []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"click\", UserId: \"0\", ItemId: \"0\"}, Value: 1.0},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"click\", UserId: \"1\", ItemId: \"2\"}, Value: 1.0},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"click\", UserId: \"2\", ItemId: \"4\"}, Value: 1.0},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"click\", UserId: \"3\", ItemId: \"6\"}, Value: 1.0},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"click\", UserId: \"4\", ItemId: \"8\"}, Value: 1.0},\n\t}\n\t//BatchInsertFeedback\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/feedback\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(feedback).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\": 5}`).\n\t\tEnd()\n\t//Get Feedback\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/feedback\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"cursor\": \"\",\n\t\t\t\"n\":      \"100\",\n\t\t}).\n\t\tExpect(t).\n\t\tBody(suite.marshal(FeedbackIterator{\n\t\t\tCursor:   \"\",\n\t\t\tFeedback: feedback,\n\t\t})).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\t// get feedback by user\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/user/1/feedback\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tBody(suite.marshal([]data.Feedback{feedback[1]})).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\t// get feedback by item\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/item/2/feedback\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tBody(suite.marshal([]data.Feedback{feedback[1]})).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\t//Get Items\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/items\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(ItemIterator{\n\t\t\tCursor: \"\",\n\t\t\tItems: []data.Item{\n\t\t\t\t{ItemId: \"0\"},\n\t\t\t\t{ItemId: \"2\"},\n\t\t\t\t{ItemId: \"4\"},\n\t\t\t\t{ItemId: \"6\"},\n\t\t\t\t{ItemId: \"8\"},\n\t\t\t},\n\t\t})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/users\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(UserIterator{\n\t\t\tCursor: \"\",\n\t\t\tUsers: []data.User{\n\t\t\t\t{UserId: \"0\"},\n\t\t\t\t{UserId: \"1\"},\n\t\t\t\t{UserId: \"2\"},\n\t\t\t\t{UserId: \"3\"},\n\t\t\t\t{UserId: \"4\"}},\n\t\t})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/user/2/feedback/click\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(`[{\"FeedbackType\":\"click\", \"UserId\": \"2\", \"ItemId\": \"4\", \"Timestamp\":\"0001-01-01T00:00:00Z\", \"Updated\":\"0001-01-01T00:00:00Z\", \"Comment\":\"\", \"Value\":1}]`).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/item/4/feedback/click\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(`[{\"FeedbackType\":\"click\", \"UserId\": \"2\", \"ItemId\": \"4\", \"Timestamp\":\"0001-01-01T00:00:00Z\", \"Updated\":\"0001-01-01T00:00:00Z\", \"Comment\":\"\", \"Value\":1}]`).\n\t\tEnd()\n\t// test overwrite\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPut(\"/api/feedback\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON([]data.Feedback{{\n\t\t\tFeedbackKey: data.FeedbackKey{FeedbackType: \"click\", UserId: \"0\", ItemId: \"0\"},\n\t\t\tComment:     \"override\",\n\t\t}}).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\": 1}`).\n\t\tEnd()\n\tret, err := suite.DataClient.GetUserFeedback(ctx, \"0\", suite.Config.Now(), expression.MustParseFeedbackTypeExpression(\"click\"))\n\tassert.NoError(t, err)\n\tassert.Equal(t, 1, len(ret))\n\tassert.Equal(t, \"override\", ret[0].Comment)\n\t// test not overwrite\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/feedback\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON([]data.Feedback{{\n\t\t\tFeedbackKey: data.FeedbackKey{FeedbackType: \"click\", UserId: \"0\", ItemId: \"0\"},\n\t\t\tComment:     \"not_override\",\n\t\t}}).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\": 1}`).\n\t\tEnd()\n\tret, err = suite.DataClient.GetUserFeedback(ctx, \"0\", suite.Config.Now(), expression.MustParseFeedbackTypeExpression(\"click\"))\n\tassert.NoError(t, err)\n\tassert.Equal(t, 1, len(ret))\n\tassert.Equal(t, \"not_override\", ret[0].Comment)\n\n\t// insert feedback without timestamp\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/feedback\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON([]Feedback{{FeedbackKey: data.FeedbackKey{UserId: \"100\", ItemId: \"100\", FeedbackType: \"Type\"}}}).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\": 1}`).\n\t\tEnd()\n}\n\nfunc (suite *ServerTestSuite) TestNonPersonalizedRecommend() {\n\tctx := suite.T().Context()\n\tsuite.Config.Recommend.ItemToItem = []config.ItemToItemConfig{{Name: \"default\"}}\n\ttype ListOperator struct {\n\t\tName       string\n\t\tCollection string\n\t\tSubset     string\n\t\tCategory   string\n\t\tURL        string\n\t}\n\toperators := []ListOperator{\n\t\t// TODO: Support hide users in the future.\n\t\t//{\"User Neighbors\", cache.Collection(cache.UserNeighbors, \"0\"), \"/api/user/0/neighbors\"},\n\t\t{\"Item Neighbors\", cache.ItemToItem, cache.Key(\"default\", \"0\"), \"\", \"/api/item/0/neighbors\"},\n\t\t{\"Item Neighbors in Category\", cache.ItemToItem, cache.Key(\"default\", \"0\"), \"0\", \"/api/item/0/neighbors/0\"},\n\t\t{\"NonPersonalized\", cache.NonPersonalized, \"trending\", \"\", \"/api/non-personalized/trending\"},\n\t\t{\"NonPersonalizedCategory\", cache.NonPersonalized, \"trending\", \"0\", \"/api/non-personalized/trending\"},\n\t\t{\"ItemToItem\", cache.ItemToItem, cache.Key(\"lookalike\", \"0\"), \"\", \"/api/item-to-item/lookalike/0\"},\n\t\t{\"ItemToItemCategory\", cache.ItemToItem, cache.Key(\"lookalike\", \"0\"), \"0\", \"/api/item-to-item/lookalike/0\"},\n\t\t{\"CollaborativeFiltering\", cache.Recommend, \"0\", \"\", \"/api/collaborative-filtering/0\"},\n\t\t{\"CollaborativeFilteringCategory\", cache.Recommend, \"0\", \"0\", \"/api/collaborative-filtering/0/0\"},\n\t}\n\tlastModified := time.Now()\n\n\tfor i, operator := range operators {\n\t\tsuite.T().Run(operator.Name, func(t *testing.T) {\n\t\t\t// insert documents\n\t\t\tdocuments := []cache.Score{\n\t\t\t\t{Id: strconv.Itoa(i) + \"0\", Score: 100, Categories: []string{operator.Category}},\n\t\t\t\t{Id: strconv.Itoa(i) + \"1\", Score: 99, Categories: []string{operator.Category}},\n\t\t\t\t{Id: strconv.Itoa(i) + \"2\", Score: 98, Categories: []string{operator.Category}},\n\t\t\t\t{Id: strconv.Itoa(i) + \"3\", Score: 97, Categories: []string{operator.Category}},\n\t\t\t\t{Id: strconv.Itoa(i) + \"4\", Score: 96, Categories: []string{operator.Category}},\n\t\t\t}\n\t\t\terr := suite.CacheClient.AddScores(ctx, operator.Collection, operator.Subset, documents)\n\t\t\tassert.NoError(t, err)\n\t\t\terr = suite.CacheClient.Set(ctx, cache.Time(cache.Key(operator.Collection+\"_update_time\", operator.Subset), lastModified))\n\t\t\tassert.NoError(t, err)\n\t\t\t// hidden item\n\t\t\tapitest.New().\n\t\t\t\tHandler(suite.handler).\n\t\t\t\tPatch(\"/api/item/\"+strconv.Itoa(i)+\"3\").\n\t\t\t\tHeader(\"X-API-Key\", apiKey).\n\t\t\t\tJSON(data.ItemPatch{IsHidden: new(true)}).\n\t\t\t\tExpect(t).\n\t\t\t\tStatus(http.StatusOK).\n\t\t\t\tEnd()\n\t\t\t// insert read feedback\n\t\t\terr = suite.DataClient.BatchInsertFeedback(ctx, []data.Feedback{{\n\t\t\t\tFeedbackKey: data.FeedbackKey{\n\t\t\t\t\tFeedbackType: \"read\",\n\t\t\t\t\tUserId:       \"0\",\n\t\t\t\t\tItemId:       strconv.Itoa(i) + \"1\",\n\t\t\t\t},\n\t\t\t\tTimestamp: time.Now().Add(-time.Hour),\n\t\t\t}}, true, true, true)\n\t\t\tassert.NoError(t, err)\n\n\t\t\tapitest.New().\n\t\t\t\tHandler(suite.handler).\n\t\t\t\tGet(operator.URL).\n\t\t\t\tQuery(\"category\", operator.Category).\n\t\t\t\tHeader(\"X-API-Key\", apiKey).\n\t\t\t\tExpect(t).\n\t\t\t\tStatus(http.StatusOK).\n\t\t\t\tHeaderPresent(\"Last-Modified\").\n\t\t\t\tBody(suite.marshal([]cache.Score{documents[0], documents[1], documents[2], documents[4]})).\n\t\t\t\tEnd()\n\t\t\tapitest.New().\n\t\t\t\tHandler(suite.handler).\n\t\t\t\tGet(operator.URL).\n\t\t\t\tQuery(\"category\", operator.Category).\n\t\t\t\tHeader(\"X-API-Key\", apiKey).\n\t\t\t\tQueryParams(map[string]string{\n\t\t\t\t\t\"offset\": \"0\",\n\t\t\t\t\t\"n\":      \"3\"}).\n\t\t\t\tExpect(t).\n\t\t\t\tStatus(http.StatusOK).\n\t\t\t\tHeaderPresent(\"Last-Modified\").\n\t\t\t\tBody(suite.marshal([]cache.Score{documents[0], documents[1], documents[2]})).\n\t\t\t\tEnd()\n\t\t\tapitest.New().\n\t\t\t\tHandler(suite.handler).\n\t\t\t\tGet(operator.URL).\n\t\t\t\tQuery(\"category\", operator.Category).\n\t\t\t\tHeader(\"X-API-Key\", apiKey).\n\t\t\t\tQueryParams(map[string]string{\n\t\t\t\t\t\"offset\": \"1\",\n\t\t\t\t\t\"n\":      \"3\"}).\n\t\t\t\tExpect(t).\n\t\t\t\tStatus(http.StatusOK).\n\t\t\t\tHeaderPresent(\"Last-Modified\").\n\t\t\t\tBody(suite.marshal([]cache.Score{documents[1], documents[2], documents[4]})).\n\t\t\t\tEnd()\n\t\t\tapitest.New().\n\t\t\t\tHandler(suite.handler).\n\t\t\t\tGet(operator.URL).\n\t\t\t\tQuery(\"category\", operator.Category).\n\t\t\t\tHeader(\"X-API-Key\", apiKey).\n\t\t\t\tQueryParams(map[string]string{\n\t\t\t\t\t\"offset\": \"0\",\n\t\t\t\t}).\n\t\t\t\tExpect(t).\n\t\t\t\tStatus(http.StatusOK).\n\t\t\t\tHeaderPresent(\"Last-Modified\").\n\t\t\t\tBody(suite.marshal([]cache.Score{documents[0], documents[1], documents[2], documents[4]})).\n\t\t\t\tEnd()\n\t\t\tapitest.New().\n\t\t\t\tHandler(suite.handler).\n\t\t\t\tGet(operator.URL).\n\t\t\t\tQuery(\"category\", operator.Category).\n\t\t\t\tHeader(\"X-API-Key\", apiKey).\n\t\t\t\tQueryParams(map[string]string{\n\t\t\t\t\t\"user-id\": \"0\",\n\t\t\t\t\t\"offset\":  \"0\",\n\t\t\t\t}).\n\t\t\t\tExpect(t).\n\t\t\t\tStatus(http.StatusOK).\n\t\t\t\tHeaderPresent(\"Last-Modified\").\n\t\t\t\tBody(suite.marshal([]cache.Score{documents[0], documents[2], documents[4]})).\n\t\t\t\tEnd()\n\t\t})\n\t}\n}\n\nfunc (suite *ServerTestSuite) TestUserToUser() {\n\tctx := suite.T().Context()\n\tsuite.Config.Recommend.UserToUser = []config.UserToUserConfig{{Name: \"default\"}}\n\terr := suite.CacheClient.AddScores(ctx, cache.UserToUser, cache.Key(\"default\", \"0\"), []cache.Score{\n\t\t{Id: \"1\", Score: 100},\n\t\t{Id: \"2\", Score: 99},\n\t\t{Id: \"3\", Score: 98},\n\t\t{Id: \"4\", Score: 97},\n\t\t{Id: \"5\", Score: 96},\n\t})\n\tsuite.NoError(err)\n\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/user-to-user/default/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]cache.Score{\n\t\t\t{Id: \"1\", Score: 100},\n\t\t\t{Id: \"2\", Score: 99},\n\t\t\t{Id: \"3\", Score: 98},\n\t\t\t{Id: \"4\", Score: 97},\n\t\t\t{Id: \"5\", Score: 96},\n\t\t})).\n\t\tEnd()\n\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/user/0/neighbors\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]cache.Score{\n\t\t\t{Id: \"1\", Score: 100},\n\t\t\t{Id: \"2\", Score: 99},\n\t\t\t{Id: \"3\", Score: 98},\n\t\t\t{Id: \"4\", Score: 97},\n\t\t\t{Id: \"5\", Score: 96},\n\t\t})).\n\t\tEnd()\n}\n\nfunc (suite *ServerTestSuite) TestDeleteFeedback() {\n\tt := suite.T()\n\t// Insert feedback\n\tfeedback := []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"type1\", UserId: \"2\", ItemId: \"3\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"type2\", UserId: \"2\", ItemId: \"3\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"type3\", UserId: \"2\", ItemId: \"3\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"type1\", UserId: \"1\", ItemId: \"6\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"type1\", UserId: \"4\", ItemId: \"8\"}},\n\t}\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/feedback\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(feedback).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\": 5}`).\n\t\tEnd()\n\t// Get Feedback\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/feedback/2/3\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tBody(suite.marshal([]data.Feedback{feedback[0], feedback[1], feedback[2]})).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\t// Get typed feedback\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/feedback/type2/2/3\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(feedback[1])).\n\t\tEnd()\n\t// delete feedback\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tDelete(\"/api/feedback/2/3\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tBody(`{\"RowAffected\": 3}`).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\t// delete typed feedback\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tDelete(\"/api/feedback/type1/4/8\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\": 1}`).\n\t\tEnd()\n}\n\nfunc (suite *ServerTestSuite) TestGetRecommends() {\n\tctx := suite.T().Context()\n\t// insert hidden items\n\terr := suite.CacheClient.AddScores(ctx, cache.Recommend, \"0\", []cache.Score{{Id: \"0\", Score: 100, Categories: []string{\"\"}}})\n\tsuite.NoError(err)\n\t// hide item\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPatch(\"/api/item/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(data.ItemPatch{IsHidden: new(true)}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\t// insert items\n\terr = suite.DataClient.BatchInsertItems(ctx, []data.Item{\n\t\t{ItemId: \"1\"},\n\t\t{ItemId: \"2\"},\n\t\t{ItemId: \"3\"},\n\t\t{ItemId: \"4\"},\n\t\t{ItemId: \"5\"},\n\t\t{ItemId: \"6\"},\n\t\t{ItemId: \"7\"},\n\t\t{ItemId: \"8\"},\n\t})\n\tsuite.NoError(err)\n\t// insert recommendation\n\terr = suite.CacheClient.AddScores(ctx, cache.Recommend, \"0\", []cache.Score{\n\t\t{Id: \"1\", Score: 99, Categories: []string{\"\"}},\n\t\t{Id: \"2\", Score: 98, Categories: []string{\"\"}},\n\t\t{Id: \"3\", Score: 97, Categories: []string{\"\"}},\n\t\t{Id: \"4\", Score: 96, Categories: []string{\"\"}},\n\t\t{Id: \"5\", Score: 95, Categories: []string{\"\"}},\n\t\t{Id: \"6\", Score: 94, Categories: []string{\"\"}},\n\t\t{Id: \"7\", Score: 93, Categories: []string{\"\"}},\n\t\t{Id: \"8\", Score: 92, Categories: []string{\"\"}},\n\t})\n\tsuite.NoError(err)\n\t// insert feedback\n\tfeedback := []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"2\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"4\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"1\"}, Timestamp: time.Now().Add(time.Hour)},\n\t}\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/feedback\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(feedback).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\": 3}`).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"3\",\n\t\t}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]string{\"1\", \"3\", \"5\"})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tHeader(\"X-API-Version\", \"2\").\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"3\",\n\t\t}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]cache.Score{{Id: \"1\", Score: 99}, {Id: \"3\", Score: 97}, {Id: \"5\", Score: 95}})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\":      \"3\",\n\t\t\t\"offset\": \"3\",\n\t\t}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]string{\"6\", \"7\", \"8\"})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\":      \"3\",\n\t\t\t\"offset\": \"10000\",\n\t\t}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]string{})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\":               \"3\",\n\t\t\t\"write-back-type\": \"read\",\n\t\t}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]string{\"1\", \"3\", \"5\"})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\":                \"3\",\n\t\t\t\"write-back-type\":  \"read\",\n\t\t\t\"write-back-delay\": \"10m\",\n\t\t}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]string{\"6\", \"7\", \"8\"})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"3\",\n\t\t}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]string{\"6\", \"7\", \"8\"})).\n\t\tEnd()\n}\n\nfunc (suite *ServerTestSuite) TestGetRecommendsMultiCategories() {\n\tctx := suite.T().Context()\n\t// insert recommendation\n\terr := suite.CacheClient.AddScores(ctx, cache.Recommend, \"0\", []cache.Score{\n\t\t{Id: \"1\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"2\", Score: 2, Categories: []string{\"\", \"2\"}},\n\t\t{Id: \"3\", Score: 3, Categories: []string{\"\", \"3\"}},\n\t\t{Id: \"4\", Score: 4, Categories: []string{\"\", \"2\"}},\n\t\t{Id: \"5\", Score: 5, Categories: []string{\"\", \"5\"}},\n\t\t{Id: \"6\", Score: 6, Categories: []string{\"\", \"2\", \"3\"}},\n\t\t{Id: \"7\", Score: 7, Categories: []string{\"\", \"7\"}},\n\t\t{Id: \"8\", Score: 8, Categories: []string{\"\", \"2\"}},\n\t\t{Id: \"9\", Score: 9, Categories: []string{\"\", \"3\"}},\n\t})\n\tsuite.NoError(err)\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryCollection(map[string][]string{\n\t\t\t\"n\":        {\"3\"},\n\t\t\t\"category\": {\"2\", \"3\"},\n\t\t}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]string{\"6\"})).\n\t\tEnd()\n}\n\nfunc (suite *ServerTestSuite) TestGetRecommendsReplacement() {\n\tctx := suite.T().Context()\n\tsuite.Config.Recommend.Replacement.EnableReplacement = true\n\t// insert recommendation\n\terr := suite.CacheClient.AddScores(ctx, cache.Recommend, \"0\", []cache.Score{\n\t\t{Id: \"0\", Score: 100, Categories: []string{\"\"}},\n\t\t{Id: \"1\", Score: 99, Categories: []string{\"\"}},\n\t\t{Id: \"2\", Score: 98, Categories: []string{\"\"}},\n\t\t{Id: \"3\", Score: 97, Categories: []string{\"\"}},\n\t\t{Id: \"4\", Score: 96, Categories: []string{\"\"}},\n\t\t{Id: \"5\", Score: 95, Categories: []string{\"\"}},\n\t\t{Id: \"6\", Score: 94, Categories: []string{\"\"}},\n\t\t{Id: \"7\", Score: 93, Categories: []string{\"\"}},\n\t\t{Id: \"8\", Score: 92, Categories: []string{\"\"}},\n\t})\n\tsuite.NoError(err)\n\t// hide item\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPatch(\"/api/item/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(data.ItemPatch{IsHidden: new(true)}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\t// insert feedback\n\tfeedback := []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"2\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"4\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"1\"}, Timestamp: time.Now().Add(time.Hour)},\n\t}\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/feedback\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(feedback).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\": 3}`).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"3\",\n\t\t}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]string{\"1\", \"2\", \"3\"})).\n\t\tEnd()\n}\n\nfunc (suite *ServerTestSuite) TestGetRecommendsFallbackItemToItem() {\n\tctx := suite.T().Context()\n\tsuite.Config.Recommend.ContextSize = 4\n\tsuite.Config.Recommend.DataSource.PositiveFeedbackTypes = []expression.FeedbackTypeExpression{\n\t\texpression.MustParseFeedbackTypeExpression(\"a\")}\n\tsuite.Config.Recommend.ItemToItem = []config.ItemToItemConfig{{Name: \"default\"}}\n\t// insert recommendation\n\terr := suite.CacheClient.AddScores(ctx, cache.Recommend, \"0\", []cache.Score{\n\t\t{Id: \"1\", Score: 99},\n\t\t{Id: \"2\", Score: 98},\n\t\t{Id: \"3\", Score: 97},\n\t\t{Id: \"4\", Score: 96}})\n\tsuite.NoError(err)\n\t// insert feedback\n\tfeedback := []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"1\"}, Timestamp: time.Date(2010, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"2\"}, Timestamp: time.Date(2009, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"3\"}, Timestamp: time.Date(2008, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"4\"}, Timestamp: time.Date(2007, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"5\"}, Timestamp: time.Date(2006, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t}\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/feedback\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(feedback).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\": 5}`).\n\t\tEnd()\n\n\t// insert similar items\n\terr = suite.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"1\"), []cache.Score{\n\t\t{Id: \"2\", Score: 100000, Categories: []string{\"\"}},\n\t\t{Id: \"9\", Score: 1, Categories: []string{\"\", \"*\"}},\n\t})\n\tsuite.NoError(err)\n\terr = suite.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"2\"), []cache.Score{\n\t\t{Id: \"3\", Score: 100000, Categories: []string{\"\", \"*\"}},\n\t\t{Id: \"8\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"9\", Score: 1, Categories: []string{\"\", \"*\"}},\n\t})\n\tsuite.NoError(err)\n\terr = suite.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"3\"), []cache.Score{\n\t\t{Id: \"4\", Score: 100000, Categories: []string{\"\"}},\n\t\t{Id: \"7\", Score: 1, Categories: []string{\"\", \"*\"}},\n\t\t{Id: \"8\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"9\", Score: 1, Categories: []string{\"\", \"*\"}},\n\t})\n\tsuite.NoError(err)\n\terr = suite.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"4\"), []cache.Score{\n\t\t{Id: \"1\", Score: 100000, Categories: []string{\"\", \"*\"}},\n\t\t{Id: \"6\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"7\", Score: 1, Categories: []string{\"\", \"*\"}},\n\t\t{Id: \"8\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"9\", Score: 1, Categories: []string{\"\", \"*\"}},\n\t})\n\tsuite.NoError(err)\n\terr = suite.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"5\"), []cache.Score{\n\t\t{Id: \"1\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"6\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"7\", Score: 100000, Categories: []string{\"\"}},\n\t\t{Id: \"8\", Score: 100, Categories: []string{\"\"}},\n\t\t{Id: \"9\", Score: 1, Categories: []string{\"\"}},\n\t})\n\tsuite.NoError(err)\n\n\t// test fallback\n\tsuite.Config.Recommend.Fallback.Recommenders = []string{\"item-to-item/default\"}\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"3\",\n\t\t}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]string{\"9\", \"8\", \"7\"})).\n\t\tEnd()\n\tsuite.Config.Recommend.Fallback.Recommenders = []string{\"item-to-item/default\"}\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/0/*\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"3\",\n\t\t}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]string{\"9\", \"7\"})).\n\t\tEnd()\n}\n\nfunc (suite *ServerTestSuite) TestGetRecommendsFallbackUserToUser() {\n\tctx := suite.T().Context()\n\tsuite.Config.Recommend.UserToUser = []config.UserToUserConfig{{Name: \"default\"}}\n\t// insert recommendation\n\terr := suite.CacheClient.AddScores(ctx, cache.Recommend, \"0\",\n\t\t[]cache.Score{{Id: \"1\", Score: 99}, {Id: \"2\", Score: 98}, {Id: \"3\", Score: 97}, {Id: \"4\", Score: 96}})\n\tsuite.NoError(err)\n\t// insert feedback\n\tfeedback := []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"1\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"2\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"3\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"4\"}},\n\t}\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/feedback\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(feedback).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\": 4}`).\n\t\tEnd()\n\t// insert similar users\n\terr = suite.CacheClient.AddScores(ctx, cache.UserToUser, cache.Key(\"default\", \"0\"), []cache.Score{\n\t\t{Id: \"1\", Score: 2, Categories: []string{\"\"}},\n\t\t{Id: \"2\", Score: 1.5, Categories: []string{\"\"}},\n\t\t{Id: \"3\", Score: 1, Categories: []string{\"\"}},\n\t})\n\tsuite.NoError(err)\n\terr = suite.DataClient.BatchInsertFeedback(ctx, []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"1\", ItemId: \"11\"}},\n\t}, true, true, true)\n\tsuite.NoError(err)\n\terr = suite.DataClient.BatchInsertFeedback(ctx, []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"2\", ItemId: \"12\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"2\", ItemId: \"48\"}},\n\t}, true, true, true)\n\tsuite.NoError(err)\n\terr = suite.DataClient.BatchInsertFeedback(ctx, []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"3\", ItemId: \"13\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"3\", ItemId: \"48\"}},\n\t}, true, true, true)\n\tsuite.NoError(err)\n\t// insert categorized items\n\terr = suite.DataClient.BatchInsertItems(ctx, []data.Item{\n\t\t{ItemId: \"12\", Categories: []string{\"*\"}},\n\t\t{ItemId: \"48\", Categories: []string{\"*\"}},\n\t})\n\tsuite.NoError(err)\n\t// test fallback\n\tsuite.Config.Recommend.Fallback.Recommenders = []string{\"user-to-user/default\"}\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"3\",\n\t\t}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]string{\"48\", \"11\", \"12\"})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/0/*\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"3\",\n\t\t}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]string{\"48\", \"12\"})).\n\t\tEnd()\n}\n\nfunc (suite *ServerTestSuite) TestRecommendFallbackLatest() {\n\tctx := suite.T().Context()\n\t// insert offline recommendation\n\terr := suite.CacheClient.AddScores(ctx, cache.Recommend, \"0\", []cache.Score{\n\t\t{Id: \"1\", Score: 99, Categories: []string{\"*\"}},\n\t\t{Id: \"2\", Score: 98, Categories: []string{\"*\"}},\n\t\t{Id: \"3\", Score: 97, Categories: []string{\"*\"}},\n\t\t{Id: \"4\", Score: 96, Categories: []string{\"*\"}}})\n\tsuite.NoError(err)\n\t// insert latest items\n\terr = suite.DataClient.BatchInsertItems(ctx, []data.Item{\n\t\t{ItemId: \"5\", Timestamp: time.Unix(95, 0)},\n\t\t{ItemId: \"6\", Timestamp: time.Unix(94, 0)},\n\t\t{ItemId: \"7\", Timestamp: time.Unix(93, 0)},\n\t\t{ItemId: \"8\", Timestamp: time.Unix(92, 0)},\n\t\t{ItemId: \"105\", Categories: []string{\"*\"}, Timestamp: time.Unix(85, 0)},\n\t\t{ItemId: \"106\", Categories: []string{\"*\"}, Timestamp: time.Unix(84, 0)},\n\t\t{ItemId: \"107\", Categories: []string{\"*\"}, Timestamp: time.Unix(83, 0)},\n\t\t{ItemId: \"108\", Categories: []string{\"*\"}, Timestamp: time.Unix(82, 0)},\n\t})\n\tsuite.NoError(err)\n\t// test latest fallback\n\tsuite.Config.Recommend.Fallback.Recommenders = []string{\"latest\"}\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"8\",\n\t\t}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]string{\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\"})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/0/*\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"8\",\n\t\t}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]string{\"1\", \"2\", \"3\", \"4\", \"105\", \"106\", \"107\", \"108\"})).\n\t\tEnd()\n}\n\nfunc (suite *ServerTestSuite) TestGetRecommendsFallbackCollaborativeFiltering() {\n\tctx := suite.T().Context()\n\t// insert offline recommendation\n\terr := suite.CacheClient.AddScores(ctx, cache.Recommend, \"0\", []cache.Score{\n\t\t{Id: \"1\", Score: 99, Categories: []string{\"*\"}},\n\t\t{Id: \"2\", Score: 98, Categories: []string{\"*\"}},\n\t\t{Id: \"3\", Score: 97, Categories: []string{\"*\"}},\n\t\t{Id: \"4\", Score: 96, Categories: []string{\"*\"}}})\n\tsuite.NoError(err)\n\t// insert collaborative filtering recommendation\n\terr = suite.CacheClient.AddScores(ctx, cache.CollaborativeFiltering, \"0\", []cache.Score{\n\t\t{Id: \"13\", Score: 79, Categories: []string{\"*\"}},\n\t\t{Id: \"14\", Score: 78, Categories: []string{\"*\"}},\n\t\t{Id: \"15\", Score: 77, Categories: []string{\"*\"}},\n\t\t{Id: \"16\", Score: 76, Categories: []string{\"*\"}}})\n\tsuite.NoError(err)\n\t// test collaborative filtering\n\tsuite.Config.Recommend.Fallback.Recommenders = []string{\"collaborative\"}\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"8\",\n\t\t}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]string{\"1\", \"2\", \"3\", \"4\", \"13\", \"14\", \"15\", \"16\"})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/0/*\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"8\",\n\t\t}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]string{\"1\", \"2\", \"3\", \"4\", \"13\", \"14\", \"15\", \"16\"})).\n\t\tEnd()\n}\n\nfunc (suite *ServerTestSuite) TestGetRecommendsFallbackNonPersonalized() {\n\tctx := suite.T().Context()\n\t// insert offline recommendation\n\terr := suite.CacheClient.AddScores(ctx, cache.Recommend, \"0\", []cache.Score{\n\t\t{Id: \"1\", Score: 99, Categories: []string{\"*\"}},\n\t\t{Id: \"2\", Score: 98, Categories: []string{\"*\"}},\n\t\t{Id: \"3\", Score: 97, Categories: []string{\"*\"}},\n\t\t{Id: \"4\", Score: 96, Categories: []string{\"*\"}}})\n\tsuite.NoError(err)\n\t// insert non-personalized recommendation\n\terr = suite.CacheClient.AddScores(ctx, cache.NonPersonalized, \"popular\", []cache.Score{\n\t\t{Id: \"5\", Score: 95, Categories: []string{\"\"}},\n\t\t{Id: \"6\", Score: 94, Categories: []string{\"\"}},\n\t\t{Id: \"7\", Score: 93, Categories: []string{\"\"}},\n\t\t{Id: \"8\", Score: 92, Categories: []string{\"\"}},\n\t\t{Id: \"105\", Score: 91, Categories: []string{\"*\"}},\n\t\t{Id: \"106\", Score: 90, Categories: []string{\"*\"}},\n\t\t{Id: \"107\", Score: 89, Categories: []string{\"*\"}},\n\t\t{Id: \"108\", Score: 88, Categories: []string{\"*\"}},\n\t})\n\tsuite.NoError(err)\n\t// test non-personalized fallback\n\tsuite.Config.Recommend.Fallback.Recommenders = []string{\"non-personalized/popular\"}\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"8\",\n\t\t}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]string{\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\"})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/0/*\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"8\",\n\t\t}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]string{\"1\", \"2\", \"3\", \"4\", \"105\", \"106\", \"107\", \"108\"})).\n\t\tEnd()\n}\n\nfunc (suite *ServerTestSuite) TestGetRecommendsLatest() {\n\tctx := suite.T().Context()\n\terr := suite.DataClient.BatchInsertItems(ctx, []data.Item{\n\t\t{ItemId: \"5\", Timestamp: time.Unix(95, 0)},\n\t\t{ItemId: \"6\", Timestamp: time.Unix(94, 0)},\n\t\t{ItemId: \"7\", Timestamp: time.Unix(93, 0)},\n\t\t{ItemId: \"8\", Timestamp: time.Unix(92, 0)},\n\t})\n\tsuite.NoError(err)\n\tsuite.Config.Recommend.Ranker.Type = \"none\"\n\tsuite.Config.Recommend.Ranker.Recommenders = []string{\"latest\"}\n\tsuite.Config.Recommend.Fallback.Recommenders = []string{}\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"4\",\n\t\t}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]string{\"5\", \"6\", \"7\", \"8\"})).\n\t\tEnd()\n}\n\nfunc (suite *ServerTestSuite) TestSessionRecommend() {\n\tctx := suite.T().Context()\n\tsuite.Config.Recommend.ContextSize = 4\n\tsuite.Config.Recommend.DataSource.PositiveFeedbackTypes = []expression.FeedbackTypeExpression{\n\t\texpression.MustParseFeedbackTypeExpression(\"a\")}\n\tsuite.Config.Recommend.ItemToItem = []config.ItemToItemConfig{{Name: \"default\"}}\n\n\t// insert similar items\n\terr := suite.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"1\"), []cache.Score{\n\t\t{Id: \"2\", Score: 100000, Categories: []string{\"\"}},\n\t\t{Id: \"9\", Score: 1, Categories: []string{\"\", \"*\"}},\n\t\t{Id: \"100\", Score: 100000, Categories: []string{\"\"}},\n\t})\n\tsuite.NoError(err)\n\terr = suite.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"2\"), []cache.Score{\n\t\t{Id: \"3\", Score: 100000, Categories: []string{\"\", \"*\"}},\n\t\t{Id: \"8\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"9\", Score: 1, Categories: []string{\"\", \"*\"}},\n\t})\n\tsuite.NoError(err)\n\terr = suite.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"3\"), []cache.Score{\n\t\t{Id: \"4\", Score: 100000, Categories: []string{\"\"}},\n\t\t{Id: \"7\", Score: 1, Categories: []string{\"\", \"*\"}},\n\t\t{Id: \"8\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"9\", Score: 1, Categories: []string{\"\", \"*\"}},\n\t})\n\tsuite.NoError(err)\n\terr = suite.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"4\"), []cache.Score{\n\t\t{Id: \"1\", Score: 100000, Categories: []string{\"\", \"*\"}},\n\t\t{Id: \"6\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"7\", Score: 1, Categories: []string{\"\", \"*\"}},\n\t\t{Id: \"8\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"9\", Score: 1, Categories: []string{\"\", \"*\"}},\n\t})\n\tsuite.NoError(err)\n\terr = suite.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"5\"), []cache.Score{\n\t\t{Id: \"1\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"6\", Score: 1, Categories: []string{\"\"}},\n\t\t{Id: \"7\", Score: 100000, Categories: []string{\"\"}},\n\t\t{Id: \"8\", Score: 100, Categories: []string{\"\"}},\n\t\t{Id: \"9\", Score: 1, Categories: []string{\"\"}},\n\t})\n\tsuite.NoError(err)\n\n\t// hide items\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/item\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(Item{ItemId: \"100\", IsHidden: true}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(`{\"RowAffected\": 1}`).\n\t\tEnd()\n\n\t// test fallback\n\tfeedback := []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"1\"}, Timestamp: time.Date(2010, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"2\"}, Timestamp: time.Date(2009, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"3\"}, Timestamp: time.Date(2008, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"4\"}, Timestamp: time.Date(2007, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"5\"}, Timestamp: time.Date(2006, 1, 1, 1, 1, 1, 1, time.UTC)},\n\t}\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/session/recommend\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"3\",\n\t\t}).\n\t\tJSON(feedback).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]cache.Score{{Id: \"9\", Score: 4}, {Id: \"8\", Score: 3}, {Id: \"7\", Score: 2}})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/session/recommend\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"offset\": \"100\",\n\t\t}).\n\t\tJSON(feedback).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]cache.Score(nil))).\n\t\tEnd()\n\tsuite.Config.Recommend.Fallback.Recommenders = []string{\"item_based\"}\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/session/recommend/*\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"3\",\n\t\t}).\n\t\tJSON(feedback).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal([]cache.Score{{Id: \"9\", Score: 4}, {Id: \"7\", Score: 2}})).\n\t\tEnd()\n}\n\nfunc (suite *ServerTestSuite) TestVisibility() {\n\tctx := suite.T().Context()\n\tsuite.Config.Recommend.ItemToItem = []config.ItemToItemConfig{{Name: \"default\"}}\n\t// insert items: 0, 1, 2, 3, 4\n\tvar items []Item\n\tfor i := 0; i < 5; i++ {\n\t\titems = append(items, Item{\n\t\t\tItemId:     strconv.Itoa(i),\n\t\t\tCategories: []string{\"a\"},\n\t\t\tTimestamp:  time.Date(1989, 6, i+1, 1, 1, 1, 1, time.UTC).String(),\n\t\t})\n\t}\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/items\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tQueryParams(map[string]string{\n\t\t\t\"n\": \"3\",\n\t\t}).\n\t\tJSON(items).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\n\t// insert cache\n\tvar documents []cache.Score\n\tfor i := range items {\n\t\tdocuments = append(documents, cache.Score{\n\t\t\tId:         strconv.Itoa(i),\n\t\t\tScore:      float64(time.Date(1989, 6, i+1, 1, 1, 1, 1, time.UTC).Unix()),\n\t\t\tCategories: []string{\"\", \"a\"},\n\t\t})\n\t}\n\tmutable.Reverse(documents)\n\terr := suite.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"100\"), documents)\n\tsuite.NoError(err)\n\terr = suite.CacheClient.AddScores(ctx, cache.Recommend, \"100\", documents)\n\tsuite.NoError(err)\n\n\t// delete item\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tDelete(\"/api/item/0\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(items).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\t// modify item\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPatch(\"/api/item/1\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(data.ItemPatch{IsHidden: new(true)}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\t// overwrite item\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/item\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(Item{ItemId: \"2\", IsHidden: true}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\n\t// recommend\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/latest\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(items).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(documents[:2])).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/item/100/neighbors/\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(items).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(documents[:2])).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/100/\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(items).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(cache.ConvertDocumentsToValues(documents[:2]))).\n\t\tEnd()\n\n\t// insert item\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/item\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(Item{ItemId: \"0\", Timestamp: time.Date(1989, 6, 1, 1, 1, 1, 1, time.UTC).String()}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\t// modify item\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPatch(\"/api/item/1\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(data.ItemPatch{IsHidden: new(false)}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\t// overwrite item\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/item\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(Item{ItemId: \"2\", IsHidden: false, Timestamp: time.Date(1989, 6, 3, 1, 1, 1, 1, time.UTC).String()}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\n\t// recommend\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/latest\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(items).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(documents)).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/item/100/neighbors/\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(items).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(documents[:len(documents)-1])).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/100/\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(items).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(cache.ConvertDocumentsToValues(documents))).\n\t\tEnd()\n\n\t// delete category\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tDelete(\"/api/item/0/category/a\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(items).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\t// modify category\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPatch(\"/api/item/1\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(data.ItemPatch{Categories: []string{}}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\t// overwrite category\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/item\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(Item{ItemId: \"2\", Categories: []string{}}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\n\t// recommend\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/latest/a\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(items).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(documents[:2])).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/item/100/neighbors/a\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(items).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(documents[:2])).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/100/a\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(items).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(cache.ConvertDocumentsToValues(documents[:2]))).\n\t\tEnd()\n\n\t// delete category\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPut(\"/api/item/0/category/a\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(items).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\t// modify category\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPatch(\"/api/item/1\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(data.ItemPatch{Categories: []string{\"a\"}}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\t// overwrite category\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tPost(\"/api/item\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(Item{ItemId: \"2\", Categories: []string{\"a\"}, Timestamp: time.Date(1989, 6, 3, 1, 1, 1, 1, time.UTC).String()}).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tEnd()\n\n\t// recommend\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/latest/a\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(items).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(documents)).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/item/100/neighbors/a\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(items).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(documents[:len(documents)-1])).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/recommend/100/a\").\n\t\tHeader(\"X-API-Key\", apiKey).\n\t\tJSON(items).\n\t\tExpect(suite.T()).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(cache.ConvertDocumentsToValues(documents))).\n\t\tEnd()\n}\n\nfunc (suite *ServerTestSuite) TestHealth() {\n\tt := suite.T()\n\t// ready\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/health/live\").\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(HealthStatus{\n\t\t\tReady:               true,\n\t\t\tDataStoreError:      nil,\n\t\t\tCacheStoreError:     nil,\n\t\t\tDataStoreConnected:  true,\n\t\t\tCacheStoreConnected: true,\n\t\t})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/health/ready\").\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(HealthStatus{\n\t\t\tReady:               true,\n\t\t\tDataStoreError:      nil,\n\t\t\tCacheStoreError:     nil,\n\t\t\tDataStoreConnected:  true,\n\t\t\tCacheStoreConnected: true,\n\t\t})).\n\t\tEnd()\n\n\t// not ready\n\tdataClient, cacheClient := suite.DataClient, suite.CacheClient\n\tsuite.DataClient, suite.CacheClient = data.NoDatabase{}, cache.NoDatabase{}\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/health/live\").\n\t\tExpect(t).\n\t\tStatus(http.StatusOK).\n\t\tBody(suite.marshal(HealthStatus{\n\t\t\tReady:               false,\n\t\t\tDataStoreError:      data.ErrNoDatabase,\n\t\t\tCacheStoreError:     cache.ErrNoDatabase,\n\t\t\tDataStoreConnected:  false,\n\t\t\tCacheStoreConnected: false,\n\t\t})).\n\t\tEnd()\n\tapitest.New().\n\t\tHandler(suite.handler).\n\t\tGet(\"/api/health/ready\").\n\t\tExpect(t).\n\t\tStatus(http.StatusServiceUnavailable).\n\t\tBody(suite.marshal(HealthStatus{\n\t\t\tReady:               false,\n\t\t\tDataStoreError:      data.ErrNoDatabase,\n\t\t\tCacheStoreError:     cache.ErrNoDatabase,\n\t\t\tDataStoreConnected:  false,\n\t\t\tCacheStoreConnected: false,\n\t\t})).\n\t\tEnd()\n\tsuite.DataClient, suite.CacheClient = dataClient, cacheClient\n}\n\nfunc TestServer(t *testing.T) {\n\tsuite.Run(t, new(ServerTestSuite))\n}\n"
  },
  {
    "path": "server/server.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage server\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"math/rand\"\n\t\"net\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/emicklei/go-restful/v3\"\n\t\"github.com/gorse-io/gorse/cmd/version\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/common/util\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/protocol\"\n\t\"github.com/gorse-io/gorse/storage\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/samber/lo\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/propagation\"\n\t\"go.uber.org/zap\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n)\n\n// Server manages states of a server node.\ntype Server struct {\n\tRestServer\n\ttraceConfig  config.TracingConfig\n\tcachePath    string\n\tcachePrefix  string\n\tdataPath     string\n\tdataPrefix   string\n\tconn         *grpc.ClientConn\n\tmasterClient protocol.MasterClient\n\tserverName   string\n\tmasterHost   string\n\tmasterPort   int\n\ttlsConfig    *util.TLSConfig\n\ttestMode     bool\n\tcacheFile    string\n}\n\n// NewServer creates a server node.\nfunc NewServer(\n\tmasterHost string,\n\tmasterPort int,\n\tserverHost string,\n\tserverPort int,\n\tcacheFile string,\n\ttlsConfig *util.TLSConfig,\n) *Server {\n\ts := &Server{\n\t\tmasterHost: masterHost,\n\t\tmasterPort: masterPort,\n\t\ttlsConfig:  tlsConfig,\n\t\tcacheFile:  cacheFile,\n\t\tRestServer: RestServer{\n\t\t\tConfig:      config.GetDefaultConfig(),\n\t\t\tCacheClient: new(cache.NoDatabase),\n\t\t\tDataClient:  new(data.NoDatabase),\n\t\t\tHttpHost:    serverHost,\n\t\t\tHttpPort:    serverPort,\n\t\t\tWebService:  new(restful.WebService),\n\t\t},\n\t}\n\treturn s\n}\n\n// Serve starts a server node.\nfunc (s *Server) Serve() {\n\trand.Seed(time.Now().UTC().UnixNano())\n\tvar err error\n\ts.serverName, err = s.ServerName()\n\tif err != nil {\n\t\tlog.Logger().Fatal(\"failed to get server name\", zap.Error(err))\n\t}\n\n\tlog.Logger().Info(\"start server\",\n\t\tzap.String(\"server_name\", s.serverName),\n\t\tzap.String(\"server_host\", s.HttpHost),\n\t\tzap.Int(\"server_port\", s.HttpPort),\n\t\tzap.String(\"master_host\", s.masterHost),\n\t\tzap.Int(\"master_port\", s.masterPort))\n\n\t// connect to master\n\tvar opts []grpc.DialOption\n\tif s.tlsConfig != nil {\n\t\tc, err := util.NewClientCreds(s.tlsConfig)\n\t\tif err != nil {\n\t\t\tlog.Logger().Fatal(\"failed to create credentials\", zap.Error(err))\n\t\t}\n\t\topts = append(opts, grpc.WithTransportCredentials(c))\n\t} else {\n\t\topts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))\n\t}\n\ts.conn, err = grpc.Dial(net.JoinHostPort(s.masterHost, strconv.Itoa(s.masterPort)), opts...)\n\tif err != nil {\n\t\tlog.Logger().Fatal(\"failed to connect master\", zap.Error(err))\n\t}\n\ts.masterClient = protocol.NewMasterClient(s.conn)\n\n\tgo s.Sync()\n\tcontainer := restful.NewContainer()\n\ts.StartHttpServer(container)\n}\n\nfunc (s *Server) ServerName() (string, error) {\n\thostname, err := os.Hostname()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\thash := md5.New()\n\thash.Write([]byte(hostname))\n\thash.Write([]byte(s.HttpHost))\n\thash.Write([]byte(strconv.Itoa(s.HttpPort)))\n\tb := hash.Sum(nil)\n\treturn hex.EncodeToString(b), nil\n}\n\nfunc (s *Server) Shutdown() {\n\terr := s.HttpServer.Shutdown(context.TODO())\n\tif err != nil {\n\t\tlog.Logger().Fatal(\"failed to shutdown http server\", zap.Error(err))\n\t}\n}\n\n// Sync this server to the master.\nfunc (s *Server) Sync() {\n\tdefer util.CheckPanic()\n\tlog.Logger().Info(\"start meta sync\", zap.Duration(\"meta_timeout\", s.Config.Master.MetaTimeout))\n\tfor {\n\t\tvar meta *protocol.Meta\n\t\tvar err error\n\t\tif meta, err = s.masterClient.GetMeta(context.Background(),\n\t\t\t&protocol.NodeInfo{\n\t\t\t\tNodeType:      protocol.NodeType_Server,\n\t\t\t\tUuid:          s.serverName,\n\t\t\t\tBinaryVersion: version.Version,\n\t\t\t\tHostname:      lo.Must(os.Hostname()),\n\t\t\t}); err != nil {\n\t\t\tlog.Logger().Error(\"failed to get meta\", zap.Error(err))\n\t\t\tgoto sleep\n\t\t}\n\n\t\t// load master config\n\t\terr = json.Unmarshal([]byte(meta.Config), &s.Config)\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to parse master config\", zap.Error(err))\n\t\t\tgoto sleep\n\t\t}\n\n\t\t// connect to data store\n\t\tif s.dataPath != s.Config.Database.DataStore || s.dataPrefix != s.Config.Database.DataTablePrefix {\n\t\t\tif strings.HasPrefix(s.Config.Database.DataStore, storage.SQLitePrefix) {\n\t\t\t\tlog.Logger().Info(\"connect cache store via master\")\n\t\t\t\ts.DataClient = data.NewProxyClient(s.conn)\n\t\t\t} else {\n\t\t\t\tlog.Logger().Info(\"connect data store\",\n\t\t\t\t\tzap.String(\"database\", log.RedactDBURL(s.Config.Database.DataStore)))\n\t\t\t\tdataOpts := s.Config.Database.StorageOptions(s.Config.Database.DataStore)\n\t\t\t\tif s.DataClient, err = data.Open(s.Config.Database.DataStore, s.Config.Database.DataTablePrefix, dataOpts...); err != nil {\n\t\t\t\t\tlog.Logger().Error(\"failed to connect data store\", zap.Error(err))\n\t\t\t\t\tgoto sleep\n\t\t\t\t}\n\t\t\t}\n\t\t\ts.dataPath = s.Config.Database.DataStore\n\t\t\ts.dataPrefix = s.Config.Database.DataTablePrefix\n\t\t}\n\n\t\t// connect to cache store\n\t\tif s.cachePath != s.Config.Database.CacheStore || s.cachePrefix != s.Config.Database.CacheTablePrefix {\n\t\t\tif strings.HasPrefix(s.Config.Database.CacheStore, storage.SQLitePrefix) {\n\t\t\t\tlog.Logger().Info(\"connect cache store via master\")\n\t\t\t\ts.CacheClient = cache.NewProxyClient(s.conn)\n\t\t\t} else {\n\t\t\t\tlog.Logger().Info(\"connect cache store\",\n\t\t\t\t\tzap.String(\"database\", log.RedactDBURL(s.Config.Database.CacheStore)))\n\t\t\t\tcacheOpts := s.Config.Database.StorageOptions(s.Config.Database.CacheStore)\n\t\t\t\tif s.CacheClient, err = cache.Open(s.Config.Database.CacheStore, s.Config.Database.CacheTablePrefix, cacheOpts...); err != nil {\n\t\t\t\t\tlog.Logger().Error(\"failed to connect cache store\", zap.Error(err))\n\t\t\t\t\tgoto sleep\n\t\t\t\t}\n\t\t\t}\n\t\t\ts.cachePath = s.Config.Database.CacheStore\n\t\t\ts.cachePrefix = s.Config.Database.CacheTablePrefix\n\t\t}\n\n\t\t// create trace provider\n\t\tif !s.traceConfig.Equal(s.Config.Tracing) {\n\t\t\tlog.Logger().Info(\"create trace provider\", zap.Any(\"tracing_config\", s.Config.Tracing))\n\t\t\ttp, err := s.Config.Tracing.NewTracerProvider()\n\t\t\tif err != nil {\n\t\t\t\tlog.Logger().Fatal(\"failed to create trace provider\", zap.Error(err))\n\t\t\t}\n\t\t\totel.SetTracerProvider(tp)\n\t\t\totel.SetErrorHandler(log.GetErrorHandler())\n\t\t\totel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))\n\t\t\ts.traceConfig = s.Config.Tracing\n\t\t}\n\n\tsleep:\n\t\tif s.testMode {\n\t\t\treturn\n\t\t}\n\t\ttime.Sleep(s.Config.Master.MetaTimeout)\n\t}\n}\n"
  },
  {
    "path": "server/server_test.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage server\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/protocol\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n)\n\ntype mockMaster struct {\n\tprotocol.UnimplementedMasterServer\n\taddr          chan string\n\tgrpcServer    *grpc.Server\n\tmeta          *protocol.Meta\n\tcacheTempFile string\n\tdataTempFile  string\n}\n\nfunc newMockMaster(t *testing.T) *mockMaster {\n\tcfg := config.GetDefaultConfig()\n\tcfg.Database.DataStore = fmt.Sprintf(\"sqlite://%s/data.db\", t.TempDir())\n\tcfg.Database.CacheStore = fmt.Sprintf(\"sqlite://%s/cache.db\", t.TempDir())\n\tbytes, err := json.Marshal(cfg)\n\tassert.NoError(t, err)\n\treturn &mockMaster{\n\t\taddr:          make(chan string),\n\t\tmeta:          &protocol.Meta{Config: string(bytes)},\n\t\tdataTempFile:  cfg.Database.DataStore,\n\t\tcacheTempFile: cfg.Database.CacheStore,\n\t}\n}\n\nfunc (m *mockMaster) GetMeta(_ context.Context, _ *protocol.NodeInfo) (*protocol.Meta, error) {\n\treturn m.meta, nil\n}\n\nfunc (m *mockMaster) Start(t *testing.T) {\n\tlisten, err := net.Listen(\"tcp\", \"localhost:0\")\n\tassert.NoError(t, err)\n\tm.addr <- listen.Addr().String()\n\tvar opts []grpc.ServerOption\n\tm.grpcServer = grpc.NewServer(opts...)\n\tprotocol.RegisterMasterServer(m.grpcServer, m)\n\terr = m.grpcServer.Serve(listen)\n\tassert.NoError(t, err)\n}\n\nfunc (m *mockMaster) Stop() {\n\tm.grpcServer.Stop()\n}\n\nfunc TestServer_Sync(t *testing.T) {\n\tmaster := newMockMaster(t)\n\tgo master.Start(t)\n\taddress := <-master.addr\n\tconn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials()))\n\tassert.NoError(t, err)\n\tserv := &Server{\n\t\ttestMode:     true,\n\t\tmasterClient: protocol.NewMasterClient(conn),\n\t\tRestServer: RestServer{\n\t\t\tConfig:      config.GetDefaultConfig(),\n\t\t\tCacheClient: new(cache.NoDatabase),\n\t\t\tDataClient:  new(data.NoDatabase),\n\t\t},\n\t}\n\tserv.Sync()\n\tassert.Equal(t, master.dataTempFile, serv.dataPath)\n\tassert.Equal(t, master.cacheTempFile, serv.cachePath)\n\tassert.NoError(t, serv.DataClient.Close())\n\tassert.NoError(t, serv.CacheClient.Close())\n\tmaster.Stop()\n}\n"
  },
  {
    "path": "storage/blob/azure.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage blob\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/juju/errors\"\n\t\"go.uber.org/zap\"\n)\n\ntype AzureBlob struct {\n\tclient    *azblob.Client\n\tcontainer string\n\tprefix    string\n}\n\nfunc NewAzureBlob(cfg config.AzureBlobConfig, container string, prefix string) (*AzureBlob, error) {\n\tvar (\n\t\tclient *azblob.Client\n\t\terr    error\n\t)\n\tif cfg.ConnectionString != \"\" {\n\t\tclient, err = azblob.NewClientFromConnectionString(cfg.ConnectionString, nil)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t} else {\n\t\tif cfg.AccountName == \"\" || cfg.AccountKey == \"\" {\n\t\t\treturn nil, errors.New(\"azure blob requires account_name and account_key or connection_string\")\n\t\t}\n\t\tendpoint := cfg.Endpoint\n\t\tif endpoint == \"\" {\n\t\t\tendpoint = fmt.Sprintf(\"https://%s.blob.core.windows.net/\", cfg.AccountName)\n\t\t}\n\t\tcred, err := azblob.NewSharedKeyCredential(cfg.AccountName, cfg.AccountKey)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tclient, err = azblob.NewClientWithSharedKeyCredential(endpoint, cred, nil)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t}\n\treturn &AzureBlob{\n\t\tclient:    client,\n\t\tcontainer: container,\n\t\tprefix:    strings.TrimPrefix(prefix, \"/\"),\n\t}, nil\n}\n\nfunc (a *AzureBlob) Open(name string) (io.ReadCloser, error) {\n\tfullPath := filepath.Join(a.prefix, name)\n\tresp, err := a.client.DownloadStream(context.Background(), a.container, fullPath, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Body, nil\n}\n\nfunc (a *AzureBlob) Create(name string) (io.WriteCloser, chan struct{}, error) {\n\tfullPath := filepath.Join(a.prefix, name)\n\tpr, pw := io.Pipe()\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tdefer close(done)\n\t\t_, err := a.client.UploadStream(context.Background(), a.container, fullPath, pr, nil)\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to upload file to Azure Blob\", zap.String(\"file\", fullPath), zap.Error(err))\n\t\t}\n\t}()\n\treturn pw, done, nil\n}\n\nfunc (a *AzureBlob) List() ([]string, error) {\n\tvar (\n\t\tprefix *string\n\t\tnames  []string\n\t)\n\tif a.prefix != \"\" {\n\t\tprefix = &a.prefix\n\t}\n\tpager := a.client.NewListBlobsFlatPager(a.container, &azblob.ListBlobsFlatOptions{Prefix: prefix})\n\tfor pager.More() {\n\t\tresp, err := pager.NextPage(context.Background())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, item := range resp.Segment.BlobItems {\n\t\t\tname := \"\"\n\t\t\tif item.Name != nil {\n\t\t\t\tname = *item.Name\n\t\t\t}\n\t\t\tif a.prefix != \"\" {\n\t\t\t\tname = strings.TrimPrefix(name, a.prefix)\n\t\t\t\tif len(name) > 0 && name[0] == '/' {\n\t\t\t\t\tname = name[1:]\n\t\t\t\t}\n\t\t\t}\n\t\t\tif name != \"\" {\n\t\t\t\tnames = append(names, name)\n\t\t\t}\n\t\t}\n\t}\n\treturn names, nil\n}\n\nfunc (a *AzureBlob) Remove(name string) error {\n\tfullPath := filepath.Join(a.prefix, name)\n\t_, err := a.client.DeleteBlob(context.Background(), a.container, fullPath, nil)\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to remove file from Azure Blob\", zap.String(\"file\", fullPath), zap.Error(err))\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "storage/blob/azure_test.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage blob\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestAzureBlobEmulator(t *testing.T) {\n\tconnectionString := os.Getenv(\"AZURE_STORAGE_CONNECTION_STRING\")\n\tif connectionString == \"\" {\n\t\tt.Skip(\"AZURE_STORAGE_CONNECTION_STRING is not set, skipping Azure Blob emulator test\")\n\t}\n\n\tclient, err := NewAzureBlob(config.AzureBlobConfig{ConnectionString: connectionString}, \"gorse-test\", \"blob\")\n\tassert.NoError(t, err)\n\n\tctx := t.Context()\n\t_, err = client.client.CreateContainer(ctx, client.container, nil)\n\tif err != nil {\n\t\tvar respErr *azcore.ResponseError\n\t\tif !errors.As(err, &respErr) || respErr.ErrorCode != string(bloberror.ContainerAlreadyExists) {\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t}\n\n\tw, done, err := client.Create(\"test.txt\")\n\tassert.NoError(t, err)\n\t_, err = w.Write([]byte(\"hello\"))\n\tassert.NoError(t, err)\n\tassert.NoError(t, w.Close())\n\t<-done\n\n\tr, err := client.Open(\"test.txt\")\n\tassert.NoError(t, err)\n\tdata, err := io.ReadAll(r)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"hello\", string(data))\n\tassert.NoError(t, r.Close())\n\n\tnames, err := client.List()\n\tassert.NoError(t, err)\n\tassert.Contains(t, names, \"test.txt\")\n\n\terr = client.Remove(\"test.txt\")\n\tassert.NoError(t, err)\n\tnames, err = client.List()\n\tassert.NoError(t, err)\n\tassert.NotContains(t, names, \"test.txt\")\n}\n"
  },
  {
    "path": "storage/blob/blob.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage blob\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/protocol\"\n\t\"github.com/juju/errors\"\n\t\"go.uber.org/zap\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n)\n\ntype Store interface {\n\tOpen(name string) (io.ReadCloser, error)\n\tCreate(name string) (io.WriteCloser, chan struct{}, error)\n\tList() ([]string, error)\n\tRemove(name string) error\n}\n\nfunc NewStore(cfg config.BlobConfig, masterConn *grpc.ClientConn) (Store, error) {\n\tif strings.Contains(cfg.URI, \"://\") {\n\t\tparsed, err := url.Parse(cfg.URI)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tswitch parsed.Scheme {\n\t\tcase \"s3\":\n\t\t\tif parsed.Host == \"\" {\n\t\t\t\treturn nil, errors.New(\"blob.uri must include bucket for s3://\")\n\t\t\t}\n\t\t\tstore, err := NewS3(cfg.S3, parsed.Host, strings.TrimPrefix(parsed.Path, \"/\"))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn store, nil\n\t\tcase \"gs\":\n\t\t\tif parsed.Host == \"\" {\n\t\t\t\treturn nil, errors.New(\"blob.uri must include bucket for gs://\")\n\t\t\t}\n\t\t\tstore, err := NewGCS(cfg.GCS, parsed.Host, strings.TrimPrefix(parsed.Path, \"/\"))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn store, nil\n\t\tcase \"az\":\n\t\t\tif parsed.Host == \"\" {\n\t\t\t\treturn nil, errors.New(\"blob.uri must include container for az://\")\n\t\t\t}\n\t\t\tstore, err := NewAzureBlob(cfg.Azure, parsed.Host, strings.TrimPrefix(parsed.Path, \"/\"))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn store, nil\n\t\tdefault:\n\t\t\treturn nil, errors.Errorf(\"unsupported blob.uri scheme: %s\", parsed.Scheme)\n\t\t}\n\t}\n\tif masterConn != nil {\n\t\treturn NewMasterStoreClient(masterConn), nil\n\t}\n\treturn NewPOSIX(cfg.URI), nil\n}\n\ntype MasterStoreServer struct {\n\tprotocol.UnimplementedBlobStoreServer\n\tdir string\n}\n\nfunc NewMasterStoreServer(dir string) *MasterStoreServer {\n\t// Create directory if not exists\n\terr := os.MkdirAll(dir, os.ModePerm)\n\tif err != nil {\n\t\tlog.Logger().Fatal(\"failed to create directory\", zap.Error(err))\n\t}\n\treturn &MasterStoreServer{dir: dir}\n}\n\nfunc (s *MasterStoreServer) UploadBlob(stream grpc.ClientStreamingServer[protocol.UploadBlobRequest, protocol.UploadBlobResponse]) error {\n\t// Create temp file\n\tfile, err := os.CreateTemp(s.dir, \"upload-*\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func(file *os.File) {\n\t\t_ = file.Close()\n\t}(file)\n\t// Write data\n\tvar (\n\t\tname      string\n\t\ttimestamp time.Time\n\t)\n\tfor {\n\t\treq, err := stream.Recv()\n\t\tif err != nil {\n\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\t// Assign name\n\t\tif name == \"\" {\n\t\t\tname = req.Name\n\t\t} else if name != req.Name {\n\t\t\treturn errors.New(\"inconsistent name\")\n\t\t}\n\t\t// Assign timestamp\n\t\tif timestamp.IsZero() {\n\t\t\ttimestamp = req.Timestamp.AsTime()\n\t\t} else if !timestamp.Equal(req.Timestamp.AsTime()) {\n\t\t\treturn errors.New(\"inconsistent timestamp\")\n\t\t}\n\t\t// Write data\n\t\t_, err = file.Write(req.Data)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t// Close file\n\terr = file.Close()\n\tif err != nil {\n\t\treturn err\n\t}\n\t// Rename file\n\terr = os.Rename(file.Name(), path.Join(s.dir, name))\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn stream.SendAndClose(&protocol.UploadBlobResponse{})\n}\n\nfunc (s *MasterStoreServer) DownloadBlob(request *protocol.DownloadBlobRequest, stream grpc.ServerStreamingServer[protocol.DownloadBlobResponse]) error {\n\t// Open file\n\tfile, err := os.Open(path.Join(s.dir, request.Name))\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func(file *os.File) {\n\t\terr = file.Close()\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to close file\", zap.Error(err))\n\t\t}\n\t}(file)\n\t// Send data\n\tfor {\n\t\tdata := make([]byte, 1024*1024*4)\n\t\tn, err := file.Read(data)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\terr = stream.Send(&protocol.DownloadBlobResponse{Data: data[:n]})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *MasterStoreServer) ListBlobs(ctx context.Context, request *protocol.ListBlobsRequest) (*protocol.ListBlobsResponse, error) {\n\tfiles, err := os.ReadDir(s.dir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar names []string\n\tfor _, file := range files {\n\t\tif !file.IsDir() {\n\t\t\tnames = append(names, file.Name())\n\t\t}\n\t}\n\treturn &protocol.ListBlobsResponse{Names: names}, nil\n}\n\nfunc (s *MasterStoreServer) RemoveBlob(ctx context.Context, request *protocol.RemoveBlobRequest) (*protocol.RemoveBlobResponse, error) {\n\terr := os.Remove(path.Join(s.dir, request.Name))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &protocol.RemoveBlobResponse{}, nil\n}\n\ntype MasterStoreClient struct {\n\tclient protocol.BlobStoreClient\n}\n\nfunc NewMasterStoreClient(clientConn *grpc.ClientConn) *MasterStoreClient {\n\treturn &MasterStoreClient{client: protocol.NewBlobStoreClient(clientConn)}\n}\n\nfunc (c *MasterStoreClient) Open(name string) (io.ReadCloser, error) {\n\tstream, err := c.client.DownloadBlob(context.Background(), &protocol.DownloadBlobRequest{Name: name})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpr, pw := io.Pipe()\n\tgo func() {\n\t\tfor {\n\t\t\tresp, err := stream.Recv()\n\t\t\tif err != nil {\n\t\t\t\t_ = pw.CloseWithError(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\t_, err = pw.Write(resp.Data)\n\t\t\tif err != nil {\n\t\t\t\t_ = pw.CloseWithError(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\treturn pr, nil\n}\n\nfunc (c *MasterStoreClient) Create(name string) (io.WriteCloser, chan struct{}, error) {\n\tstream, err := c.client.UploadBlob(context.Background())\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tdone := make(chan struct{})\n\tpr, pw := io.Pipe()\n\tgo func() {\n\t\tdefer close(done)\n\t\tfor {\n\t\t\tdata := make([]byte, 1024*1024*4)\n\t\t\tn, err := pr.Read(data)\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlog.Logger().Error(\"failed to read data\", zap.Error(err))\n\t\t\t\treturn\n\t\t\t}\n\t\t\terr = stream.Send(&protocol.UploadBlobRequest{\n\t\t\t\tName:      name,\n\t\t\t\tTimestamp: timestamppb.Now(),\n\t\t\t\tData:      data[:n],\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tlog.Logger().Error(\"failed to send data\", zap.Error(err))\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\t_, err = stream.CloseAndRecv()\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to close stream\", zap.Error(err))\n\t\t}\n\t}()\n\treturn pw, done, nil\n}\n\nfunc (c *MasterStoreClient) List() ([]string, error) {\n\tresp, err := c.client.ListBlobs(context.Background(), &protocol.ListBlobsRequest{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Names, nil\n}\n\nfunc (c *MasterStoreClient) Remove(name string) error {\n\t_, err := c.client.RemoveBlob(context.Background(), &protocol.RemoveBlobRequest{Name: name})\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "storage/blob/blob_test.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage blob\n\nimport (\n\t\"io\"\n\t\"net\"\n\t\"path\"\n\t\"testing\"\n\n\t\"github.com/gorse-io/gorse/protocol\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"google.golang.org/grpc\"\n)\n\nfunc TestBlob(t *testing.T) {\n\t// start server\n\tlis, err := net.Listen(\"tcp\", \"localhost:0\")\n\tassert.NoError(t, err)\n\tgrpcServer := grpc.NewServer()\n\tprotocol.RegisterBlobStoreServer(grpcServer, NewMasterStoreServer(path.Join(t.TempDir(), \"blob\")))\n\tgo func() {\n\t\terr = grpcServer.Serve(lis)\n\t\tassert.NoError(t, err)\n\t}()\n\tdefer grpcServer.Stop()\n\n\t// create client\n\tclientConn, err := grpc.Dial(lis.Addr().String(), grpc.WithInsecure())\n\tassert.NoError(t, err)\n\tclient := NewMasterStoreClient(clientConn)\n\n\t// write a temp file\n\tw, done, err := client.Create(\"test\")\n\tassert.NoError(t, err)\n\t_, err = w.Write([]byte(\"hello world\"))\n\tassert.NoError(t, err)\n\tassert.NoError(t, w.Close())\n\t<-done\n\n\t// read the file\n\tr, err := client.Open(\"test\")\n\tassert.NoError(t, err)\n\tcontent := make([]byte, 11)\n\t_, err = r.Read(content)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"hello world\", string(content))\n\t_, err = r.Read(content)\n\tassert.ErrorIs(t, err, io.EOF)\n\n\t// list files\n\tfiles, err := client.List()\n\tassert.NoError(t, err)\n\tassert.Contains(t, files, \"test\")\n\n\t// remove the file\n\terr = client.Remove(\"test\")\n\tassert.NoError(t, err)\n\tfiles, err = client.List()\n\tassert.NoError(t, err)\n\tassert.NotContains(t, files, \"test\")\n}\n"
  },
  {
    "path": "storage/blob/gcs.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage blob\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"cloud.google.com/go/storage\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"google.golang.org/api/iterator\"\n\t\"google.golang.org/api/option\"\n)\n\ntype GCS struct {\n\tclient *storage.Client\n\tbucket string\n\tprefix string\n}\n\nfunc NewGCS(cfg config.GCSConfig, bucket string, prefix string) (*GCS, error) {\n\tvar opts []option.ClientOption\n\tif os.Getenv(\"GCS_EMULATOR_ENDPOINT\") != \"\" {\n\t\topts = append(opts, option.WithEndpoint(os.Getenv(\"GCS_EMULATOR_ENDPOINT\")))\n\t\topts = append(opts, option.WithoutAuthentication())\n\t}\n\tif cfg.CredentialsFile != \"\" {\n\t\topts = append(opts, option.WithCredentialsFile(cfg.CredentialsFile))\n\t}\n\tclient, err := storage.NewClient(context.Background(), opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &GCS{\n\t\tclient: client,\n\t\tbucket: bucket,\n\t\tprefix: prefix,\n\t}, nil\n}\n\nfunc (g *GCS) Open(name string) (io.ReadCloser, error) {\n\tpath := filepath.Join(g.prefix, name)\n\treturn g.client.Bucket(g.bucket).Object(path).NewReader(context.Background())\n}\n\nfunc (g *GCS) Create(name string) (io.WriteCloser, chan struct{}, error) {\n\tpath := filepath.Join(g.prefix, name)\n\twc := g.client.Bucket(g.bucket).Object(path).NewWriter(context.Background())\n\tdone := make(chan struct{})\n\treturn &gcsWriter{wc, done}, done, nil\n}\n\ntype gcsWriter struct {\n\t*storage.Writer\n\tdone chan struct{}\n}\n\nfunc (w *gcsWriter) Close() error {\n\terr := w.Writer.Close()\n\tclose(w.done)\n\treturn err\n}\n\nfunc (g *GCS) List() ([]string, error) {\n\tvar names []string\n\tit := g.client.Bucket(g.bucket).Objects(context.Background(), &storage.Query{\n\t\tPrefix: g.prefix,\n\t})\n\tfor {\n\t\tattrs, err := it.Next()\n\t\tif err == iterator.Done {\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tname := attrs.Name[len(g.prefix):]\n\t\tif len(name) > 0 && name[0] == os.PathSeparator {\n\t\t\tname = name[1:]\n\t\t}\n\t\tnames = append(names, name)\n\t}\n\treturn names, nil\n}\n\nfunc (g *GCS) Remove(name string) error {\n\tpath := filepath.Join(g.prefix, name)\n\treturn g.client.Bucket(g.bucket).Object(path).Delete(context.Background())\n}\n"
  },
  {
    "path": "storage/blob/gcs_test.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage blob\n\nimport (\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/fsouza/fake-gcs-server/fakestorage\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGCS(t *testing.T) {\n\tserver, err := fakestorage.NewServerWithOptions(fakestorage.Options{\n\t\tScheme:     \"http\",\n\t\tPort:       5050,\n\t\tPublicHost: \"localhost:5050\",\n\t})\n\tassert.NoError(t, err)\n\tdefer server.Stop()\n\tt.Setenv(\"GCS_EMULATOR_ENDPOINT\", \"http://localhost:5050/storage/v1/\")\n\n\t// create client\n\tclient, err := NewGCS(config.GCSConfig{}, \"gorse-test\", \"blob\")\n\tassert.NoError(t, err)\n\n\t// create bucket if not exists\n\terr = client.client.Bucket(\"gorse-test\").Create(t.Context(), \"test-project\", nil)\n\tif err != nil {\n\t\tassert.ErrorContains(t, err, \"A Cloud Storage bucket named 'gorse-test' already exists.\")\n\t}\n\n\t// create file\n\tw, done, err := client.Create(\"test.txt\")\n\tassert.NoError(t, err)\n\t_, err = w.Write([]byte(\"hello\"))\n\tassert.NoError(t, err)\n\terr = w.Close()\n\tassert.NoError(t, err)\n\t<-done\n\n\t// list files\n\tnames, err := client.List()\n\tassert.NoError(t, err)\n\tassert.Equal(t, []string{\"test.txt\"}, names)\n\n\t// read file\n\tr, err := client.Open(\"test.txt\")\n\tassert.NoError(t, err)\n\tdata, err := io.ReadAll(r)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"hello\", string(data))\n\terr = r.Close()\n\tassert.NoError(t, err)\n\n\t// remove file\n\terr = client.Remove(\"test.txt\")\n\tassert.NoError(t, err)\n\n\t// list files again\n\tnames, err = client.List()\n\tassert.NoError(t, err)\n\tassert.Empty(t, names)\n}\n"
  },
  {
    "path": "storage/blob/posix.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage blob\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"path\"\n\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"go.uber.org/zap\"\n)\n\ntype POSIX struct {\n\tdir string\n}\n\nfunc NewPOSIX(dir string) *POSIX {\n\treturn &POSIX{dir: dir}\n}\n\n// Open a file for reading. It returns an io.Reader that can be used to read the file's content.\nfunc (p *POSIX) Open(name string) (io.ReadCloser, error) {\n\tfullPath := path.Join(p.dir, name)\n\treturn os.Open(fullPath)\n}\n\n// Create a new file for writing. It returns an io.WriteCloser that can be used to write data to the file. It also\n// returns a done channel that is closed when the writing is complete.\nfunc (p *POSIX) Create(name string) (io.WriteCloser, chan struct{}, error) {\n\tfullPath := path.Join(p.dir, name)\n\tif err := os.MkdirAll(path.Dir(fullPath), os.ModePerm); err != nil {\n\t\treturn nil, nil, err\n\t}\n\tfile, err := os.Create(fullPath)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tdone := make(chan struct{})\n\tpr, pw := io.Pipe()\n\tgo func() {\n\t\tdefer func() {\n\t\t\t_ = file.Close()\n\t\t\tclose(done)\n\t\t}()\n\t\t_, err := io.Copy(file, pr)\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to write to file\", zap.String(\"file\", fullPath), zap.Error(err))\n\t\t}\n\t}()\n\treturn pw, done, err\n}\n\n// List files in the directory. It returns a slice of file names.\nfunc (p *POSIX) List() ([]string, error) {\n\tfiles, err := os.ReadDir(p.dir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar names []string\n\tfor _, file := range files {\n\t\tif !file.IsDir() {\n\t\t\tnames = append(names, file.Name())\n\t\t}\n\t}\n\treturn names, nil\n}\n\n// Remove a file by its name. It deletes the file from the filesystem.\nfunc (p *POSIX) Remove(name string) error {\n\tfullPath := path.Join(p.dir, name)\n\tif err := os.Remove(fullPath); err != nil {\n\t\tlog.Logger().Error(\"failed to remove file\", zap.String(\"file\", fullPath), zap.Error(err))\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "storage/blob/posix_test.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage blob\n\nimport (\n\t\"github.com/stretchr/testify/assert\"\n\t\"path\"\n\t\"testing\"\n)\n\nfunc TestPOSIX(t *testing.T) {\n\t// create client\n\tclient := NewPOSIX(path.Join(t.TempDir(), \"blob\"))\n\n\t// write a temp file\n\tw, done, err := client.Create(\"test\")\n\tassert.NoError(t, err)\n\t_, err = w.Write([]byte(\"hello world\"))\n\tassert.NoError(t, err)\n\tassert.NoError(t, w.Close())\n\t<-done\n\n\t// read the file\n\tr, err := client.Open(\"test\")\n\tassert.NoError(t, err)\n\tcontent := make([]byte, 11)\n\t_, err = r.Read(content)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"hello world\", string(content))\n\tassert.NoError(t, r.Close())\n\n\t// list files\n\tfiles, err := client.List()\n\tassert.NoError(t, err)\n\tassert.Contains(t, files, \"test\")\n\n\t// remove the file\n\terr = client.Remove(\"test\")\n\tassert.NoError(t, err)\n\tfiles, err = client.List()\n\tassert.NoError(t, err)\n\tassert.NotContains(t, files, \"test\")\n}\n"
  },
  {
    "path": "storage/blob/s3.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage blob\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"path/filepath\"\n\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/minio/minio-go/v7\"\n\t\"github.com/minio/minio-go/v7/pkg/credentials\"\n\t\"go.uber.org/zap\"\n)\n\ntype S3 struct {\n\t*minio.Client\n\tbucket string\n\tprefix string\n}\n\nfunc NewS3(cfg config.S3Config, bucket string, prefix string) (*S3, error) {\n\tminioClient, err := minio.New(cfg.Endpoint, &minio.Options{\n\t\tCreds: credentials.NewStaticV4(cfg.AccessKeyID, cfg.SecretAccessKey, \"\"),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &S3{\n\t\tClient: minioClient,\n\t\tbucket: bucket,\n\t\tprefix: prefix,\n\t}, nil\n}\n\n// Open a file in S3 for reading. This function returns an io.Reader that can be used to read the file's content.\nfunc (s *S3) Open(name string) (io.ReadCloser, error) {\n\tobject, err := s.Client.GetObject(context.Background(), s.bucket, filepath.Join(s.prefix, name), minio.GetObjectOptions{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn object, nil\n}\n\n// Create a new file in S3 for writing. This function returns an io.WriteCloser that can be used to write data to the\n// file. It also returns a done channel that is closed when the writing is complete.\nfunc (s *S3) Create(name string) (io.WriteCloser, chan struct{}, error) {\n\tfullPath := filepath.Join(s.prefix, name)\n\tpr, pw := io.Pipe()\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tdefer close(done)\n\t\t_, err := s.Client.PutObject(context.Background(), s.bucket, fullPath, pr, -1, minio.PutObjectOptions{})\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to upload file to S3\", zap.String(\"file\", fullPath), zap.Error(err))\n\t\t}\n\t}()\n\treturn pw, done, nil\n}\n\n// List files in the S3 bucket with the specified prefix. This function returns a slice of file names.\nfunc (s *S3) List() ([]string, error) {\n\tvar names []string\n\tfor object := range s.Client.ListObjects(context.Background(), s.bucket, minio.ListObjectsOptions{\n\t\tPrefix:    s.prefix,\n\t\tRecursive: true,\n\t}) {\n\t\tif object.Err != nil {\n\t\t\treturn nil, object.Err\n\t\t}\n\t\tname := object.Key[len(s.prefix):]\n\t\tif len(name) > 0 && name[0] == '/' {\n\t\t\tname = name[1:]\n\t\t}\n\t\tnames = append(names, name)\n\t}\n\treturn names, nil\n}\n\n// Remove a file from the S3 bucket by its name. This function deletes the file from S3.\nfunc (s *S3) Remove(name string) error {\n\tfullPath := filepath.Join(s.prefix, name)\n\terr := s.Client.RemoveObject(context.Background(), s.bucket, fullPath, minio.RemoveObjectOptions{})\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to remove file from S3\", zap.String(\"file\", fullPath), zap.Error(err))\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "storage/blob/s3_test.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage blob\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/minio/minio-go/v7\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar (\n\tendpoint        = os.Getenv(\"S3_ENDPOINT\")\n\taccessKeyID     = os.Getenv(\"S3_ACCESS_KEY_ID\")\n\tsecretAccessKey = os.Getenv(\"S3_SECRET_ACCESS_KEY\")\n)\n\nfunc TestS3(t *testing.T) {\n\tif endpoint == \"\" || accessKeyID == \"\" || secretAccessKey == \"\" {\n\t\tt.Skip(\"S3 environment variables are not set, skipping S3 tests\")\n\t}\n\n\t// create client\n\tclient, err := NewS3(config.S3Config{\n\t\tEndpoint:        endpoint,\n\t\tAccessKeyID:     accessKeyID,\n\t\tSecretAccessKey: secretAccessKey,\n\t}, \"gorse-test\", \"blob\")\n\tassert.NoError(t, err)\n\n\t// create bucket if not exists\n\terr = client.Client.MakeBucket(t.Context(), client.bucket, minio.MakeBucketOptions{})\n\tassert.NoError(t, err)\n\n\t// write a temp file\n\tw, done, err := client.Create(\"test\")\n\tassert.NoError(t, err)\n\t_, err = w.Write([]byte(\"hello world\"))\n\tassert.NoError(t, err)\n\tassert.NoError(t, w.Close())\n\t<-done\n\n\t// read the file\n\tr, err := client.Open(\"test\")\n\tassert.NoError(t, err)\n\tcontent := make([]byte, 11)\n\t_, err = r.Read(content)\n\tassert.ErrorIs(t, err, io.EOF)\n\tassert.Equal(t, \"hello world\", string(content))\n\n\t// list files\n\tfiles, err := client.List()\n\tassert.NoError(t, err)\n\tassert.Contains(t, files, \"test\")\n\n\t// remove the file\n\terr = client.Remove(\"test\")\n\tassert.NoError(t, err)\n\tfiles, err = client.List()\n\tassert.NoError(t, err)\n\tassert.NotContains(t, files, \"test\")\n}\n"
  },
  {
    "path": "storage/cache/database.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cache\n\nimport (\n\t\"context\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/araddon/dateparse\"\n\t\"github.com/gorse-io/gorse/storage\"\n\t\"github.com/juju/errors\"\n)\n\nconst (\n\tNonPersonalized                  = \"non-personalized\"\n\tNonPersonalizedDigest            = \"non-personalized_digest\"\n\tNonPersonalizedUpdateTime        = \"non-personalized_update_time\"\n\tItemToItem                       = \"item-to-item\"\n\tItemToItemDigest                 = \"item-to-item_digest\"\n\tItemToItemUpdateTime             = \"item-to-item_update_time\"\n\tUserToUser                       = \"user-to-user\"\n\tUserToUserDigest                 = \"user-to-user_digest\"\n\tUserToUserUpdateTime             = \"user-to-user_update_time\"\n\tCollaborativeFiltering           = \"collaborative-filtering\"\n\tCollaborativeFilteringDigest     = \"collaborative-filtering_digest\"\n\tCollaborativeFilteringUpdateTime = \"collaborative-filtering_update_time\"\n\tRecommend                        = \"recommend\"\n\tRecommendDigest                  = \"recommend_digest\"\n\tRecommendUpdateTime              = \"recommend_update_time\"\n\n\t// ItemCategories is the set of item categories. The format of key:\n\t//\tGlobal item categories - item_categories\n\tItemCategories = \"item_categories\"\n\n\tLastModifyItemTime = \"last_modify_item_time\" // the latest timestamp that a user related data was modified\n\tLastModifyUserTime = \"last_modify_user_time\" // the latest timestamp that an item related data was modified\n\n\t// GlobalMeta is global meta information\n\tGlobalMeta                 = \"global_meta\"\n\tNumUsers                   = \"num_users\"\n\tNumItems                   = \"num_items\"\n\tNumFeedback                = \"num_feedback\"\n\tNumPosFeedbacks            = \"num_pos_feedbacks\"\n\tNumNegFeedbacks            = \"num_neg_feedbacks\"\n\tNumUserLabels              = \"num_user_labels\"\n\tNumItemLabels              = \"num_item_labels\"\n\tNumTotalPosFeedbacks       = \"num_total_pos_feedbacks\"\n\tNumValidPosFeedbacks       = \"num_valid_pos_feedbacks\"\n\tNumValidNegFeedbacks       = \"num_valid_neg_feedbacks\"\n\tLastFitMatchingModelTime   = \"last_fit_matching_model_time\"\n\tLastFitRankingModelTime    = \"last_fit_ranking_model_time\"\n\tLastUpdateLatestItemsTime  = \"last_update_latest_items_time\"  // the latest timestamp that latest items were updated\n\tLastUpdatePopularItemsTime = \"last_update_popular_items_time\" // the latest timestamp that popular items were updated\n\tCFNDCG                     = \"cf_ndcg\"\n\tCFPrecision                = \"cf_precision\"\n\tCFRecall                   = \"cf_recall\"\n\tCTRPrecision               = \"ctr_precision\"\n\tCTRRecall                  = \"ctr_recall\"\n\tCTRAUC                     = \"ctr_auc\"\n\tPositiveFeedbackRatio      = \"positive_feedback_ratio\"\n)\n\nvar ItemCache = []string{\n\tNonPersonalized,\n\tItemToItem,\n\tRecommend,\n}\n\nvar (\n\tErrObjectNotExist = errors.NotFoundf(\"object\")\n\tErrNoDatabase     = errors.NotAssignedf(\"database\")\n)\n\n// Key creates key for cache. Empty field will be ignored.\nfunc Key(keys ...string) string {\n\tif len(keys) == 0 {\n\t\treturn \"\"\n\t}\n\tvar builder strings.Builder\n\tbuilder.WriteString(keys[0])\n\tfor _, key := range keys[1:] {\n\t\tif key != \"\" {\n\t\t\tbuilder.WriteRune('/')\n\t\t\tbuilder.WriteString(key)\n\t\t}\n\t}\n\treturn builder.String()\n}\n\ntype Value struct {\n\tname  string\n\tvalue string\n}\n\nfunc String(name, value string) Value {\n\treturn Value{name: name, value: value}\n}\n\nfunc Integer(name string, value int) Value {\n\treturn Value{name: name, value: strconv.Itoa(value)}\n}\n\nfunc Time(name string, value time.Time) Value {\n\treturn Value{name: name, value: value.String()}\n}\n\ntype ReturnValue struct {\n\tvalue  string\n\terr    error\n\texists bool\n}\n\nfunc (r *ReturnValue) String() (string, error) {\n\tif r.err != nil {\n\t\treturn \"\", r.err\n\t}\n\tif !r.exists {\n\t\treturn \"\", nil\n\t}\n\treturn r.value, nil\n}\n\nfunc (r *ReturnValue) Integer() (int, error) {\n\tif r.err != nil {\n\t\treturn 0, r.err\n\t}\n\tif !r.exists {\n\t\treturn 0, nil\n\t}\n\treturn strconv.Atoi(r.value)\n}\n\nfunc (r *ReturnValue) Time() (time.Time, error) {\n\tif r.err != nil {\n\t\treturn time.Time{}, r.err\n\t}\n\tif !r.exists {\n\t\treturn time.Time{}, nil\n\t}\n\tt, err := dateparse.ParseAny(r.value)\n\tif err != nil {\n\t\treturn time.Time{}, errors.Trace(err)\n\t}\n\treturn t.In(time.UTC), nil\n}\n\nfunc (r *ReturnValue) Exists() bool {\n\treturn r.exists\n}\n\ntype Score struct {\n\tId         string\n\tScore      float64\n\tIsHidden   bool      `json:\"-\"`\n\tCategories []string  `json:\"-\" gorm:\"type:text;serializer:json\"`\n\tTimestamp  time.Time `json:\"-\"`\n}\n\nfunc SortDocuments(documents []Score) {\n\tsort.Slice(documents, func(i, j int) bool {\n\t\treturn documents[i].Score > documents[j].Score\n\t})\n}\n\nfunc ConvertDocumentsToValues(documents []Score) []string {\n\tvalues := make([]string, len(documents))\n\tfor i := range values {\n\t\tvalues[i] = documents[i].Id\n\t}\n\treturn values\n}\n\ntype ScoreCondition struct {\n\tSubset *string\n\tId     *string\n\tBefore *time.Time\n}\n\nfunc (condition *ScoreCondition) Check() error {\n\tif condition.Id == nil && condition.Before == nil && condition.Subset == nil {\n\t\treturn errors.NotValidf(\"document condition\")\n\t}\n\treturn nil\n}\n\ntype ScorePatch struct {\n\tIsHidden   *bool\n\tCategories []string\n\tScore      *float64\n}\n\ntype TimeSeriesPoint struct {\n\tName      string    `gorm:\"primaryKey\"`\n\tTimestamp time.Time `gorm:\"primaryKey\"`\n\tValue     float64\n}\n\n// Database is the common interface for cache store.\ntype Database interface {\n\tClose() error\n\tPing() error\n\tInit() error\n\tScan(work func(string) error) error\n\tPurge() error\n\n\tSet(ctx context.Context, values ...Value) error\n\tGet(ctx context.Context, name string) *ReturnValue\n\tDelete(ctx context.Context, name string) error\n\n\tPush(ctx context.Context, name, value string) error\n\tPop(ctx context.Context, name string) (string, error)\n\tRemain(ctx context.Context, name string) (int64, error)\n\n\tAddScores(ctx context.Context, collection, subset string, documents []Score) error\n\tSearchScores(ctx context.Context, collection, subset string, query []string, begin, end int) ([]Score, error)\n\tDeleteScores(ctx context.Context, collection []string, condition ScoreCondition) error\n\tUpdateScores(ctx context.Context, collections []string, subset *string, id string, patch ScorePatch) error\n\tScanScores(ctx context.Context, callback func(collection, id, subset string, timestamp time.Time) error) error\n\n\tAddTimeSeriesPoints(ctx context.Context, points []TimeSeriesPoint) error\n\tGetTimeSeriesPoints(ctx context.Context, name string, begin, end time.Time, duration time.Duration) ([]TimeSeriesPoint, error)\n}\n\n// Creator creates a database instance.\ntype Creator func(path, tablePrefix string, opts ...storage.Option) (Database, error)\n\nvar creators = make(map[string]Creator)\n\n// Register a database creator.\nfunc Register(prefixes []string, creator Creator) {\n\tfor _, p := range prefixes {\n\t\tcreators[p] = creator\n\t}\n}\n\n// Open a connection to a database.\nfunc Open(path, tablePrefix string, opts ...storage.Option) (Database, error) {\n\tfor prefix, creator := range creators {\n\t\tif strings.HasPrefix(path, prefix) {\n\t\t\treturn creator(path, tablePrefix, opts...)\n\t\t}\n\t}\n\treturn nil, errors.Errorf(\"Unknown database: %s\", path)\n}\n"
  },
  {
    "path": "storage/cache/database_test.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage cache\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"math\"\n\t\"math/rand\"\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/fxtlabs/primes\"\n\t\"github.com/juju/errors\"\n\t\"github.com/samber/lo\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/suite\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n)\n\ntype baseTestSuite struct {\n\tsuite.Suite\n\tDatabase\n}\n\nfunc (suite *baseTestSuite) TearDownSuite() {\n\terr := suite.Database.Close()\n\tsuite.NoError(err)\n}\n\nfunc (suite *baseTestSuite) SetupTest() {\n\terr := suite.Database.Ping()\n\tsuite.NoError(err)\n\terr = suite.Database.Purge()\n\tsuite.NoError(err)\n}\n\nfunc (suite *baseTestSuite) TearDownTest() {\n\terr := suite.Database.Purge()\n\tsuite.NoError(err)\n}\n\nfunc (suite *baseTestSuite) TestInit() {\n\terr := suite.Database.Init()\n\tsuite.NoError(err)\n}\n\nfunc (suite *baseTestSuite) TestMeta() {\n\tctx := suite.T().Context()\n\t// Set meta string\n\terr := suite.Database.Set(ctx, String(Key(\"meta\", \"1\"), \"2\"), String(Key(\"meta\", \"1000\"), \"10\"))\n\tsuite.NoError(err)\n\t// Get meta string\n\tvalue, err := suite.Database.Get(ctx, Key(\"meta\", \"1\")).String()\n\tsuite.NoError(err)\n\tsuite.Equal(\"2\", value)\n\tvalue, err = suite.Database.Get(ctx, Key(\"meta\", \"1000\")).String()\n\tsuite.NoError(err)\n\tsuite.Equal(\"10\", value)\n\t// Delete string\n\terr = suite.Database.Delete(ctx, Key(\"meta\", \"1\"))\n\tsuite.NoError(err)\n\t// Get meta not existed\n\tret := suite.Database.Get(ctx, Key(\"meta\", \"1\"))\n\tsuite.False(ret.Exists())\n\tvalue, err = ret.String()\n\tsuite.NoError(err)\n\tsuite.Equal(\"\", value)\n\t// Set meta int\n\terr = suite.Database.Set(ctx, Integer(Key(\"meta\", \"1\"), 2))\n\tsuite.NoError(err)\n\t// Get meta int\n\tvalInt, err := suite.Database.Get(ctx, Key(\"meta\", \"1\")).Integer()\n\tsuite.NoError(err)\n\tsuite.Equal(2, valInt)\n\t// set meta time\n\terr = suite.Database.Set(ctx, Time(Key(\"meta\", \"1\"), time.Date(1996, 4, 8, 0, 0, 0, 0, time.UTC)))\n\tsuite.NoError(err)\n\t// get meta time\n\tvalTime, err := suite.Database.Get(ctx, Key(\"meta\", \"1\")).Time()\n\tsuite.NoError(err)\n\tsuite.Equal(1996, valTime.Year())\n\tsuite.Equal(time.Month(4), valTime.Month())\n\tsuite.Equal(8, valTime.Day())\n\n\t// test set empty\n\terr = suite.Database.Set(ctx)\n\tsuite.NoError(err)\n\t// test set duplicate\n\terr = suite.Database.Set(ctx, String(\"100\", \"1\"), String(\"100\", \"2\"))\n\tsuite.NoError(err)\n}\n\nfunc (suite *baseTestSuite) TestExists() {\n\tctx := suite.T().Context()\n\n\t// Test non-existent key\n\tret := suite.Database.Get(ctx, Key(\"test\", \"nonexistent\"))\n\tsuite.False(ret.Exists())\n\tvalue, err := ret.String()\n\tsuite.NoError(err)\n\tsuite.Equal(\"\", value)\n\n\t// Set a value\n\terr = suite.Database.Set(ctx, String(Key(\"test\", \"exists\"), \"somevalue\"))\n\tsuite.NoError(err)\n\n\t// Test existing key\n\tret = suite.Database.Get(ctx, Key(\"test\", \"exists\"))\n\tsuite.True(ret.Exists())\n\tvalue, err = ret.String()\n\tsuite.NoError(err)\n\tsuite.Equal(\"somevalue\", value)\n\n\t// Delete the key\n\terr = suite.Database.Delete(ctx, Key(\"test\", \"exists\"))\n\tsuite.NoError(err)\n\n\t// Test deleted key no longer exists\n\tret = suite.Database.Get(ctx, Key(\"test\", \"exists\"))\n\tsuite.False(ret.Exists())\n\n\t// Test with integer values\n\terr = suite.Database.Set(ctx, Integer(Key(\"test\", \"int\"), 42))\n\tsuite.NoError(err)\n\tret = suite.Database.Get(ctx, Key(\"test\", \"int\"))\n\tsuite.True(ret.Exists())\n\tintVal, err := ret.Integer()\n\tsuite.NoError(err)\n\tsuite.Equal(42, intVal)\n\n\t// Test non-existent integer - should return 0 with no error\n\tret = suite.Database.Get(ctx, Key(\"test\", \"noint\"))\n\tsuite.False(ret.Exists())\n\tintVal, err = ret.Integer()\n\tsuite.NoError(err)\n\tsuite.Equal(0, intVal)\n}\n\nfunc (suite *baseTestSuite) TestScan() {\n\tctx := suite.T().Context()\n\terr := suite.Database.Set(ctx, String(\"1\", \"1\"))\n\tsuite.NoError(err)\n\n\tvar keys []string\n\terr = suite.Database.Scan(func(s string) error {\n\t\tkeys = append(keys, s)\n\t\treturn nil\n\t})\n\tsuite.NoError(err)\n\tsuite.ElementsMatch([]string{\"1\"}, keys)\n}\n\nfunc (suite *baseTestSuite) TestPurge() {\n\tctx := suite.T().Context()\n\t// insert data\n\terr := suite.Database.Set(ctx, String(\"key\", \"value\"))\n\tsuite.NoError(err)\n\tret := suite.Database.Get(ctx, \"key\")\n\tsuite.NoError(ret.err)\n\tsuite.Equal(\"value\", ret.value)\n\tsuite.True(ret.Exists())\n\n\t// purge data\n\terr = suite.Database.Purge()\n\tsuite.NoError(err)\n\tret = suite.Database.Get(ctx, \"key\")\n\tsuite.False(ret.Exists())\n\n\t// purge empty dataset\n\terr = suite.Database.Purge()\n\tsuite.NoError(err)\n}\n\nfunc (suite *baseTestSuite) TestPushPop() {\n\tctx := suite.T().Context()\n\terr := suite.Push(ctx, \"a\", \"1\")\n\tsuite.NoError(err)\n\terr = suite.Push(ctx, \"a\", \"2\")\n\tsuite.NoError(err)\n\tcount, err := suite.Remain(ctx, \"a\")\n\tsuite.NoError(err)\n\tsuite.Equal(int64(2), count)\n\n\terr = suite.Push(ctx, \"b\", \"1\")\n\tsuite.NoError(err)\n\terr = suite.Push(ctx, \"b\", \"2\")\n\tsuite.NoError(err)\n\terr = suite.Push(ctx, \"b\", \"1\")\n\tsuite.NoError(err)\n\tcount, err = suite.Remain(ctx, \"b\")\n\tsuite.NoError(err)\n\tsuite.Equal(int64(2), count)\n\n\tvalue, err := suite.Pop(ctx, \"a\")\n\tsuite.NoError(err)\n\tsuite.Equal(\"1\", value)\n\tvalue, err = suite.Pop(ctx, \"a\")\n\tsuite.NoError(err)\n\tsuite.Equal(\"2\", value)\n\t_, err = suite.Pop(ctx, \"a\")\n\tsuite.ErrorIs(err, io.EOF)\n\n\tvalue, err = suite.Pop(ctx, \"b\")\n\tsuite.NoError(err)\n\tsuite.Equal(\"2\", value)\n\tvalue, err = suite.Pop(ctx, \"b\")\n\tsuite.NoError(err)\n\tsuite.Equal(\"1\", value)\n\t_, err = suite.Pop(ctx, \"b\")\n\tsuite.ErrorIs(err, io.EOF)\n}\n\nfunc (suite *baseTestSuite) TestDocument() {\n\tts := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)\n\tctx := suite.T().Context()\n\terr := suite.AddScores(ctx, \"a\", \"\", []Score{{\n\t\tId:         \"0\",\n\t\tScore:      math.MaxFloat64,\n\t\tIsHidden:   true,\n\t\tCategories: []string{\"a\", \"b\"},\n\t\tTimestamp:  ts,\n\t}})\n\tsuite.NoError(err)\n\terr = suite.AddScores(ctx, \"a\", \"\", []Score{{\n\t\tId:         \"1\",\n\t\tScore:      100,\n\t\tCategories: []string{\"a\", \"b\"},\n\t\tTimestamp:  ts,\n\t}})\n\tsuite.NoError(err)\n\terr = suite.AddScores(ctx, \"a\", \"\", []Score{\n\t\t{\n\t\t\tId:         \"1\",\n\t\t\tScore:      1,\n\t\t\tCategories: []string{\"a\", \"b\"},\n\t\t\tTimestamp:  ts,\n\t\t},\n\t\t{\n\t\t\tId:         \"2\",\n\t\t\tScore:      2,\n\t\t\tCategories: []string{\"b\", \"c\"},\n\t\t\tTimestamp:  ts,\n\t\t},\n\t\t{\n\t\t\tId:         \"3\",\n\t\t\tScore:      3,\n\t\t\tCategories: []string{\"b\"},\n\t\t\tTimestamp:  time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t},\n\t\t{\n\t\t\tId:         \"4\",\n\t\t\tScore:      4,\n\t\t\tCategories: []string{\"\"},\n\t\t\tTimestamp:  ts,\n\t\t},\n\t\t{\n\t\t\tId:         \"5\",\n\t\t\tScore:      5,\n\t\t\tCategories: []string{\"b\"},\n\t\t\tTimestamp:  ts,\n\t\t},\n\t})\n\tsuite.NoError(err)\n\terr = suite.AddScores(ctx, \"b\", \"\", []Score{{\n\t\tId:         \"6\",\n\t\tScore:      6,\n\t\tCategories: []string{\"b\"},\n\t\tTimestamp:  ts,\n\t}})\n\tsuite.NoError(err)\n\n\t// search documents\n\tdocuments, err := suite.SearchScores(ctx, \"a\", \"\", []string{\"b\"}, 1, 3)\n\tsuite.NoError(err)\n\tsuite.Equal([]Score{\n\t\t{Id: \"3\", Score: 3, Categories: []string{\"b\"}, Timestamp: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)},\n\t\t{Id: \"2\", Score: 2, Categories: []string{\"b\", \"c\"}, Timestamp: ts},\n\t}, documents)\n\tdocuments, err = suite.SearchScores(ctx, \"a\", \"\", []string{\"b\"}, 0, -1)\n\tsuite.NoError(err)\n\tsuite.Equal([]Score{\n\t\t{Id: \"5\", Score: 5, Categories: []string{\"b\"}, Timestamp: ts},\n\t\t{Id: \"3\", Score: 3, Categories: []string{\"b\"}, Timestamp: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)},\n\t\t{Id: \"2\", Score: 2, Categories: []string{\"b\", \"c\"}, Timestamp: ts},\n\t\t{Id: \"1\", Score: 1, Categories: []string{\"a\", \"b\"}, Timestamp: ts},\n\t}, documents)\n\n\t// search documents with nil category\n\tdocuments, err = suite.SearchScores(ctx, \"a\", \"\", nil, 0, -1)\n\tsuite.NoError(err)\n\tsuite.Equal([]Score{\n\t\t{Id: \"5\", Score: 5, Categories: []string{\"b\"}, Timestamp: ts},\n\t\t{Id: \"4\", Score: 4, Categories: []string{\"\"}, Timestamp: ts},\n\t\t{Id: \"3\", Score: 3, Categories: []string{\"b\"}, Timestamp: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)},\n\t\t{Id: \"2\", Score: 2, Categories: []string{\"b\", \"c\"}, Timestamp: ts},\n\t\t{Id: \"1\", Score: 1, Categories: []string{\"a\", \"b\"}, Timestamp: ts},\n\t}, documents)\n\n\t// search documents with empty category\n\tdocuments, err = suite.SearchScores(ctx, \"a\", \"\", []string{\"\"}, 0, -1)\n\tsuite.NoError(err)\n\tsuite.Equal([]Score{{Id: \"4\", Score: 4, Categories: []string{\"\"}, Timestamp: ts}}, documents)\n\n\t// delete nothing\n\terr = suite.DeleteScores(ctx, []string{\"a\"}, ScoreCondition{})\n\tsuite.ErrorIs(err, errors.NotValid)\n\t// delete by value\n\terr = suite.DeleteScores(ctx, []string{\"a\"}, ScoreCondition{Id: new(\"5\")})\n\tsuite.NoError(err)\n\tdocuments, err = suite.SearchScores(ctx, \"a\", \"\", []string{\"b\"}, 0, 1)\n\tsuite.NoError(err)\n\tsuite.Len(documents, 1)\n\tsuite.Equal(\"3\", documents[0].Id)\n\t// delete by timestamp\n\terr = suite.DeleteScores(ctx, []string{\"a\"}, ScoreCondition{Before: lo.ToPtr(time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC))})\n\tsuite.NoError(err)\n\tdocuments, err = suite.SearchScores(ctx, \"a\", \"\", []string{\"b\"}, 0, 1)\n\tsuite.NoError(err)\n\tsuite.Len(documents, 1)\n\tsuite.Equal(\"2\", documents[0].Id)\n\n\t// update categories\n\terr = suite.UpdateScores(ctx, []string{\"a\"}, nil, \"2\", ScorePatch{Categories: []string{\"c\", \"s\"}})\n\tsuite.NoError(err)\n\tdocuments, err = suite.SearchScores(ctx, \"a\", \"\", []string{\"s\"}, 0, 1)\n\tsuite.NoError(err)\n\tsuite.Len(documents, 1)\n\tsuite.Equal(\"2\", documents[0].Id)\n\terr = suite.UpdateScores(ctx, []string{\"a\"}, nil, \"2\", ScorePatch{Categories: []string{\"c\"}})\n\tsuite.NoError(err)\n\tdocuments, err = suite.SearchScores(ctx, \"a\", \"\", []string{\"s\"}, 0, 1)\n\tsuite.NoError(err)\n\tsuite.Empty(documents)\n\n\t// update is hidden\n\terr = suite.UpdateScores(ctx, []string{\"a\"}, nil, \"0\", ScorePatch{IsHidden: new(false)})\n\tsuite.NoError(err)\n\tdocuments, err = suite.SearchScores(ctx, \"a\", \"\", []string{\"b\"}, 0, 1)\n\tsuite.NoError(err)\n\tsuite.Len(documents, 1)\n\tsuite.Equal(\"0\", documents[0].Id)\n}\n\nfunc (suite *baseTestSuite) TestSubsetDocument() {\n\tts := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)\n\tctx := suite.T().Context()\n\terr := suite.AddScores(ctx, \"a\", \"a\", []Score{\n\t\t{\n\t\t\tId:         \"1\",\n\t\t\tScore:      1,\n\t\t\tCategories: []string{\"a\", \"b\"},\n\t\t\tTimestamp:  ts,\n\t\t},\n\t\t{\n\t\t\tId:         \"2\",\n\t\t\tScore:      2,\n\t\t\tCategories: []string{\"b\", \"c\"},\n\t\t\tTimestamp:  ts,\n\t\t},\n\t\t{\n\t\t\tId:         \"3\",\n\t\t\tScore:      3,\n\t\t\tCategories: []string{\"b\"},\n\t\t\tTimestamp:  ts,\n\t\t},\n\t})\n\tsuite.NoError(err)\n\terr = suite.AddScores(ctx, \"b\", \"\", []Score{\n\t\t{\n\t\t\tId:         \"4\",\n\t\t\tScore:      4,\n\t\t\tCategories: []string{},\n\t\t\tTimestamp:  ts,\n\t\t},\n\t\t{\n\t\t\tId:         \"3\",\n\t\t\tScore:      3,\n\t\t\tCategories: []string{\"b\"},\n\t\t\tTimestamp:  ts,\n\t\t},\n\t\t{\n\t\t\tId:         \"2\",\n\t\t\tScore:      2,\n\t\t\tCategories: []string{\"b\"},\n\t\t\tTimestamp:  ts,\n\t\t},\n\t})\n\tsuite.NoError(err)\n\n\t// search documents\n\tdocuments, err := suite.SearchScores(ctx, \"a\", \"a\", []string{\"b\"}, 0, -1)\n\tsuite.NoError(err)\n\tsuite.Equal([]Score{\n\t\t{Id: \"3\", Score: 3, Categories: []string{\"b\"}, Timestamp: ts},\n\t\t{Id: \"2\", Score: 2, Categories: []string{\"b\", \"c\"}, Timestamp: ts},\n\t\t{Id: \"1\", Score: 1, Categories: []string{\"a\", \"b\"}, Timestamp: ts},\n\t}, documents)\n\n\t// update categories\n\terr = suite.UpdateScores(ctx, []string{\"a\", \"b\"}, nil, \"2\", ScorePatch{Categories: []string{\"b\", \"s\"}})\n\tsuite.NoError(err)\n\tdocuments, err = suite.SearchScores(ctx, \"a\", \"a\", []string{\"s\"}, 0, 1)\n\tsuite.NoError(err)\n\tsuite.Len(documents, 1)\n\tsuite.Equal(\"2\", documents[0].Id)\n\tdocuments, err = suite.SearchScores(ctx, \"b\", \"\", []string{\"s\"}, 0, 1)\n\tsuite.NoError(err)\n\tsuite.Len(documents, 1)\n\tsuite.Equal(\"2\", documents[0].Id)\n\n\t// update categories in subset\n\terr = suite.UpdateScores(ctx, []string{\"a\", \"b\"}, new(\"a\"), \"2\", ScorePatch{Categories: []string{\"b\", \"x\"}})\n\tsuite.NoError(err)\n\tdocuments, err = suite.SearchScores(ctx, \"a\", \"a\", []string{\"x\"}, 0, 1)\n\tsuite.NoError(err)\n\tsuite.Len(documents, 1)\n\tsuite.Equal(\"2\", documents[0].Id)\n\tdocuments, err = suite.SearchScores(ctx, \"b\", \"\", []string{\"x\"}, 0, 1)\n\tsuite.NoError(err)\n\tsuite.Empty(documents)\n\n\t// delete by value\n\terr = suite.DeleteScores(ctx, []string{\"a\", \"b\"}, ScoreCondition{Id: new(\"3\")})\n\tsuite.NoError(err)\n\tdocuments, err = suite.SearchScores(ctx, \"a\", \"a\", []string{\"b\"}, 0, 1)\n\tsuite.NoError(err)\n\tsuite.Len(documents, 1)\n\tsuite.Equal(\"2\", documents[0].Id)\n\tdocuments, err = suite.SearchScores(ctx, \"b\", \"\", []string{\"b\"}, 0, 1)\n\tsuite.NoError(err)\n\tsuite.Len(documents, 1)\n\tsuite.Equal(\"2\", documents[0].Id)\n\n\t// delete in subset\n\terr = suite.DeleteScores(ctx, []string{\"a\", \"b\"}, ScoreCondition{\n\t\tSubset: new(\"a\"),\n\t\tId:     new(\"2\"),\n\t})\n\tsuite.NoError(err)\n\tdocuments, err = suite.SearchScores(ctx, \"a\", \"a\", []string{\"b\"}, 0, 1)\n\tsuite.NoError(err)\n\tsuite.Len(documents, 1)\n\tsuite.Equal(\"1\", documents[0].Id)\n\tdocuments, err = suite.SearchScores(ctx, \"b\", \"\", []string{\"b\"}, 0, 1)\n\tsuite.NoError(err)\n\tsuite.Len(documents, 1)\n\tsuite.Equal(\"2\", documents[0].Id)\n}\n\nfunc (suite *baseTestSuite) TestScanScores() {\n\t// add scores\n\ttimestamp := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)\n\tscores := map[lo.Tuple2[string, string]][]Score{\n\t\t{\"a\", \"b\"}: {\n\t\t\t{Id: \"1\", Score: 1, Categories: []string{\"a\", \"b\"}, Timestamp: timestamp},\n\t\t\t{Id: \"2\", Score: 2, Categories: []string{\"b\", \"c\"}, Timestamp: timestamp},\n\t\t\t{Id: \"3\", Score: 3, Categories: []string{\"b\"}, Timestamp: timestamp},\n\t\t},\n\t\t{\"a\", \"c\"}: {\n\t\t\t{Id: \"4\", Score: 4, Categories: []string{\"a\", \"b\"}, Timestamp: timestamp},\n\t\t\t{Id: \"5\", Score: 5, Categories: []string{\"b\", \"c\"}, Timestamp: timestamp},\n\t\t\t{Id: \"6\", Score: 6, Categories: []string{\"b\"}, Timestamp: timestamp},\n\t\t},\n\t\t{\"b\", \"c\"}: {\n\t\t\t{Id: \"7\", Score: 7, Categories: []string{\"a\", \"b\"}, Timestamp: timestamp},\n\t\t\t{Id: \"8\", Score: 8, Categories: []string{\"b\", \"c\"}, Timestamp: timestamp},\n\t\t\t{Id: \"9\", Score: 9, Categories: []string{\"b\"}, Timestamp: timestamp},\n\t\t},\n\t}\n\tfor k, v := range scores {\n\t\terr := suite.AddScores(suite.T().Context(), k.A, k.B, v)\n\t\tsuite.NoError(err)\n\t}\n\n\t// scan scores\n\ttotalScores := 0\n\tctx := suite.T().Context()\n\terr := suite.ScanScores(ctx, func(collection, id, subset string, t time.Time) error {\n\t\ttotalScores++\n\t\tsuite.Equal(timestamp, t.UTC())\n\t\treturn nil\n\t})\n\tsuite.NoError(err)\n\tsuite.Equal(9, totalScores)\n\n\t// scan scores with timeout\n\tscanScores := 0\n\tctx, cancel := context.WithTimeout(suite.T().Context(), time.Millisecond)\n\tdefer cancel()\n\terr = suite.ScanScores(ctx, func(collection, id, subset string, timestamp time.Time) error {\n\t\ttime.Sleep(time.Millisecond)\n\t\tscanScores++\n\t\treturn nil\n\t})\n\tif err != nil && status.Code(err) != codes.DeadlineExceeded {\n\t\tsuite.ErrorIs(err, context.DeadlineExceeded)\n\t}\n\tsuite.Less(scanScores, 9)\n}\n\nfunc (suite *baseTestSuite) TestTimeSeries() {\n\tts := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)\n\tctx := suite.T().Context()\n\terr := suite.AddTimeSeriesPoints(ctx, []TimeSeriesPoint{\n\t\t{Name: \"a\", Value: 1, Timestamp: ts.Add(1 * time.Second)},\n\t\t{Name: \"a\", Value: 2, Timestamp: ts.Add(2 * time.Second)},\n\t\t{Name: \"a\", Value: 3, Timestamp: ts.Add(3 * time.Second)},\n\t\t{Name: \"a\", Value: 4, Timestamp: ts.Add(4 * time.Second)},\n\t\t{Name: \"a\", Value: 5, Timestamp: ts.Add(5 * time.Second)},\n\t\t{Name: \"b\", Value: 3, Timestamp: ts.Add(3 * time.Second)},\n\t})\n\tsuite.NoError(err)\n\n\tpoints, err := suite.GetTimeSeriesPoints(ctx, \"a\", ts.Add(2*time.Second), ts.Add(4*time.Second), time.Second)\n\tsuite.NoError(err)\n\tsuite.Equal([]TimeSeriesPoint{\n\t\t{Name: \"a\", Value: 2, Timestamp: ts.Add(2 * time.Second)},\n\t\t{Name: \"a\", Value: 3, Timestamp: ts.Add(3 * time.Second)},\n\t\t{Name: \"a\", Value: 4, Timestamp: ts.Add(4 * time.Second)},\n\t}, points)\n\n\tpoints, err = suite.GetTimeSeriesPoints(ctx, \"a\", ts.Add(2*time.Second), ts.Add(4*time.Second), 2*time.Second)\n\tsuite.NoError(err)\n\tsuite.Equal([]TimeSeriesPoint{\n\t\t{Name: \"a\", Value: 3, Timestamp: ts.Add(2 * time.Second)},\n\t\t{Name: \"a\", Value: 4, Timestamp: ts.Add(4 * time.Second)},\n\t}, points)\n}\n\nfunc (suite *baseTestSuite) TestTimestampPrecision() {\n\tctx := suite.T().Context()\n\ttimestamp := time.Date(2023, 1, 1, 0, 0, 0, 500, time.UTC)\n\t// add scores\n\terr := suite.Database.AddScores(ctx, \"a\", \"s\", []Score{\n\t\t{Id: \"1\", Score: 1, Categories: []string{\"\"}, Timestamp: timestamp},\n\t})\n\tsuite.NoError(err)\n\t// remove by timestamp\n\terr = suite.Database.DeleteScores(ctx, []string{\"a\"}, ScoreCondition{\n\t\tSubset: new(\"s\"),\n\t\tBefore: lo.ToPtr(timestamp)})\n\tsuite.NoError(err)\n\t// search scores\n\tdocuments, err := suite.Database.SearchScores(ctx, \"a\", \"s\", nil, 0, -1)\n\tsuite.NoError(err)\n\tsuite.NotEmpty(documents)\n}\n\nfunc TestKey(t *testing.T) {\n\tassert.Empty(t, Key())\n\tassert.Equal(t, \"a\", Key(\"a\"))\n\tassert.Equal(t, \"a\", Key(\"a\", \"\"))\n\tassert.Equal(t, \"a/b\", Key(\"a\", \"b\"))\n}\n\nvar (\n\tbenchmarkDataSize = 100000\n\tprimeTable        []int\n)\n\nfunc init() {\n\tbenchmarkDataSizeStr := os.Getenv(\"BENCHMARK_DATA_SIZE\")\n\tif benchmarkDataSizeStr != \"\" {\n\t\tbenchmarkDataSize, _ = strconv.Atoi(benchmarkDataSizeStr)\n\t}\n\tprimeTable = primes.Sieve(benchmarkDataSize)\n}\n\nfunc primeFactor(n int) []int {\n\tvar factors []int\n\tfor _, p := range primeTable {\n\t\tif n%p == 0 {\n\t\t\tfactors = append(factors, p)\n\t\t}\n\t}\n\treturn factors\n}\n\nfunc benchmark(b *testing.B, database Database) {\n\tb.Run(\"AddScores\", func(b *testing.B) {\n\t\tbenchmarkAddDocuments(b, database)\n\t})\n\tb.Run(\"SearchScores\", func(b *testing.B) {\n\t\tbenchmarkSearchDocuments(b, database)\n\t})\n\tb.Run(\"UpdateScores\", func(b *testing.B) {\n\t\tbenchmarkUpdateDocuments(b, database)\n\t})\n}\n\nfunc benchmarkAddDocuments(b *testing.B, database Database) {\n\tctx := b.Context()\n\tvar documents []Score\n\tfor i := 1; i <= b.N; i++ {\n\t\tdocuments = append(documents, Score{\n\t\t\tId:         strconv.Itoa(i),\n\t\t\tScore:      float64(-i),\n\t\t\tCategories: lo.Map(primeFactor(i), func(n, _ int) string { return strconv.Itoa(n) }),\n\t\t\tTimestamp:  time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t})\n\t}\n\tb.ResetTimer()\n\terr := database.AddScores(ctx, \"a\", \"\", documents)\n\tassert.NoError(b, err)\n}\n\nfunc benchmarkSearchDocuments(b *testing.B, database Database) {\n\t// insert data\n\tctx := b.Context()\n\tvar documents []Score\n\tfor i := 1; i <= benchmarkDataSize; i++ {\n\t\tdocuments = append(documents, Score{\n\t\t\tId:         strconv.Itoa(i),\n\t\t\tScore:      float64(-i),\n\t\t\tCategories: lo.Map(primeFactor(i), func(n, _ int) string { return strconv.Itoa(n) }),\n\t\t\tTimestamp:  time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t})\n\t}\n\terr := database.AddScores(ctx, \"a\", \"\", documents)\n\tassert.NoError(b, err)\n\t// search data\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\t// select a random prime\n\t\tp := primeTable[rand.Intn(len(primeTable))]\n\t\t// search documents\n\t\tr, err := database.SearchScores(ctx, \"a\", \"\", []string{strconv.Itoa(p)}, 0, 10)\n\t\tassert.NoError(b, err)\n\t\tassert.NotEmpty(b, r)\n\t}\n}\n\nfunc benchmarkUpdateDocuments(b *testing.B, database Database) {\n\tctx := b.Context()\n\tb.ResetTimer()\n\tfor i := 1; i <= b.N; i++ {\n\t\t// select a random number\n\t\tn := rand.Intn(benchmarkDataSize) + 1\n\t\t// update documents\n\t\terr := database.UpdateScores(ctx, []string{\"a\"}, nil, strconv.Itoa(n), ScorePatch{\n\t\t\tScore: new(float64(n)),\n\t\t})\n\t\tassert.NoError(b, err)\n\t}\n}\n"
  },
  {
    "path": "storage/cache/mongodb.go",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cache\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/storage\"\n\t\"github.com/juju/errors\"\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n\t\"go.mongodb.org/mongo-driver/x/mongo/driver/connstring\"\n\t\"go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo\"\n)\n\nfunc init() {\n\tRegister([]string{storage.MongoPrefix, storage.MongoSrvPrefix}, func(path, tablePrefix string, opts ...storage.Option) (Database, error) {\n\t\t// connect to database\n\t\tdatabase := new(MongoDB)\n\t\tclientOpts := options.Client()\n\t\tclientOpts.Monitor = otelmongo.NewMonitor()\n\t\tclientOpts.ApplyURI(path)\n\t\tvar err error\n\t\tif database.client, err = mongo.Connect(context.Background(), clientOpts); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\t// parse DSN and extract database name\n\t\tif cs, err := connstring.ParseAndValidate(path); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t} else {\n\t\t\tdatabase.dbName = cs.Database\n\t\t\tdatabase.TablePrefix = storage.TablePrefix(tablePrefix)\n\t\t}\n\t\treturn database, nil\n\t})\n}\n\ntype MongoDB struct {\n\tstorage.TablePrefix\n\tclient *mongo.Client\n\tdbName string\n}\n\nfunc (m MongoDB) Init() error {\n\tctx := context.Background()\n\td := m.client.Database(m.dbName)\n\t// list collections\n\tvar hasValues bool\n\tcollections, err := d.ListCollectionNames(ctx, bson.M{})\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tfor _, collectionName := range collections {\n\t\tswitch collectionName {\n\t\tcase m.ValuesTable():\n\t\t\thasValues = true\n\t\t}\n\t}\n\t// create collections\n\tif !hasValues {\n\t\tif err = d.CreateCollection(ctx, m.ValuesTable()); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\t_, err = d.Collection(m.MessageTable()).Indexes().CreateMany(ctx, []mongo.IndexModel{\n\t\t{\n\t\t\t// update set ... where name = ? and value = ?\n\t\t\tKeys: bson.D{\n\t\t\t\t{\"name\", 1},\n\t\t\t\t{\"value\", 1},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// select * from messages where name = ? order by timestamp asc limit 1\n\t\t\tKeys: bson.D{\n\t\t\t\t{\"name\", 1},\n\t\t\t\t{\"timestamp\", 1},\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t_, err = d.Collection(m.DocumentTable()).Indexes().CreateMany(ctx, []mongo.IndexModel{\n\t\t{\n\t\t\tKeys: bson.D{\n\t\t\t\t{\"collection\", 1},\n\t\t\t\t{\"subset\", 1},\n\t\t\t\t{\"id\", 1},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tKeys: bson.D{\n\t\t\t\t{\"collection\", 1},\n\t\t\t\t{\"subset\", 1},\n\t\t\t\t{\"categories\", 1},\n\t\t\t\t{\"is_hidden\", 1},\n\t\t\t\t{\"score\", -1},\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t_, err = d.Collection(m.PointsTable()).Indexes().CreateMany(ctx, []mongo.IndexModel{\n\t\t{\n\t\t\t// update set ... where name = ? and timestammp = ?\n\t\t\tKeys: bson.D{\n\t\t\t\t{\"name\", 1},\n\t\t\t\t{\"timestamp\", 1},\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\treturn nil\n}\n\nfunc (m MongoDB) Close() error {\n\treturn m.client.Disconnect(context.Background())\n}\n\nfunc (m MongoDB) Ping() error {\n\treturn m.client.Ping(context.Background(), nil)\n}\n\nfunc (m MongoDB) Scan(work func(string) error) error {\n\tctx := context.Background()\n\n\t// scan values\n\tvaluesCollection := m.client.Database(m.dbName).Collection(m.ValuesTable())\n\tvaluesIterator, err := valuesCollection.Find(ctx, bson.M{})\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tdefer valuesIterator.Close(ctx)\n\tfor valuesIterator.Next(ctx) {\n\t\tvar row bson.Raw\n\t\tif err = valuesIterator.Decode(&row); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\tif err = work(row.Lookup(\"_id\").StringValue()); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (m MongoDB) Purge() error {\n\ttables := []string{m.ValuesTable(), m.DocumentTable()}\n\tfor _, tableName := range tables {\n\t\tc := m.client.Database(m.dbName).Collection(tableName)\n\t\t_, err := c.DeleteMany(context.Background(), bson.D{})\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (m MongoDB) Set(ctx context.Context, values ...Value) error {\n\tif len(values) == 0 {\n\t\treturn nil\n\t}\n\tc := m.client.Database(m.dbName).Collection(m.ValuesTable())\n\tvar models []mongo.WriteModel\n\tfor _, value := range values {\n\t\tmodels = append(models, mongo.NewUpdateOneModel().\n\t\t\tSetUpsert(true).\n\t\t\tSetFilter(bson.M{\"_id\": value.name}).\n\t\t\tSetUpdate(bson.M{\"$set\": bson.M{\"_id\": value.name, \"value\": value.value}}))\n\t}\n\t_, err := c.BulkWrite(ctx, models)\n\treturn errors.Trace(err)\n}\n\nfunc (m MongoDB) Get(ctx context.Context, name string) *ReturnValue {\n\tc := m.client.Database(m.dbName).Collection(m.ValuesTable())\n\tr := c.FindOne(ctx, bson.M{\"_id\": bson.M{\"$eq\": name}})\n\tif err := r.Err(); err == mongo.ErrNoDocuments {\n\t\treturn &ReturnValue{value: \"\", exists: false}\n\t} else if err != nil {\n\t\treturn &ReturnValue{err: errors.Trace(err), exists: false}\n\t}\n\tif raw, err := r.DecodeBytes(); err != nil {\n\t\treturn &ReturnValue{err: errors.Trace(err), exists: false}\n\t} else {\n\t\treturn &ReturnValue{value: raw.Lookup(\"value\").StringValue(), exists: true}\n\t}\n}\n\nfunc (m MongoDB) Delete(ctx context.Context, name string) error {\n\tc := m.client.Database(m.dbName).Collection(m.ValuesTable())\n\t_, err := c.DeleteOne(ctx, bson.M{\"_id\": bson.M{\"$eq\": name}})\n\treturn errors.Trace(err)\n}\n\nfunc (m MongoDB) Push(ctx context.Context, name, value string) error {\n\t_, err := m.client.Database(m.dbName).Collection(m.MessageTable()).UpdateOne(ctx,\n\t\tbson.M{\"name\": name, \"value\": value},\n\t\tbson.M{\"$set\": bson.M{\"name\": name, \"value\": value, \"timestamp\": time.Now().UnixNano()}},\n\t\toptions.Update().SetUpsert(true))\n\treturn err\n}\n\nfunc (m MongoDB) Pop(ctx context.Context, name string) (string, error) {\n\tresult := m.client.Database(m.dbName).Collection(m.MessageTable()).FindOneAndDelete(ctx,\n\t\tbson.M{\"name\": name}, options.FindOneAndDelete().SetSort(bson.M{\"timestamp\": 1}))\n\tif err := result.Err(); err == mongo.ErrNoDocuments {\n\t\treturn \"\", io.EOF\n\t} else if err != nil {\n\t\treturn \"\", errors.Trace(err)\n\t}\n\tvar b bson.M\n\tif err := result.Decode(&b); err != nil {\n\t\treturn \"\", errors.Trace(err)\n\t}\n\treturn b[\"value\"].(string), nil\n}\n\nfunc (m MongoDB) Remain(ctx context.Context, name string) (int64, error) {\n\treturn m.client.Database(m.dbName).Collection(m.MessageTable()).CountDocuments(ctx, bson.M{\n\t\t\"name\": name,\n\t})\n}\n\nfunc (m MongoDB) AddScores(ctx context.Context, collection, subset string, documents []Score) error {\n\tif len(documents) == 0 {\n\t\treturn nil\n\t}\n\tvar models []mongo.WriteModel\n\tfor _, document := range documents {\n\t\tmodels = append(models, mongo.NewUpdateOneModel().\n\t\t\tSetUpsert(true).\n\t\t\tSetFilter(bson.M{\n\t\t\t\t\"collection\": collection,\n\t\t\t\t\"subset\":     subset,\n\t\t\t\t\"id\":         document.Id,\n\t\t\t}).\n\t\t\tSetUpdate(bson.M{\"$set\": bson.M{\n\t\t\t\t\"score\":      document.Score,\n\t\t\t\t\"is_hidden\":  document.IsHidden,\n\t\t\t\t\"categories\": document.Categories,\n\t\t\t\t\"timestamp\":  document.Timestamp,\n\t\t\t}}))\n\t}\n\t_, err := m.client.Database(m.dbName).Collection(m.DocumentTable()).BulkWrite(ctx, models)\n\treturn errors.Trace(err)\n}\n\nfunc (m MongoDB) SearchScores(ctx context.Context, collection, subset string, query []string, begin, end int) ([]Score, error) {\n\topt := options.Find().SetSkip(int64(begin)).SetSort(bson.M{\"score\": -1})\n\tif end != -1 {\n\t\topt.SetLimit(int64(end - begin))\n\t}\n\tfilter := bson.M{\n\t\t\"collection\": collection,\n\t\t\"subset\":     subset,\n\t\t\"is_hidden\":  false,\n\t}\n\tif len(query) > 0 {\n\t\tfilter[\"categories\"] = bson.M{\"$all\": query}\n\t}\n\tcur, err := m.client.Database(m.dbName).Collection(m.DocumentTable()).Find(ctx, filter, opt)\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\tdocuments := make([]Score, 0)\n\tfor cur.Next(ctx) {\n\t\tvar document Score\n\t\tif err = cur.Decode(&document); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tdocuments = append(documents, document)\n\t}\n\treturn documents, nil\n}\n\nfunc (m MongoDB) UpdateScores(ctx context.Context, collections []string, subset *string, id string, patch ScorePatch) error {\n\tif len(collections) == 0 {\n\t\treturn nil\n\t}\n\tif patch.IsHidden == nil && patch.Categories == nil && patch.Score == nil {\n\t\treturn nil\n\t}\n\tfilter := bson.M{\n\t\t\"collection\": bson.M{\"$in\": collections},\n\t\t\"id\":         id,\n\t}\n\tif subset != nil {\n\t\tfilter[\"subset\"] = *subset\n\t}\n\tupdate := bson.D{}\n\tif patch.IsHidden != nil {\n\t\tupdate = append(update, bson.E{Key: \"$set\", Value: bson.M{\"is_hidden\": *patch.IsHidden}})\n\t}\n\tif patch.Categories != nil {\n\t\tupdate = append(update, bson.E{Key: \"$set\", Value: bson.M{\"categories\": patch.Categories}})\n\t}\n\tif patch.Score != nil {\n\t\tupdate = append(update, bson.E{Key: \"$set\", Value: bson.M{\"score\": *patch.Score}})\n\t}\n\t_, err := m.client.Database(m.dbName).Collection(m.DocumentTable()).UpdateMany(ctx, filter, update)\n\treturn errors.Trace(err)\n}\n\nfunc (m MongoDB) DeleteScores(ctx context.Context, collections []string, condition ScoreCondition) error {\n\tif err := condition.Check(); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tfilter := bson.M{\"collection\": bson.M{\"$in\": collections}}\n\tif condition.Subset != nil {\n\t\tfilter[\"subset\"] = condition.Subset\n\t}\n\tif condition.Id != nil {\n\t\tfilter[\"id\"] = condition.Id\n\t}\n\tif condition.Before != nil {\n\t\tfilter[\"timestamp\"] = bson.M{\"$lt\": condition.Before}\n\t}\n\t_, err := m.client.Database(m.dbName).Collection(m.DocumentTable()).DeleteMany(ctx, filter)\n\treturn errors.Trace(err)\n}\n\nfunc (m MongoDB) ScanScores(ctx context.Context, callback func(collection string, id string, subset string, timestamp time.Time) error) error {\n\tcursor, err := m.client.Database(m.dbName).Collection(m.DocumentTable()).Find(ctx, bson.M{})\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tdefer cursor.Close(ctx)\n\tfor cursor.Next(ctx) {\n\t\t// check context cancellation\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn errors.Trace(ctx.Err())\n\t\tdefault:\n\t\t}\n\t\t// decode document\n\t\tcollection := cursor.Current.Lookup(\"collection\").StringValue()\n\t\tsubset := cursor.Current.Lookup(\"subset\").StringValue()\n\t\tid := cursor.Current.Lookup(\"id\").StringValue()\n\t\ttimestamp := cursor.Current.Lookup(\"timestamp\").Time()\n\t\tif err = callback(collection, id, subset, timestamp); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (m MongoDB) AddTimeSeriesPoints(ctx context.Context, points []TimeSeriesPoint) error {\n\tvar models []mongo.WriteModel\n\tfor _, point := range points {\n\t\tmodels = append(models, mongo.NewUpdateOneModel().\n\t\t\tSetUpsert(true).\n\t\t\tSetFilter(bson.M{\n\t\t\t\t\"name\":      point.Name,\n\t\t\t\t\"timestamp\": point.Timestamp,\n\t\t\t}).\n\t\t\tSetUpdate(bson.M{\"$set\": bson.M{\n\t\t\t\t\"value\": point.Value,\n\t\t\t}}))\n\t}\n\t_, err := m.client.Database(m.dbName).Collection(m.PointsTable()).BulkWrite(ctx, models)\n\treturn errors.Trace(err)\n}\n\nfunc (m MongoDB) GetTimeSeriesPoints(ctx context.Context, name string, begin, end time.Time, duration time.Duration) ([]TimeSeriesPoint, error) {\n\tcursor, err := m.client.Database(m.dbName).Collection(m.PointsTable()).Aggregate(ctx, []bson.M{\n\t\t{\"$match\": bson.M{\n\t\t\t\"name\":      name,\n\t\t\t\"timestamp\": bson.M{\"$gte\": begin, \"$lte\": end},\n\t\t}},\n\t\t{\"$sort\": bson.M{\"timestamp\": -1}},\n\t\t{\"$group\": bson.M{\n\t\t\t\"_id\": bson.M{\n\t\t\t\t\"$multiply\": bson.A{\n\t\t\t\t\tbson.M{\"$floor\": bson.M{\"$divide\": bson.A{bson.M{\"$toLong\": \"$timestamp\"}, duration.Milliseconds()}}},\n\t\t\t\t\tduration.Milliseconds(),\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"name\":  bson.M{\"$first\": \"$name\"},\n\t\t\t\"value\": bson.M{\"$first\": \"$value\"},\n\t\t}},\n\t\t{\"$project\": bson.M{\n\t\t\t\"_id\":       0,\n\t\t\t\"timestamp\": bson.M{\"$toDate\": \"$_id\"},\n\t\t\t\"name\":      1,\n\t\t\t\"value\":     1,\n\t\t}},\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\tdefer cursor.Close(ctx)\n\tvar points []TimeSeriesPoint\n\tfor cursor.Next(ctx) {\n\t\tvar point TimeSeriesPoint\n\t\tif err = cursor.Decode(&point); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tpoints = append(points, point)\n\t}\n\tif err = cursor.Err(); err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\treturn points, nil\n}\n"
  },
  {
    "path": "storage/cache/mongodb_test.go",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cache\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\nvar (\n\tmongoUri string\n)\n\nfunc init() {\n\t// get environment variables\n\tenv := func(key, defaultValue string) string {\n\t\tif value := os.Getenv(key); value != \"\" {\n\t\t\treturn value\n\t\t}\n\t\treturn defaultValue\n\t}\n\tmongoUri = env(\"MONGO_URI\", \"mongodb://root:password@127.0.0.1:27017/\")\n}\n\ntype MongoTestSuite struct {\n\tbaseTestSuite\n}\n\nfunc (suite *MongoTestSuite) SetupSuite() {\n\tctx := suite.T().Context()\n\tvar err error\n\t// create database\n\tsuite.Database, err = Open(mongoUri, \"gorse_\")\n\tsuite.NoError(err)\n\tdbName := \"gorse_cache_test\"\n\tdatabaseComm := suite.getMongoDB()\n\tsuite.NoError(err)\n\terr = databaseComm.client.Database(dbName).Drop(ctx)\n\tif err == nil {\n\t\tsuite.T().Log(\"delete existed database:\", dbName)\n\t}\n\terr = suite.Database.Close()\n\tsuite.NoError(err)\n\t// create schema\n\tsuite.Database, err = Open(mongoUri+dbName+\"?authSource=admin&connect=direct\", \"gorse_\")\n\tsuite.NoError(err)\n\terr = suite.Database.Init()\n\tsuite.NoError(err)\n}\n\nfunc (suite *MongoTestSuite) getMongoDB() *MongoDB {\n\tvar mongoDatabase *MongoDB\n\tvar ok bool\n\tmongoDatabase, ok = suite.Database.(*MongoDB)\n\tsuite.True(ok)\n\treturn mongoDatabase\n}\n\nfunc TestMongo(t *testing.T) {\n\tsuite.Run(t, new(MongoTestSuite))\n}\n\nfunc BenchmarkMongo(b *testing.B) {\n\tlog.CloseLogger()\n\tctx := b.Context()\n\t// create database\n\tdatabase, err := Open(mongoUri, \"gorse_\")\n\tassert.NoError(b, err)\n\tdbName := \"gorse_cache_benchmark\"\n\tdatabaseComm := database.(*MongoDB)\n\t_ = databaseComm.client.Database(dbName).Drop(ctx)\n\terr = database.Close()\n\tassert.NoError(b, err)\n\t// create schema\n\tdatabase, err = Open(mongoUri+dbName+\"?authSource=admin&connect=direct\", \"gorse_\")\n\tassert.NoError(b, err)\n\terr = database.Init()\n\tassert.NoError(b, err)\n\t// benchmark\n\tbenchmark(b, database)\n}\n"
  },
  {
    "path": "storage/cache/no_database.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cache\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n// NoDatabase means no database used for cache.\ntype NoDatabase struct{}\n\n// Close method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) Close() error {\n\treturn ErrNoDatabase\n}\n\nfunc (NoDatabase) Ping() error {\n\treturn ErrNoDatabase\n}\n\n// Init method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) Init() error {\n\treturn ErrNoDatabase\n}\n\nfunc (NoDatabase) Scan(_ func(string) error) error {\n\treturn ErrNoDatabase\n}\n\nfunc (NoDatabase) Purge() error {\n\treturn ErrNoDatabase\n}\n\nfunc (NoDatabase) Set(_ context.Context, _ ...Value) error {\n\treturn ErrNoDatabase\n}\n\n// Get method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) Get(_ context.Context, _ string) *ReturnValue {\n\treturn &ReturnValue{err: ErrNoDatabase, exists: false}\n}\n\n// Delete method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) Delete(_ context.Context, _ string) error {\n\treturn ErrNoDatabase\n}\n\nfunc (NoDatabase) Push(_ context.Context, _, _ string) error {\n\treturn ErrNoDatabase\n}\n\nfunc (NoDatabase) Pop(_ context.Context, _ string) (string, error) {\n\treturn \"\", ErrNoDatabase\n}\n\nfunc (NoDatabase) Remain(_ context.Context, _ string) (int64, error) {\n\treturn 0, ErrNoDatabase\n}\n\nfunc (NoDatabase) AddScores(_ context.Context, _, _ string, _ []Score) error {\n\treturn ErrNoDatabase\n}\n\nfunc (NoDatabase) SearchScores(_ context.Context, _, _ string, _ []string, _, _ int) ([]Score, error) {\n\treturn nil, ErrNoDatabase\n}\n\nfunc (NoDatabase) UpdateScores(context.Context, []string, *string, string, ScorePatch) error {\n\treturn ErrNoDatabase\n}\n\nfunc (NoDatabase) DeleteScores(_ context.Context, _ []string, _ ScoreCondition) error {\n\treturn ErrNoDatabase\n}\n\nfunc (NoDatabase) ScanScores(context.Context, func(collection, id, subset string, timestamp time.Time) error) error {\n\treturn ErrNoDatabase\n}\n\nfunc (NoDatabase) AddTimeSeriesPoints(_ context.Context, _ []TimeSeriesPoint) error {\n\treturn ErrNoDatabase\n}\n\nfunc (NoDatabase) GetTimeSeriesPoints(_ context.Context, _ string, _, _ time.Time, _ time.Duration) ([]TimeSeriesPoint, error) {\n\treturn nil, ErrNoDatabase\n}\n"
  },
  {
    "path": "storage/cache/no_database_test.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cache\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNoDatabase(t *testing.T) {\n\tctx := t.Context()\n\tvar database NoDatabase\n\terr := database.Close()\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\terr = database.Ping()\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\terr = database.Init()\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\terr = database.Scan(nil)\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\terr = database.Purge()\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\terr = database.Set(ctx)\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, err = database.Get(ctx, Key(\"\", \"\")).String()\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, err = database.Get(ctx, Key(\"\", \"\")).Integer()\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, err = database.Get(ctx, Key(\"\", \"\")).Time()\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\terr = database.Delete(ctx, Key(\"\", \"\"))\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\n\terr = database.Push(ctx, \"\", \"\")\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, err = database.Pop(ctx, \"\")\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, err = database.Remain(ctx, \"\")\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\n\terr = database.AddScores(ctx, \"\", \"\", nil)\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, err = database.SearchScores(ctx, \"\", \"\", nil, 0, 0)\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\terr = database.UpdateScores(ctx, nil, nil, \"\", ScorePatch{})\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\terr = database.DeleteScores(ctx, nil, ScoreCondition{})\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\terr = database.ScanScores(ctx, nil)\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\n\terr = database.AddTimeSeriesPoints(ctx, nil)\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, err = database.GetTimeSeriesPoints(ctx, \"\", time.Time{}, time.Time{}, 0)\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n}\n"
  },
  {
    "path": "storage/cache/proxy.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cache\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/protocol\"\n\t\"github.com/juju/errors\"\n\t\"github.com/samber/lo\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n)\n\ntype ProxyServer struct {\n\tprotocol.UnimplementedCacheStoreServer\n\tdatabase Database\n\tserver   *grpc.Server\n}\n\nfunc NewProxyServer(database Database) *ProxyServer {\n\treturn &ProxyServer{database: database}\n}\n\nfunc (p *ProxyServer) Serve(lis net.Listener) error {\n\tp.server = grpc.NewServer()\n\tprotocol.RegisterCacheStoreServer(p.server, p)\n\treturn p.server.Serve(lis)\n}\n\nfunc (p *ProxyServer) Stop() {\n\tp.server.Stop()\n}\n\nfunc (p *ProxyServer) Ping(context.Context, *protocol.PingRequest) (*protocol.PingResponse, error) {\n\treturn &protocol.PingResponse{}, p.database.Ping()\n}\n\nfunc (p *ProxyServer) Get(ctx context.Context, request *protocol.GetRequest) (*protocol.GetResponse, error) {\n\tvalue := p.database.Get(ctx, request.GetName())\n\tif !value.Exists() {\n\t\treturn &protocol.GetResponse{}, nil\n\t}\n\treturn &protocol.GetResponse{Value: new(value.value)}, value.err\n}\n\nfunc (p *ProxyServer) Set(ctx context.Context, request *protocol.SetRequest) (*protocol.SetResponse, error) {\n\tvalues := make([]Value, len(request.Values))\n\tfor i, value := range request.Values {\n\t\tvalues[i] = Value{\n\t\t\tname:  value.GetName(),\n\t\t\tvalue: value.GetValue(),\n\t\t}\n\t}\n\treturn &protocol.SetResponse{}, p.database.Set(ctx, values...)\n}\n\nfunc (p *ProxyServer) Delete(ctx context.Context, request *protocol.DeleteRequest) (*protocol.DeleteResponse, error) {\n\treturn &protocol.DeleteResponse{}, p.database.Delete(ctx, request.GetName())\n}\n\nfunc (p *ProxyServer) Push(ctx context.Context, request *protocol.PushRequest) (*protocol.PushResponse, error) {\n\treturn &protocol.PushResponse{}, p.database.Push(ctx, request.GetName(), request.GetValue())\n}\n\nfunc (p *ProxyServer) Pop(ctx context.Context, request *protocol.PopRequest) (*protocol.PopResponse, error) {\n\tvalue, err := p.database.Pop(ctx, request.GetName())\n\tif err != nil {\n\t\tif errors.Is(err, io.EOF) {\n\t\t\treturn &protocol.PopResponse{}, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn &protocol.PopResponse{Value: &value}, nil\n}\n\nfunc (p *ProxyServer) Remain(ctx context.Context, request *protocol.RemainRequest) (*protocol.RemainResponse, error) {\n\tcount, err := p.database.Remain(ctx, request.GetName())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &protocol.RemainResponse{Count: count}, nil\n}\n\nfunc (p *ProxyServer) AddScores(ctx context.Context, request *protocol.AddScoresRequest) (*protocol.AddScoresResponse, error) {\n\tscores := make([]Score, len(request.Documents))\n\tfor i, doc := range request.Documents {\n\t\tscores[i] = Score{\n\t\t\tId:         doc.GetId(),\n\t\t\tScore:      doc.GetScore(),\n\t\t\tIsHidden:   doc.GetIsHidden(),\n\t\t\tCategories: doc.GetCategories(),\n\t\t\tTimestamp:  doc.GetTimestamp().AsTime(),\n\t\t}\n\t}\n\treturn &protocol.AddScoresResponse{}, p.database.AddScores(ctx, request.GetCollection(), request.GetSubset(), scores)\n}\n\nfunc (p *ProxyServer) SearchScores(ctx context.Context, request *protocol.SearchScoresRequest) (*protocol.SearchScoresResponse, error) {\n\tresp, err := p.database.SearchScores(ctx, request.GetCollection(), request.GetSubset(), request.GetQuery(), int(request.GetBegin()), int(request.GetEnd()))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tscores := make([]*protocol.Score, len(resp))\n\tfor i, score := range resp {\n\t\tscores[i] = &protocol.Score{\n\t\t\tId:         score.Id,\n\t\t\tScore:      score.Score,\n\t\t\tIsHidden:   score.IsHidden,\n\t\t\tCategories: score.Categories,\n\t\t\tTimestamp:  timestamppb.New(score.Timestamp),\n\t\t}\n\t}\n\treturn &protocol.SearchScoresResponse{Documents: scores}, nil\n}\n\nfunc (p *ProxyServer) DeleteScores(ctx context.Context, request *protocol.DeleteScoresRequest) (*protocol.DeleteScoresResponse, error) {\n\tvar before *time.Time\n\tif request.Condition.Before != nil {\n\t\tbefore = lo.ToPtr(request.Condition.Before.AsTime())\n\t}\n\treturn &protocol.DeleteScoresResponse{}, p.database.DeleteScores(ctx, request.GetCollection(), ScoreCondition{\n\t\tSubset: request.Condition.Subset,\n\t\tId:     request.Condition.Id,\n\t\tBefore: before,\n\t})\n}\n\nfunc (p *ProxyServer) UpdateScores(ctx context.Context, request *protocol.UpdateScoresRequest) (*protocol.UpdateScoresResponse, error) {\n\treturn &protocol.UpdateScoresResponse{}, p.database.UpdateScores(ctx, request.GetCollection(), request.Subset, request.GetId(), ScorePatch{\n\t\tIsHidden:   request.GetPatch().IsHidden,\n\t\tCategories: request.GetPatch().Categories,\n\t\tScore:      request.GetPatch().Score,\n\t})\n}\n\nfunc (p *ProxyServer) ScanScores(request *protocol.ScanScoresRequest, stream grpc.ServerStreamingServer[protocol.ScanScoresResponse]) error {\n\terr := p.database.ScanScores(stream.Context(), func(collection, id, subset string, timestamp time.Time) error {\n\t\treturn stream.Send(&protocol.ScanScoresResponse{\n\t\t\tCollection: collection,\n\t\t\tId:         id,\n\t\t\tSubset:     subset,\n\t\t\tTimestamp:  timestamppb.New(timestamp),\n\t\t})\n\t})\n\tif err != nil {\n\t\treturn status.Errorf(codes.Internal, \"failed to scan scores: %v\", err)\n\t}\n\treturn nil\n}\n\nfunc (p *ProxyServer) AddTimeSeriesPoints(ctx context.Context, request *protocol.AddTimeSeriesPointsRequest) (*protocol.AddTimeSeriesPointsResponse, error) {\n\tpoints := make([]TimeSeriesPoint, len(request.Points))\n\tfor i, point := range request.Points {\n\t\tpoints[i] = TimeSeriesPoint{\n\t\t\tName:      point.Name,\n\t\t\tTimestamp: point.Timestamp.AsTime(),\n\t\t\tValue:     point.Value,\n\t\t}\n\t}\n\treturn &protocol.AddTimeSeriesPointsResponse{}, p.database.AddTimeSeriesPoints(ctx, points)\n}\n\nfunc (p *ProxyServer) GetTimeSeriesPoints(ctx context.Context, request *protocol.GetTimeSeriesPointsRequest) (*protocol.GetTimeSeriesPointsResponse, error) {\n\tresp, err := p.database.GetTimeSeriesPoints(ctx, request.GetName(), request.GetBegin().AsTime(), request.GetEnd().AsTime(), time.Duration(request.GetDuration()))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpoints := make([]*protocol.TimeSeriesPoint, len(resp))\n\tfor i, point := range resp {\n\t\tpoints[i] = &protocol.TimeSeriesPoint{\n\t\t\tName:      point.Name,\n\t\t\tTimestamp: timestamppb.New(point.Timestamp),\n\t\t\tValue:     point.Value,\n\t\t}\n\t}\n\treturn &protocol.GetTimeSeriesPointsResponse{Points: points}, nil\n}\n\ntype ProxyClient struct {\n\tprotocol.CacheStoreClient\n}\n\nfunc (p ProxyClient) Ping() error {\n\t_, err := p.CacheStoreClient.Ping(context.Background(), &protocol.PingRequest{})\n\treturn err\n}\n\nfunc (p ProxyClient) Close() error {\n\treturn nil\n}\n\nfunc (p ProxyClient) Init() error {\n\treturn errors.MethodNotAllowedf(\"init is not allowed in proxy client\")\n}\n\nfunc (p ProxyClient) Scan(_ func(string) error) error {\n\treturn errors.MethodNotAllowedf(\"scan is not allowed in proxy client\")\n}\n\nfunc (p ProxyClient) Purge() error {\n\treturn errors.MethodNotAllowedf(\"purge is not allowed in proxy client\")\n}\n\nfunc (p ProxyClient) Set(ctx context.Context, values ...Value) error {\n\tpbValues := make([]*protocol.Value, len(values))\n\tfor i, value := range values {\n\t\tpbValues[i] = &protocol.Value{\n\t\t\tName:  value.name,\n\t\t\tValue: value.value,\n\t\t}\n\t}\n\t_, err := p.CacheStoreClient.Set(ctx, &protocol.SetRequest{\n\t\tValues: pbValues,\n\t})\n\treturn err\n}\n\nfunc (p ProxyClient) Get(ctx context.Context, name string) *ReturnValue {\n\tresp, err := p.CacheStoreClient.Get(ctx, &protocol.GetRequest{\n\t\tName: name,\n\t})\n\tif err != nil {\n\t\treturn &ReturnValue{err: err, exists: false}\n\t}\n\tif resp.Value == nil {\n\t\treturn &ReturnValue{value: \"\", exists: false}\n\t}\n\treturn &ReturnValue{value: resp.GetValue(), err: err, exists: true}\n}\n\nfunc (p ProxyClient) Delete(ctx context.Context, name string) error {\n\t_, err := p.CacheStoreClient.Delete(ctx, &protocol.DeleteRequest{\n\t\tName: name,\n\t})\n\treturn err\n}\n\nfunc (p ProxyClient) Push(ctx context.Context, name, value string) error {\n\t_, err := p.CacheStoreClient.Push(ctx, &protocol.PushRequest{\n\t\tName:  name,\n\t\tValue: value,\n\t})\n\treturn err\n}\n\nfunc (p ProxyClient) Pop(ctx context.Context, name string) (string, error) {\n\tresp, err := p.CacheStoreClient.Pop(ctx, &protocol.PopRequest{\n\t\tName: name,\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif resp.Value == nil {\n\t\treturn \"\", io.EOF\n\t}\n\treturn resp.GetValue(), nil\n}\n\nfunc (p ProxyClient) Remain(ctx context.Context, name string) (int64, error) {\n\tresp, err := p.CacheStoreClient.Remain(ctx, &protocol.RemainRequest{\n\t\tName: name,\n\t})\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn resp.Count, nil\n}\n\nfunc (p ProxyClient) AddScores(ctx context.Context, collection, subset string, documents []Score) error {\n\tscores := make([]*protocol.Score, len(documents))\n\tfor i, doc := range documents {\n\t\tscores[i] = &protocol.Score{\n\t\t\tId:         doc.Id,\n\t\t\tScore:      doc.Score,\n\t\t\tIsHidden:   doc.IsHidden,\n\t\t\tCategories: doc.Categories,\n\t\t\tTimestamp:  timestamppb.New(doc.Timestamp),\n\t\t}\n\t}\n\t_, err := p.CacheStoreClient.AddScores(ctx, &protocol.AddScoresRequest{\n\t\tCollection: collection,\n\t\tSubset:     subset,\n\t\tDocuments:  scores,\n\t})\n\treturn err\n}\n\nfunc (p ProxyClient) SearchScores(ctx context.Context, collection, subset string, query []string, begin, end int) ([]Score, error) {\n\tresp, err := p.CacheStoreClient.SearchScores(ctx, &protocol.SearchScoresRequest{\n\t\tCollection: collection,\n\t\tSubset:     subset,\n\t\tQuery:      query,\n\t\tBegin:      int32(begin),\n\t\tEnd:        int32(end),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tscores := make([]Score, len(resp.Documents))\n\tfor i, score := range resp.Documents {\n\t\tscores[i] = Score{\n\t\t\tId:         score.Id,\n\t\t\tScore:      score.Score,\n\t\t\tIsHidden:   score.IsHidden,\n\t\t\tCategories: score.Categories,\n\t\t\tTimestamp:  score.Timestamp.AsTime(),\n\t\t}\n\t}\n\treturn scores, nil\n}\n\nfunc (p ProxyClient) DeleteScores(ctx context.Context, collection []string, condition ScoreCondition) error {\n\tif err := condition.Check(); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tvar before *timestamppb.Timestamp\n\tif condition.Before != nil {\n\t\tbefore = timestamppb.New(*condition.Before)\n\t}\n\t_, err := p.CacheStoreClient.DeleteScores(ctx, &protocol.DeleteScoresRequest{\n\t\tCollection: collection,\n\t\tCondition: &protocol.ScoreCondition{\n\t\t\tSubset: condition.Subset,\n\t\t\tId:     condition.Id,\n\t\t\tBefore: before,\n\t\t},\n\t})\n\treturn err\n}\n\nfunc (p ProxyClient) UpdateScores(ctx context.Context, collection []string, subset *string, id string, patch ScorePatch) error {\n\t_, err := p.CacheStoreClient.UpdateScores(ctx, &protocol.UpdateScoresRequest{\n\t\tCollection: collection,\n\t\tSubset:     subset,\n\t\tId:         id,\n\t\tPatch: &protocol.ScorePatch{\n\t\t\tScore:      patch.Score,\n\t\t\tIsHidden:   patch.IsHidden,\n\t\t\tCategories: patch.Categories,\n\t\t},\n\t})\n\treturn err\n}\n\nfunc (p ProxyClient) ScanScores(ctx context.Context, callback func(collection string, id string, subset string, timestamp time.Time) error) error {\n\tstream, err := p.CacheStoreClient.ScanScores(ctx, &protocol.ScanScoresRequest{})\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor {\n\t\t// check for context cancellation\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t}\n\t\t// receive the next message\n\t\tresp, err := stream.Recv()\n\t\tif err == io.EOF {\n\t\t\treturn nil\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := callback(resp.Collection, resp.Id, resp.Subset, resp.Timestamp.AsTime()); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n\nfunc (p ProxyClient) AddTimeSeriesPoints(ctx context.Context, points []TimeSeriesPoint) error {\n\tpbPoints := make([]*protocol.TimeSeriesPoint, len(points))\n\tfor i, point := range points {\n\t\tpbPoints[i] = &protocol.TimeSeriesPoint{\n\t\t\tName:      point.Name,\n\t\t\tTimestamp: timestamppb.New(point.Timestamp),\n\t\t\tValue:     point.Value,\n\t\t}\n\t}\n\t_, err := p.CacheStoreClient.AddTimeSeriesPoints(ctx, &protocol.AddTimeSeriesPointsRequest{\n\t\tPoints: pbPoints,\n\t})\n\treturn err\n}\n\nfunc (p ProxyClient) GetTimeSeriesPoints(ctx context.Context, name string, begin, end time.Time, duration time.Duration) ([]TimeSeriesPoint, error) {\n\tresp, err := p.CacheStoreClient.GetTimeSeriesPoints(ctx, &protocol.GetTimeSeriesPointsRequest{\n\t\tName:     name,\n\t\tBegin:    timestamppb.New(begin),\n\t\tEnd:      timestamppb.New(end),\n\t\tDuration: int64(duration),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpoints := make([]TimeSeriesPoint, len(resp.Points))\n\tfor i, point := range resp.Points {\n\t\tpoints[i] = TimeSeriesPoint{\n\t\t\tName:      point.Name,\n\t\t\tTimestamp: point.Timestamp.AsTime(),\n\t\t\tValue:     point.Value,\n\t\t}\n\t}\n\treturn points, nil\n}\n\nfunc NewProxyClient(conn *grpc.ClientConn) *ProxyClient {\n\treturn &ProxyClient{\n\t\tCacheStoreClient: protocol.NewCacheStoreClient(conn),\n\t}\n}\n"
  },
  {
    "path": "storage/cache/proxy_test.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cache\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/suite\"\n\t\"google.golang.org/grpc\"\n)\n\ntype ProxyTestSuite struct {\n\tbaseTestSuite\n\tsqlite     Database\n\tserver     *ProxyServer\n\tclientConn *grpc.ClientConn\n}\n\nfunc (suite *ProxyTestSuite) SetupSuite() {\n\t// create database\n\tvar err error\n\tpath := fmt.Sprintf(\"sqlite://%s/sqlite.db\", suite.T().TempDir())\n\tsuite.sqlite, err = Open(path, \"gorse_\")\n\tsuite.NoError(err)\n\t// create schema\n\terr = suite.sqlite.Init()\n\tsuite.NoError(err)\n\t// start server\n\tlis, err := net.Listen(\"tcp\", \"localhost:0\")\n\tsuite.NoError(err)\n\tsuite.server = NewProxyServer(suite.sqlite)\n\tgo func() {\n\t\terr = suite.server.Serve(lis)\n\t\tsuite.NoError(err)\n\t}()\n\t// create proxy client\n\tsuite.clientConn, err = grpc.Dial(lis.Addr().String(), grpc.WithInsecure())\n\tsuite.NoError(err)\n\tsuite.Database = NewProxyClient(suite.clientConn)\n}\n\nfunc (suite *ProxyTestSuite) TearDownSuite() {\n\tsuite.server.Stop()\n\tsuite.NoError(suite.clientConn.Close())\n\tsuite.NoError(suite.sqlite.Close())\n}\n\nfunc (suite *ProxyTestSuite) SetupTest() {\n\terr := suite.sqlite.Ping()\n\tsuite.NoError(err)\n\terr = suite.sqlite.Purge()\n\tsuite.NoError(err)\n}\n\nfunc (suite *ProxyTestSuite) TearDownTest() {\n\terr := suite.sqlite.Purge()\n\tsuite.NoError(err)\n}\n\nfunc (suite *ProxyTestSuite) TestInit() {\n\tsuite.T().Skip()\n}\n\nfunc (suite *ProxyTestSuite) TestPurge() {\n\tsuite.T().Skip()\n}\n\nfunc (suite *ProxyTestSuite) TestScan() {\n\tsuite.T().Skip()\n}\n\nfunc TestProxy(t *testing.T) {\n\tsuite.Run(t, new(ProxyTestSuite))\n}\n"
  },
  {
    "path": "storage/cache/redis.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cache\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/common/util\"\n\t\"github.com/gorse-io/gorse/storage\"\n\t\"github.com/juju/errors\"\n\t\"github.com/redis/go-redis/extra/redisotel/v9\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/samber/lo\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.8.0\"\n\t\"go.uber.org/zap\"\n)\n\nfunc init() {\n\tRegister([]string{storage.RedisPrefix, storage.RedissPrefix}, func(path, tablePrefix string, opts ...storage.Option) (Database, error) {\n\t\topt, err := redis.ParseURL(path)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\topt.Protocol = 2\n\t\tdatabase := new(Redis)\n\t\tdatabase.client = redis.NewClient(opt)\n\t\tdatabase.TablePrefix = storage.TablePrefix(tablePrefix)\n\t\tdatabase.maxSearchResults = storage.NewOptions(opts...).MaxSearchResults\n\t\tif err = redisotel.InstrumentTracing(database.client, redisotel.WithAttributes(semconv.DBSystemRedis)); err != nil {\n\t\t\tlog.Logger().Error(\"failed to add tracing for redis\", zap.Error(err))\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\treturn database, nil\n\t})\n\tRegister([]string{storage.RedisClusterPrefix, storage.RedissClusterPrefix}, func(path, tablePrefix string, opts ...storage.Option) (Database, error) {\n\t\tvar newURL string\n\t\tif strings.HasPrefix(path, storage.RedisClusterPrefix) {\n\t\t\tnewURL = strings.Replace(path, storage.RedisClusterPrefix, storage.RedisPrefix, 1)\n\t\t} else if strings.HasPrefix(path, storage.RedissClusterPrefix) {\n\t\t\tnewURL = strings.Replace(path, storage.RedissClusterPrefix, storage.RedissPrefix, 1)\n\t\t}\n\t\topt, err := redis.ParseClusterURL(newURL)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\topt.Protocol = 2\n\t\tdatabase := new(Redis)\n\t\tdatabase.client = redis.NewClusterClient(opt)\n\t\tdatabase.TablePrefix = storage.TablePrefix(tablePrefix)\n\t\tdatabase.maxSearchResults = storage.NewOptions(opts...).MaxSearchResults\n\t\tif err = redisotel.InstrumentTracing(database.client, redisotel.WithAttributes(semconv.DBSystemRedis)); err != nil {\n\t\t\tlog.Logger().Error(\"failed to add tracing for redis\", zap.Error(err))\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\treturn database, nil\n\t})\n}\n\n// Redis cache storage.\ntype Redis struct {\n\tstorage.TablePrefix\n\tclient           redis.UniversalClient\n\tmaxSearchResults int\n}\n\n// Close redis connection.\nfunc (r *Redis) Close() error {\n\treturn r.client.Close()\n}\n\nfunc (r *Redis) Ping() error {\n\treturn r.client.Ping(context.Background()).Err()\n}\n\n// Init nothing.\nfunc (r *Redis) Init() error {\n\t// list indices\n\tindices, err := r.client.FT_List(context.Background()).Result()\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t// create index\n\tif !lo.Contains(indices, r.DocumentTable()) {\n\t\t_, err = r.client.FTCreate(context.TODO(), r.DocumentTable(),\n\t\t\t&redis.FTCreateOptions{\n\t\t\t\tOnHash: true,\n\t\t\t\tPrefix: []any{r.DocumentTable() + \":\"},\n\t\t\t},\n\t\t\t&redis.FieldSchema{FieldName: \"collection\", FieldType: redis.SearchFieldTypeTag},\n\t\t\t&redis.FieldSchema{FieldName: \"subset\", FieldType: redis.SearchFieldTypeTag},\n\t\t\t&redis.FieldSchema{FieldName: \"id\", FieldType: redis.SearchFieldTypeTag},\n\t\t\t&redis.FieldSchema{FieldName: \"score\", FieldType: redis.SearchFieldTypeNumeric},\n\t\t\t&redis.FieldSchema{FieldName: \"is_hidden\", FieldType: redis.SearchFieldTypeNumeric},\n\t\t\t&redis.FieldSchema{FieldName: \"categories\", FieldType: redis.SearchFieldTypeTag, Separator: \";\"},\n\t\t\t&redis.FieldSchema{FieldName: \"timestamp\", FieldType: redis.SearchFieldTypeNumeric},\n\t\t).Result()\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (r *Redis) Scan(work func(string) error) error {\n\tctx := context.Background()\n\tif clusterClient, isCluster := r.client.(*redis.ClusterClient); isCluster {\n\t\treturn clusterClient.ForEachMaster(ctx, func(ctx context.Context, client *redis.Client) error {\n\t\t\treturn r.scan(ctx, client, work)\n\t\t})\n\t} else {\n\t\treturn r.scan(ctx, r.client, work)\n\t}\n}\n\nfunc (r *Redis) scan(ctx context.Context, client redis.UniversalClient, work func(string) error) error {\n\tvar (\n\t\tresult []string\n\t\tcursor uint64\n\t\terr    error\n\t)\n\tfor {\n\t\tresult, cursor, err = client.Scan(ctx, cursor, string(r.TablePrefix)+\"*\", 0).Result()\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\tfor _, key := range result {\n\t\t\tif err = work(key[len(r.TablePrefix):]); err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t}\n\t\tif cursor == 0 {\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\nfunc (r *Redis) Purge() error {\n\tctx := context.Background()\n\tif clusterClient, isCluster := r.client.(*redis.ClusterClient); isCluster {\n\t\treturn clusterClient.ForEachMaster(ctx, func(ctx context.Context, client *redis.Client) error {\n\t\t\treturn r.purge(ctx, client, isCluster)\n\t\t})\n\t} else {\n\t\treturn r.purge(ctx, r.client, isCluster)\n\t}\n}\n\nfunc (r *Redis) purge(ctx context.Context, client redis.UniversalClient, isCluster bool) error {\n\tvar (\n\t\tresult []string\n\t\tcursor uint64\n\t\terr    error\n\t)\n\tfor {\n\t\tresult, cursor, err = client.Scan(ctx, cursor, string(r.TablePrefix)+\"*\", 0).Result()\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\tif len(result) > 0 {\n\t\t\tif isCluster {\n\t\t\t\tp := client.Pipeline()\n\t\t\t\tfor _, key := range result {\n\t\t\t\t\tif err = p.Del(ctx, key).Err(); err != nil {\n\t\t\t\t\t\treturn errors.Trace(err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif _, err = p.Exec(ctx); err != nil {\n\t\t\t\t\treturn errors.Trace(err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err = client.Del(ctx, result...).Err(); err != nil {\n\t\t\t\t\treturn errors.Trace(err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif cursor == 0 {\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\nfunc (r *Redis) Set(ctx context.Context, values ...Value) error {\n\tp := r.client.Pipeline()\n\tfor _, v := range values {\n\t\tif err := p.Set(ctx, r.Key(v.name), v.value, 0).Err(); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\t_, err := p.Exec(ctx)\n\treturn errors.Trace(err)\n}\n\n// Get returns a value from Redis.\nfunc (r *Redis) Get(ctx context.Context, key string) *ReturnValue {\n\tval, err := r.client.Get(ctx, r.Key(key)).Result()\n\tif err != nil {\n\t\tif err == redis.Nil {\n\t\t\treturn &ReturnValue{value: \"\", exists: false}\n\t\t}\n\t\treturn &ReturnValue{err: err, exists: false}\n\t}\n\treturn &ReturnValue{value: val, exists: true}\n}\n\n// Delete object from Redis.\nfunc (r *Redis) Delete(ctx context.Context, key string) error {\n\treturn r.client.Del(ctx, r.Key(key)).Err()\n}\n\nfunc (r *Redis) Push(ctx context.Context, name string, message string) error {\n\t_, err := r.client.ZAdd(ctx, r.Key(name), redis.Z{Member: message, Score: float64(time.Now().UnixNano())}).Result()\n\treturn err\n}\n\nfunc (r *Redis) Pop(ctx context.Context, name string) (string, error) {\n\tz, err := r.client.ZPopMin(ctx, r.Key(name), 1).Result()\n\tif err != nil {\n\t\treturn \"\", errors.Trace(err)\n\t}\n\tif len(z) == 0 {\n\t\treturn \"\", io.EOF\n\t}\n\treturn z[0].Member.(string), nil\n}\n\nfunc (r *Redis) Remain(ctx context.Context, name string) (int64, error) {\n\treturn r.client.ZCard(ctx, r.Key(name)).Result()\n}\n\nfunc (r *Redis) documentKey(collection, subset, value string) string {\n\treturn r.DocumentTable() + \":\" + collection + \":\" + subset + \":\" + value\n}\n\nfunc (r *Redis) AddScores(ctx context.Context, collection, subset string, documents []Score) error {\n\tp := r.client.Pipeline()\n\tfor _, document := range documents {\n\t\tp.HSet(ctx, r.documentKey(collection, subset, document.Id),\n\t\t\t\"collection\", collection,\n\t\t\t\"subset\", subset,\n\t\t\t\"id\", document.Id,\n\t\t\t\"score\", document.Score,\n\t\t\t\"is_hidden\", document.IsHidden,\n\t\t\t\"categories\", encodeCategories(document.Categories),\n\t\t\t\"timestamp\", document.Timestamp.UnixMicro())\n\t}\n\t_, err := p.Exec(ctx)\n\treturn errors.Trace(err)\n}\n\nfunc (r *Redis) SearchScores(ctx context.Context, collection, subset string, query []string, begin, end int) ([]Score, error) {\n\tvar builder strings.Builder\n\tfmt.Fprintf(&builder, \"@collection:{ %s } @is_hidden:[0 0]\", escape(collection))\n\tif subset != \"\" {\n\t\tfmt.Fprintf(&builder, \" @subset:{ %s }\", escape(subset))\n\t}\n\tfor _, q := range query {\n\t\tfmt.Fprintf(&builder, \" @categories:{ %s }\", escape(encodeCategory(q)))\n\t}\n\toptions := &redis.FTSearchOptions{\n\t\tSortBy:      []redis.FTSearchSortBy{{FieldName: \"score\", Desc: true}},\n\t\tLimitOffset: begin,\n\t}\n\tif end == -1 {\n\t\toptions.Limit = 10000\n\t} else {\n\t\toptions.Limit = end - begin\n\t}\n\tresult, err := r.client.FTSearchWithArgs(ctx, r.DocumentTable(), builder.String(), options).Result()\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\tdocuments := make([]Score, 0, len(result.Docs))\n\tfor _, doc := range result.Docs {\n\t\tvar document Score\n\t\tdocument.Id = doc.Fields[\"id\"]\n\t\tscore, err := strconv.ParseFloat(doc.Fields[\"score\"], 64)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tdocument.Score = score\n\t\tisHidden, err := strconv.ParseInt(doc.Fields[\"is_hidden\"], 10, 64)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tdocument.IsHidden = isHidden != 0\n\t\tcategories, err := decodeCategories(doc.Fields[\"categories\"])\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tdocument.Categories = categories\n\t\ttimestamp, err := strconv.ParseInt(doc.Fields[\"timestamp\"], 10, 64)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tdocument.Timestamp = time.UnixMicro(timestamp).In(time.UTC)\n\t\tdocuments = append(documents, document)\n\t}\n\treturn documents, nil\n}\n\nfunc (r *Redis) UpdateScores(ctx context.Context, collections []string, subset *string, id string, patch ScorePatch) error {\n\tif len(collections) == 0 {\n\t\treturn nil\n\t}\n\tif patch.Score == nil && patch.IsHidden == nil && patch.Categories == nil {\n\t\treturn nil\n\t}\n\tvar builder strings.Builder\n\tfmt.Fprintf(&builder, \"@collection:{ %s }\", escape(strings.Join(collections, \" | \")))\n\tfmt.Fprintf(&builder, \" @id:{ %s }\", escape(id))\n\tif subset != nil {\n\t\tfmt.Fprintf(&builder, \" @subset:{ %s }\", escape(*subset))\n\t}\n\tlimit := r.maxSearchResults\n\tif limit <= 0 {\n\t\tlimit = 10000\n\t}\n\n\t// Two-phase update:\n\t// 1) collect matched document IDs with pagination,\n\t// 2) mutate documents by key.\n\t// This avoids pagination drift when patch.Score changes the sort order.\n\tkeys := make([]string, 0)\n\tkeySet := make(map[string]struct{})\n\toffset := 0\n\tfor {\n\t\t// search documents\n\t\tresult, err := r.client.FTSearchWithArgs(ctx, r.DocumentTable(), builder.String(), &redis.FTSearchOptions{\n\t\t\tSortBy:      []redis.FTSearchSortBy{{FieldName: \"score\", Desc: true}},\n\t\t\tLimitOffset: offset,\n\t\t\tLimit:       limit,\n\t\t}).Result()\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\tif len(result.Docs) == 0 {\n\t\t\tbreak\n\t\t}\n\t\tnewKeys := 0\n\t\tfor _, doc := range result.Docs {\n\t\t\tif _, exists := keySet[doc.ID]; !exists {\n\t\t\t\tkeySet[doc.ID] = struct{}{}\n\t\t\t\tkeys = append(keys, doc.ID)\n\t\t\t\tnewKeys++\n\t\t\t}\n\t\t}\n\t\toffset += len(result.Docs)\n\t\t// Stop when:\n\t\t// 1) the last page is shorter than the limit (common Redis behavior), or\n\t\t// 2) no new keys are discovered (defensive for engines with non-standard total/offset semantics).\n\t\tif len(result.Docs) < limit || newKeys == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tvalues := make([]any, 0)\n\tif patch.Score != nil {\n\t\tvalues = append(values, \"score\", *patch.Score)\n\t}\n\tif patch.IsHidden != nil {\n\t\tvalues = append(values, \"is_hidden\", *patch.IsHidden)\n\t}\n\tif patch.Categories != nil {\n\t\tvalues = append(values, \"categories\", encodeCategories(patch.Categories))\n\t}\n\tfor _, key := range keys {\n\t\tif err := r.client.Watch(ctx, func(tx *redis.Tx) error {\n\t\t\tif exist, err := tx.Exists(ctx, key).Result(); err != nil {\n\t\t\t\treturn err\n\t\t\t} else if exist == 0 {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn tx.HSet(ctx, key, values...).Err()\n\t\t}, key); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (r *Redis) DeleteScores(ctx context.Context, collections []string, condition ScoreCondition) error {\n\tif err := condition.Check(); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tvar builder strings.Builder\n\tfmt.Fprintf(&builder, \"@collection:{ %s }\", escape(strings.Join(collections, \" | \")))\n\tif condition.Subset != nil {\n\t\tfmt.Fprintf(&builder, \" @subset:{ %s }\", escape(*condition.Subset))\n\t}\n\tif condition.Id != nil {\n\t\tfmt.Fprintf(&builder, \" @id:{ %s }\", escape(*condition.Id))\n\t}\n\tif condition.Before != nil {\n\t\tfmt.Fprintf(&builder, \" @timestamp:[-inf (%d]\", condition.Before.UnixMicro())\n\t}\n\tfor {\n\t\t// search documents\n\t\tresult, err := r.client.FTSearchWithArgs(ctx, r.DocumentTable(), builder.String(), &redis.FTSearchOptions{\n\t\t\tSortBy:      []redis.FTSearchSortBy{{FieldName: \"score\", Desc: true}},\n\t\t\tLimitOffset: 0,\n\t\t\tLimit:       10000,\n\t\t}).Result()\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\t// delete documents\n\t\tp := r.client.Pipeline()\n\t\tfor _, doc := range result.Docs {\n\t\t\tp.Del(ctx, doc.ID)\n\t\t}\n\t\t_, err = p.Exec(ctx)\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\t// break if no more documents\n\t\tif result.Total == len(result.Docs) {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (r *Redis) ScanScores(ctx context.Context, callback func(collection string, id string, subset string, timestamp time.Time) error) error {\n\tif clusterClient, isCluster := r.client.(*redis.ClusterClient); isCluster {\n\t\treturn clusterClient.ForEachMaster(ctx, func(ctx context.Context, client *redis.Client) error {\n\t\t\treturn r.scanScores(ctx, client, callback)\n\t\t})\n\t} else {\n\t\treturn r.scanScores(ctx, r.client, callback)\n\t}\n}\n\nfunc (r *Redis) scanScores(ctx context.Context, client redis.UniversalClient, callback func(collection string, id string, subset string, timestamp time.Time) error) error {\n\tvar (\n\t\tresult []string\n\t\tcursor uint64\n\t\terr    error\n\t)\n\tfor {\n\t\tresult, cursor, err = client.Scan(ctx, cursor, r.DocumentTable()+\"*\", 0).Result()\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\tfor _, key := range result {\n\t\t\tvar row map[string]string\n\t\t\trow, err = client.HGetAll(ctx, key).Result()\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t\tvar usec int64\n\t\t\tusec, err = util.ParseInt[int64](row[\"timestamp\"])\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t\tif err = callback(row[\"collection\"], row[\"id\"], row[\"subset\"], time.UnixMicro(usec).In(time.UTC)); err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t}\n\t\tif cursor == 0 {\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\nfunc (r *Redis) AddTimeSeriesPoints(ctx context.Context, points []TimeSeriesPoint) error {\n\tp := r.client.Pipeline()\n\topt := &redis.TSOptions{DuplicatePolicy: \"LAST\"}\n\tfor _, point := range points {\n\t\tif err := p.TSAddWithArgs(ctx, r.PointsTable()+\":\"+point.Name, point.Timestamp.UnixMilli(), point.Value, opt).Err(); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\t_, err := p.Exec(ctx)\n\treturn errors.Trace(err)\n}\n\nfunc (r *Redis) GetTimeSeriesPoints(ctx context.Context, name string, begin, end time.Time, duration time.Duration) ([]TimeSeriesPoint, error) {\n\tresult, err := r.client.TSRangeWithArgs(ctx, r.PointsTable()+\":\"+name, int(begin.UnixMilli()), int(end.UnixMilli()),\n\t\t&redis.TSRangeOptions{Aggregator: redis.Last, BucketDuration: int(duration / time.Millisecond)}).Result()\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\tpoints := make([]TimeSeriesPoint, 0, len(result))\n\tfor _, doc := range result {\n\t\tvar point TimeSeriesPoint\n\t\tpoint.Name = name\n\t\tpoint.Value = doc.Value\n\t\tpoint.Timestamp = time.UnixMilli(doc.Timestamp).UTC()\n\t\tpoints = append(points, point)\n\t}\n\treturn points, nil\n}\n\nfunc encodeCategory(category string) string {\n\treturn base64.RawStdEncoding.EncodeToString([]byte(\"_\" + category))\n}\n\nfunc decodeCategory(s string) (string, error) {\n\tb, err := base64.RawStdEncoding.DecodeString(s)\n\tif err != nil {\n\t\treturn \"\", errors.Trace(err)\n\t}\n\treturn string(b[1:]), nil\n}\n\nfunc encodeCategories(categories []string) string {\n\tvar builder strings.Builder\n\tfor i, category := range categories {\n\t\tif i > 0 {\n\t\t\tbuilder.WriteByte(';')\n\t\t}\n\t\tbuilder.WriteString(encodeCategory(category))\n\t}\n\treturn builder.String()\n}\n\nfunc decodeCategories(s string) ([]string, error) {\n\tif s == \"\" {\n\t\treturn []string{}, nil\n\t}\n\tvar categories []string\n\tfor _, category := range strings.Split(s, \";\") {\n\t\tcategory, err := decodeCategory(category)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tcategories = append(categories, category)\n\t}\n\treturn categories, nil\n}\n\n// escape -:.\nfunc escape(s string) string {\n\tr := strings.NewReplacer(\n\t\t\"-\", \"\\\\-\",\n\t\t\":\", \"\\\\:\",\n\t\t\".\", \"\\\\.\",\n\t\t\"/\", \"\\\\/\",\n\t\t\"+\", \"\\\\+\",\n\t)\n\treturn r.Replace(s)\n}\n"
  },
  {
    "path": "storage/cache/redis_test.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cache\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\nvar (\n\tredisDSN string\n)\n\nfunc init() {\n\t// get environment variables\n\tenv := func(key, defaultValue string) string {\n\t\tif value := os.Getenv(key); value != \"\" {\n\t\t\treturn value\n\t\t}\n\t\treturn defaultValue\n\t}\n\tredisDSN = env(\"REDIS_URI\", \"redis://127.0.0.1:6379/\")\n}\n\ntype RedisTestSuite struct {\n\tbaseTestSuite\n}\n\nfunc (suite *RedisTestSuite) SetupSuite() {\n\tvar err error\n\tsuite.Database, err = Open(redisDSN, \"gorse_\")\n\tsuite.NoError(err)\n\t// flush db\n\tredisClient, ok := suite.Database.(*Redis)\n\tsuite.True(ok)\n\tif clusterClient, ok := redisClient.client.(*redis.ClusterClient); ok {\n\t\terr = clusterClient.ForEachMaster(suite.T().Context(), func(ctx context.Context, client *redis.Client) error {\n\t\t\treturn client.FlushDB(ctx).Err()\n\t\t})\n\t\tsuite.NoError(err)\n\t} else {\n\t\terr = redisClient.client.FlushDB(suite.T().Context()).Err()\n\t\tsuite.NoError(err)\n\t}\n\t// create schema\n\terr = suite.Database.Init()\n\tsuite.NoError(err)\n}\n\nfunc (suite *RedisTestSuite) TestEscapeCharacters() {\n\tts := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)\n\tctx := suite.T().Context()\n\tfor _, c := range []string{\"-\", \":\", \".\", \"/\"} {\n\t\tsuite.Run(c, func() {\n\t\t\tcollection := fmt.Sprintf(\"a%s1\", c)\n\t\t\tsubset := fmt.Sprintf(\"b%s2\", c)\n\t\t\tid := fmt.Sprintf(\"c%s3\", c)\n\t\t\terr := suite.AddScores(ctx, collection, subset, []Score{{\n\t\t\t\tId:         id,\n\t\t\t\tScore:      math.MaxFloat64,\n\t\t\t\tCategories: []string{\"a\", \"b\"},\n\t\t\t\tTimestamp:  ts,\n\t\t\t}})\n\t\t\tsuite.NoError(err)\n\t\t\tdocuments, err := suite.SearchScores(ctx, collection, subset, []string{\"b\"}, 0, -1)\n\t\t\tsuite.NoError(err)\n\t\t\tsuite.Equal([]Score{{Id: id, Score: math.MaxFloat64, Categories: []string{\"a\", \"b\"}, Timestamp: ts}}, documents)\n\n\t\t\terr = suite.UpdateScores(ctx, []string{collection}, nil, id, ScorePatch{Score: new(float64(1))})\n\t\t\tsuite.NoError(err)\n\t\t\tdocuments, err = suite.SearchScores(ctx, collection, subset, []string{\"b\"}, 0, -1)\n\t\t\tsuite.NoError(err)\n\t\t\tsuite.Equal([]Score{{Id: id, Score: 1, Categories: []string{\"a\", \"b\"}, Timestamp: ts}}, documents)\n\n\t\t\terr = suite.DeleteScores(ctx, []string{collection}, ScoreCondition{\n\t\t\t\tSubset: new(subset),\n\t\t\t\tId:     new(id),\n\t\t\t})\n\t\t\tsuite.NoError(err)\n\t\t\tdocuments, err = suite.SearchScores(ctx, collection, subset, []string{\"b\"}, 0, -1)\n\t\t\tsuite.NoError(err)\n\t\t\tsuite.Empty(documents)\n\t\t})\n\t}\n}\n\nfunc (suite *RedisTestSuite) TestUpdateScoresWithPagination() {\n\tctx := suite.T().Context()\n\tdb, ok := suite.Database.(*Redis)\n\tsuite.True(ok)\n\tlimit := db.maxSearchResults\n\tdb.maxSearchResults = 2\n\tdefer func() {\n\t\tdb.maxSearchResults = limit\n\t}()\n\n\tfor i := 0; i < 5; i++ {\n\t\tsubset := fmt.Sprintf(\"subset-%d\", i)\n\t\terr := suite.AddScores(ctx, \"collection-a\", subset, []Score{{\n\t\t\tId:         \"shared-item\",\n\t\t\tScore:      float64(i),\n\t\t\tCategories: []string{\"old\"},\n\t\t\tTimestamp:  time.Now().UTC(),\n\t\t}})\n\t\tsuite.NoError(err)\n\t}\n\n\terr := suite.UpdateScores(ctx, []string{\"collection-a\"}, nil, \"shared-item\", ScorePatch{\n\t\tCategories: []string{\"new\"},\n\t})\n\tsuite.NoError(err)\n\n\tfor i := 0; i < 5; i++ {\n\t\tsubset := fmt.Sprintf(\"subset-%d\", i)\n\t\tdocs, err := suite.SearchScores(ctx, \"collection-a\", subset, []string{\"new\"}, 0, -1)\n\t\tsuite.NoError(err)\n\t\tsuite.Require().Len(docs, 1)\n\t\tsuite.Equal(\"shared-item\", docs[0].Id)\n\t}\n}\n\nfunc (suite *RedisTestSuite) TestUpdateScoresWithPaginationAndScorePatch() {\n\tctx := suite.T().Context()\n\tdb, ok := suite.Database.(*Redis)\n\tsuite.True(ok)\n\tlimit := db.maxSearchResults\n\tdb.maxSearchResults = 1\n\tdefer func() {\n\t\tdb.maxSearchResults = limit\n\t}()\n\n\tinitialScores := []float64{3, 2, 1}\n\tfor i, score := range initialScores {\n\t\tsubset := fmt.Sprintf(\"score-subset-%d\", i)\n\t\terr := suite.AddScores(ctx, \"collection-b\", subset, []Score{{\n\t\t\tId:         \"shared-item\",\n\t\t\tScore:      score,\n\t\t\tCategories: []string{\"score-old\"},\n\t\t\tTimestamp:  time.Now().UTC(),\n\t\t}})\n\t\tsuite.NoError(err)\n\t}\n\n\ttargetScore := float64(0)\n\terr := suite.UpdateScores(ctx, []string{\"collection-b\"}, nil, \"shared-item\", ScorePatch{\n\t\tScore: &targetScore,\n\t})\n\tsuite.NoError(err)\n\n\tfor i := range initialScores {\n\t\tsubset := fmt.Sprintf(\"score-subset-%d\", i)\n\t\tdocs, err := suite.SearchScores(ctx, \"collection-b\", subset, nil, 0, -1)\n\t\tsuite.NoError(err)\n\t\tsuite.Require().Len(docs, 1)\n\t\tsuite.Equal(targetScore, docs[0].Score)\n\t}\n}\n\nfunc (suite *RedisTestSuite) TestUpdateScoresWithPaginationAndTiedScores() {\n\tctx := suite.T().Context()\n\tdb, ok := suite.Database.(*Redis)\n\tsuite.True(ok)\n\tlimit := db.maxSearchResults\n\tdb.maxSearchResults = 2\n\tdefer func() {\n\t\tdb.maxSearchResults = limit\n\t}()\n\n\tfor i := 0; i < 5; i++ {\n\t\tsubset := fmt.Sprintf(\"tie-subset-%d\", i)\n\t\terr := suite.AddScores(ctx, \"collection-c\", subset, []Score{{\n\t\t\tId:         \"shared-item\",\n\t\t\tScore:      1,\n\t\t\tCategories: []string{\"tie-old\"},\n\t\t\tTimestamp:  time.Now().UTC(),\n\t\t}})\n\t\tsuite.NoError(err)\n\t}\n\n\terr := suite.UpdateScores(ctx, []string{\"collection-c\"}, nil, \"shared-item\", ScorePatch{\n\t\tCategories: []string{\"tie-new\"},\n\t})\n\tsuite.NoError(err)\n\n\tfor i := 0; i < 5; i++ {\n\t\tsubset := fmt.Sprintf(\"tie-subset-%d\", i)\n\t\tdocs, err := suite.SearchScores(ctx, \"collection-c\", subset, []string{\"tie-new\"}, 0, -1)\n\t\tsuite.NoError(err)\n\t\tsuite.Require().Len(docs, 1)\n\t\tsuite.Equal(\"shared-item\", docs[0].Id)\n\t}\n}\n\nfunc TestRedis(t *testing.T) {\n\tsuite.Run(t, new(RedisTestSuite))\n}\n\nfunc TestEncodeDecodeCategories(t *testing.T) {\n\tencoded := encodeCategories([]string{\"z\", \"h\"})\n\tdecoded, err := decodeCategories(encoded)\n\tassert.NoError(t, err)\n\tassert.Equal(t, []string{\"z\", \"h\"}, decoded)\n\n\tencoded = encodeCategories(nil)\n\tdecoded, err = decodeCategories(encoded)\n\tassert.NoError(t, err)\n\tassert.Equal(t, []string{}, decoded)\n}\n\nfunc BenchmarkRedis(b *testing.B) {\n\tlog.CloseLogger()\n\t// open db\n\tdatabase, err := Open(redisDSN, \"gorse_\")\n\tassert.NoError(b, err)\n\t// flush db\n\terr = database.(*Redis).client.FlushDB(b.Context()).Err()\n\tassert.NoError(b, err)\n\t// create schema\n\terr = database.Init()\n\tassert.NoError(b, err)\n\t// benchmark\n\tbenchmark(b, database)\n}\n"
  },
  {
    "path": "storage/cache/sql.go",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cache\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/XSAM/otelsql\"\n\t\"github.com/araddon/dateparse\"\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/go-sql-driver/mysql\"\n\t\"github.com/gorse-io/gorse/storage\"\n\t\"github.com/juju/errors\"\n\t\"github.com/lib/pq\"\n\t\"github.com/samber/lo\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.8.0\"\n\tgormmysql \"gorm.io/driver/mysql\"\n\t\"gorm.io/driver/postgres\"\n\t\"gorm.io/driver/sqlite\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n)\n\nfunc init() {\n\tRegister([]string{storage.PostgresPrefix, storage.PostgreSQLPrefix}, func(path, tablePrefix string, opts ...storage.Option) (Database, error) {\n\t\tdatabase := new(SQLDatabase)\n\t\tdatabase.driver = Postgres\n\t\tdatabase.TablePrefix = storage.TablePrefix(tablePrefix)\n\t\toption := storage.NewOptions(opts...)\n\t\tvar err error\n\t\tif database.client, err = otelsql.Open(\"postgres\", path,\n\t\t\totelsql.WithAttributes(semconv.DBSystemPostgreSQL),\n\t\t\totelsql.WithSpanOptions(otelsql.SpanOptions{DisableErrSkip: true}),\n\t\t); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tstorage.ApplySQLPool(database.client, option)\n\t\tdatabase.gormDB, err = gorm.Open(postgres.New(postgres.Config{Conn: database.client}), storage.NewGORMConfig(tablePrefix))\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\treturn database, nil\n\t})\n\tRegister([]string{storage.MySQLPrefix}, func(path, tablePrefix string, opts ...storage.Option) (Database, error) {\n\t\tname := path[len(storage.MySQLPrefix):]\n\t\toption := storage.NewOptions(opts...)\n\t\t// probe isolation variable name\n\t\tisolationVarName, err := storage.ProbeMySQLIsolationVariableName(name)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\t// append parameters\n\t\tif name, err = storage.AppendMySQLParams(name, map[string]string{\n\t\t\tisolationVarName: fmt.Sprintf(\"'%s'\", option.IsolationLevel),\n\t\t\t\"parseTime\":      \"true\",\n\t\t}); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\t// connect to database\n\t\tdatabase := new(SQLDatabase)\n\t\tdatabase.driver = MySQL\n\t\tdatabase.TablePrefix = storage.TablePrefix(tablePrefix)\n\t\tif database.client, err = otelsql.Open(\"mysql\", name,\n\t\t\totelsql.WithAttributes(semconv.DBSystemMySQL),\n\t\t\totelsql.WithSpanOptions(otelsql.SpanOptions{DisableErrSkip: true}),\n\t\t); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tstorage.ApplySQLPool(database.client, option)\n\t\tdatabase.gormDB, err = gorm.Open(gormmysql.New(gormmysql.Config{Conn: database.client}), storage.NewGORMConfig(tablePrefix))\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\treturn database, nil\n\t})\n\tRegister([]string{storage.SQLitePrefix}, func(path, tablePrefix string, opts ...storage.Option) (Database, error) {\n\t\tdataSourceName := path[len(storage.SQLitePrefix):]\n\t\t// append parameters\n\t\tvar err error\n\t\tif dataSourceName, err = storage.AppendURLParams(dataSourceName, []lo.Tuple2[string, string]{\n\t\t\t{\"_pragma\", \"busy_timeout(10000)\"},\n\t\t\t{\"_pragma\", \"journal_mode(wal)\"},\n\t\t}); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\t// connect to database\n\t\tdatabase := new(SQLDatabase)\n\t\tdatabase.driver = SQLite\n\t\tdatabase.TablePrefix = storage.TablePrefix(tablePrefix)\n\t\tif database.client, err = otelsql.Open(\"sqlite\", dataSourceName,\n\t\t\totelsql.WithAttributes(semconv.DBSystemSqlite),\n\t\t\totelsql.WithSpanOptions(otelsql.SpanOptions{DisableErrSkip: true}),\n\t\t); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tdatabase.gormDB, err = gorm.Open(sqlite.Dialector{Conn: database.client}, storage.NewGORMConfig(tablePrefix))\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\treturn database, nil\n\t})\n}\n\ntype SQLDriver int\n\nconst (\n\tMySQL SQLDriver = iota\n\tPostgres\n\tSQLite\n)\n\ntype SQLValue struct {\n\tName  string `gorm:\"type:varchar(256);primaryKey\"`\n\tValue string `gorm:\"type:varchar(256);not null\"`\n}\n\ntype Message struct {\n\tName      string `gorm:\"primaryKey;index:timestamp\"`\n\tValue     string `gorm:\"primaryKey\"`\n\tTimestamp int64  `gorm:\"index:timestamp\"`\n}\n\ntype PostgresDocument struct {\n\tCollection string `gorm:\"primaryKey\"`\n\tSubset     string `gorm:\"primaryKey\"`\n\tId         string `gorm:\"primaryKey\"`\n\tIsHidden   bool\n\tCategories pq.StringArray `gorm:\"type:text[]\"`\n\tScore      float64\n\tTimestamp  time.Time\n}\n\ntype SQLDocument struct {\n\tCollection string `gorm:\"primaryKey\"`\n\tSubset     string `gorm:\"primaryKey\"`\n\tId         string `gorm:\"primaryKey\"`\n\tIsHidden   bool\n\tCategories []string `gorm:\"type:text;serializer:json\"`\n\tScore      float64\n\tTimestamp  time.Time\n}\n\ntype SQLDatabase struct {\n\tstorage.TablePrefix\n\tgormDB *gorm.DB\n\tclient *sql.DB\n\tdriver SQLDriver\n}\n\nfunc (db *SQLDatabase) Close() error {\n\treturn db.client.Close()\n}\n\nfunc (db *SQLDatabase) Ping() error {\n\treturn db.client.Ping()\n}\n\nfunc (db *SQLDatabase) Init() error {\n\terr := db.gormDB.AutoMigrate(&SQLValue{}, &Message{}, &TimeSeriesPoint{})\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tswitch db.driver {\n\tcase Postgres:\n\t\terr = db.gormDB.AutoMigrate(&PostgresDocument{})\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\t// create extension btree_gin\n\t\terr = db.gormDB.Exec(\"CREATE EXTENSION IF NOT EXISTS btree_gin\").Error\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\t// create index\n\t\terr = db.gormDB.Exec(fmt.Sprintf(\"CREATE INDEX IF NOT EXISTS idx_collection_subset_categories ON %s USING GIN (collection, subset, categories)\", db.DocumentTable())).Error\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\terr = db.gormDB.Exec(fmt.Sprintf(\"CREATE INDEX IF NOT EXISTS idx_collection_id ON %s (collection, id)\", db.DocumentTable())).Error\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\tcase MySQL:\n\t\terr = db.gormDB.AutoMigrate(&SQLDocument{})\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\t// create index\n\t\terr = db.gormDB.Exec(fmt.Sprintf(\"ALTER TABLE %s ADD INDEX idx_collection_subset_categories (collection, subset, (CAST(categories AS CHAR(255) ARRAY)))\", db.DocumentTable())).Error\n\t\tif err != nil {\n\t\t\tif mysqlErr, ok := err.(*mysql.MySQLError); ok && mysqlErr.Number == 1061 {\n\t\t\t\t// ignore duplicate index error\n\t\t\t} else {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t}\n\t\terr = db.gormDB.Exec(fmt.Sprintf(\"ALTER TABLE %s ADD INDEX idx_collection_id (collection, id)\", db.DocumentTable())).Error\n\t\tif err != nil {\n\t\t\tif mysqlErr, ok := err.(*mysql.MySQLError); ok && mysqlErr.Number == 1061 {\n\t\t\t\t// ignore duplicate index error\n\t\t\t\terr = nil\n\t\t\t} else {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t}\n\tcase SQLite:\n\t\terr = db.gormDB.AutoMigrate(&SQLDocument{})\n\t}\n\treturn errors.Trace(err)\n}\n\nfunc (db *SQLDatabase) Scan(work func(string) error) error {\n\tvar (\n\t\tvaluerRows *sql.Rows\n\t\terr        error\n\t)\n\n\t// scan values\n\tvaluerRows, err = db.gormDB.Table(db.ValuesTable()).Select(\"name\").Rows()\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tdefer valuerRows.Close()\n\tfor valuerRows.Next() {\n\t\tvar key string\n\t\tif err = valuerRows.Scan(&key); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\tif err = work(key); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (db *SQLDatabase) Purge() error {\n\ttables := []any{SQLValue{}, Message{}, SQLDocument{}}\n\tfor _, table := range tables {\n\t\terr := db.gormDB.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&table).Error\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (db *SQLDatabase) Set(ctx context.Context, values ...Value) error {\n\tif len(values) == 0 {\n\t\treturn nil\n\t}\n\tvalueSet := mapset.NewSet[string]()\n\trows := make([]SQLValue, 0, len(values))\n\tfor _, value := range values {\n\t\tif !valueSet.Contains(value.name) {\n\t\t\trows = append(rows, SQLValue{\n\t\t\t\tName:  value.name,\n\t\t\t\tValue: value.value,\n\t\t\t})\n\t\t\tvalueSet.Add(value.name)\n\t\t}\n\t}\n\terr := db.gormDB.WithContext(ctx).Clauses(clause.OnConflict{\n\t\tColumns:   []clause.Column{{Name: \"name\"}},\n\t\tDoUpdates: clause.AssignmentColumns([]string{\"value\"}),\n\t}).Create(rows).Error\n\treturn errors.Trace(err)\n}\n\nfunc (db *SQLDatabase) Get(ctx context.Context, name string) *ReturnValue {\n\trs, err := db.gormDB.WithContext(ctx).Table(db.ValuesTable()).Where(\"name = ?\", name).Select(\"value\").Rows()\n\tif err != nil {\n\t\treturn &ReturnValue{err: errors.Trace(err), exists: false}\n\t}\n\tdefer rs.Close()\n\tif rs.Next() {\n\t\tvar value string\n\t\terr := rs.Scan(&value)\n\t\tif err != nil {\n\t\t\treturn &ReturnValue{err: errors.Trace(err), exists: false}\n\t\t}\n\t\treturn &ReturnValue{value: value, exists: true}\n\t}\n\treturn &ReturnValue{value: \"\", exists: false}\n}\n\nfunc (db *SQLDatabase) Delete(ctx context.Context, name string) error {\n\terr := db.gormDB.WithContext(ctx).Delete(&SQLValue{Name: name}).Error\n\treturn errors.Trace(err)\n}\n\nfunc (db *SQLDatabase) Push(ctx context.Context, name, value string) error {\n\treturn db.gormDB.WithContext(ctx).Clauses(clause.OnConflict{\n\t\tColumns:   []clause.Column{{Name: \"name\"}, {Name: \"value\"}},\n\t\tDoUpdates: clause.AssignmentColumns([]string{\"timestamp\"}),\n\t}).Create(&Message{\n\t\tName:      name,\n\t\tValue:     value,\n\t\tTimestamp: time.Now().UnixNano(),\n\t}).Error\n}\n\nfunc (db *SQLDatabase) Pop(ctx context.Context, name string) (string, error) {\n\tvar message Message\n\terr := db.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {\n\t\tif err := db.gormDB.Order(\"timestamp\").First(&message, \"name = ?\", name).Error; err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := db.gormDB.Delete(&message).Error; err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n\tif err == gorm.ErrRecordNotFound {\n\t\treturn \"\", io.EOF\n\t}\n\treturn message.Value, err\n}\n\nfunc (db *SQLDatabase) Remain(ctx context.Context, name string) (count int64, err error) {\n\terr = db.gormDB.WithContext(ctx).Model(&Message{}).Where(\"name = ?\", name).Count(&count).Error\n\treturn\n}\n\nfunc (db *SQLDatabase) AddScores(ctx context.Context, collection, subset string, documents []Score) error {\n\tvar rows any\n\tswitch db.driver {\n\tcase Postgres:\n\t\trows = lo.Map(documents, func(document Score, _ int) PostgresDocument {\n\t\t\treturn PostgresDocument{\n\t\t\t\tCollection: collection,\n\t\t\t\tSubset:     subset,\n\t\t\t\tId:         document.Id,\n\t\t\t\tScore:      document.Score,\n\t\t\t\tIsHidden:   document.IsHidden,\n\t\t\t\tCategories: document.Categories,\n\t\t\t\tTimestamp:  document.Timestamp,\n\t\t\t}\n\t\t})\n\tcase SQLite, MySQL:\n\t\trows = lo.Map(documents, func(document Score, _ int) SQLDocument {\n\t\t\treturn SQLDocument{\n\t\t\t\tCollection: collection,\n\t\t\t\tSubset:     subset,\n\t\t\t\tId:         document.Id,\n\t\t\t\tScore:      document.Score,\n\t\t\t\tIsHidden:   document.IsHidden,\n\t\t\t\tCategories: document.Categories,\n\t\t\t\tTimestamp:  document.Timestamp,\n\t\t\t}\n\t\t})\n\t}\n\terr := db.gormDB.WithContext(ctx).Table(db.DocumentTable()).Clauses(clause.OnConflict{\n\t\tColumns:   []clause.Column{{Name: \"collection\"}, {Name: \"subset\"}, {Name: \"id\"}},\n\t\tDoUpdates: clause.AssignmentColumns([]string{\"score\", \"categories\", \"timestamp\"}),\n\t}).Create(rows).Error\n\treturn errors.Trace(err)\n}\n\nfunc (db *SQLDatabase) SearchScores(ctx context.Context, collection, subset string, query []string, begin, end int) ([]Score, error) {\n\ttx := db.gormDB.WithContext(ctx).\n\t\tModel(&PostgresDocument{}).\n\t\tSelect(\"id, score, categories, timestamp\").\n\t\tWhere(\"collection = ? and subset = ? and is_hidden = false\", collection, subset)\n\tif len(query) > 0 {\n\t\tswitch db.driver {\n\t\tcase Postgres:\n\t\t\ttx.Where(\"categories @> ?\", pq.StringArray(query))\n\t\tcase SQLite, MySQL:\n\t\t\tq, err := json.Marshal(query)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Trace(err)\n\t\t\t}\n\t\t\ttx.Where(\"JSON_CONTAINS(categories,?)\", string(q))\n\t\t}\n\t}\n\ttx.Order(\"score desc\").Offset(begin)\n\tif end != -1 {\n\t\ttx.Limit(end - begin)\n\t} else {\n\t\ttx.Limit(math.MaxInt64)\n\t}\n\trows, err := tx.Rows()\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\tdocuments := make([]Score, 0, 10)\n\tfor rows.Next() {\n\t\tswitch db.driver {\n\t\tcase Postgres:\n\t\t\tvar document PostgresDocument\n\t\t\tif err = rows.Scan(&document.Id, &document.Score, &document.Categories, &document.Timestamp); err != nil {\n\t\t\t\treturn nil, errors.Trace(err)\n\t\t\t}\n\t\t\tdocuments = append(documents, Score{\n\t\t\t\tId:         document.Id,\n\t\t\t\tScore:      document.Score,\n\t\t\t\tCategories: document.Categories,\n\t\t\t\tTimestamp:  document.Timestamp,\n\t\t\t})\n\t\tcase SQLite, MySQL:\n\t\t\tvar document Score\n\t\t\tif err = db.gormDB.ScanRows(rows, &document); err != nil {\n\t\t\t\treturn nil, errors.Trace(err)\n\t\t\t}\n\t\t\tdocument.Timestamp = document.Timestamp.In(time.UTC)\n\t\t\tdocuments = append(documents, document)\n\t\t}\n\t}\n\treturn documents, nil\n}\n\nfunc (db *SQLDatabase) UpdateScores(ctx context.Context, collections []string, subset *string, id string, patch ScorePatch) error {\n\tif len(collections) == 0 {\n\t\treturn nil\n\t}\n\tif patch.Score == nil && patch.IsHidden == nil && patch.Categories == nil {\n\t\treturn nil\n\t}\n\ttx := db.gormDB.WithContext(ctx).\n\t\tModel(&PostgresDocument{}).\n\t\tWhere(\"collection in (?) and id = ?\", collections, id)\n\tif subset != nil {\n\t\ttx.Where(\"subset = ?\", subset)\n\t}\n\tif patch.Score != nil {\n\t\ttx.Update(\"score\", *patch.Score)\n\t}\n\tif patch.IsHidden != nil {\n\t\ttx.Update(\"is_hidden\", *patch.IsHidden)\n\t}\n\tif patch.Categories != nil {\n\t\tswitch db.driver {\n\t\tcase Postgres:\n\t\t\ttx.Update(\"categories\", pq.StringArray(patch.Categories))\n\t\tcase SQLite, MySQL:\n\t\t\tq, err := json.Marshal(patch.Categories)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t\ttx.Update(\"categories\", string(q))\n\t\t}\n\t}\n\treturn tx.Error\n}\n\nfunc (db *SQLDatabase) DeleteScores(ctx context.Context, collections []string, condition ScoreCondition) error {\n\tif err := condition.Check(); err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tvar builder strings.Builder\n\tbuilder.WriteString(\"collection in (?)\")\n\tvar args []any\n\targs = append(args, collections)\n\tif condition.Subset != nil {\n\t\tbuilder.WriteString(\" and subset = ?\")\n\t\targs = append(args, *condition.Subset)\n\t}\n\tif condition.Id != nil {\n\t\tbuilder.WriteString(\" and id = ?\")\n\t\targs = append(args, *condition.Id)\n\t}\n\tif condition.Before != nil {\n\t\tbuilder.WriteString(\" and timestamp < ?\")\n\t\tif db.driver == MySQL {\n\t\t\t// In MySQL, we need to truncate the time to milliseconds because MySQL will round the time to milliseconds.\n\t\t\targs = append(args, condition.Before.Truncate(time.Millisecond))\n\t\t} else {\n\t\t\targs = append(args, *condition.Before)\n\t\t}\n\t}\n\treturn db.gormDB.WithContext(ctx).Delete(&SQLDocument{}, append([]any{builder.String()}, args...)...).Error\n}\n\nfunc (db *SQLDatabase) ScanScores(ctx context.Context, callback func(collection, id, subset string, timestamp time.Time) error) error {\n\trows, err := db.gormDB.WithContext(ctx).Table(db.DocumentTable()).Select(\"collection, id, subset, timestamp\").Rows()\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar collection, id, subset string\n\t\tvar timestamp time.Time\n\t\tif err = rows.Scan(&collection, &id, &subset, &timestamp); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\tif err = callback(collection, id, subset, timestamp); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (db *SQLDatabase) AddTimeSeriesPoints(ctx context.Context, points []TimeSeriesPoint) error {\n\tif len(points) == 0 {\n\t\treturn nil\n\t}\n\treturn db.gormDB.WithContext(ctx).Table(db.PointsTable()).Clauses(clause.OnConflict{\n\t\tColumns:   []clause.Column{{Name: \"name\"}, {Name: \"timestamp\"}},\n\t\tDoUpdates: clause.AssignmentColumns([]string{\"value\"}),\n\t}).Create(points).Error\n}\n\nfunc (db *SQLDatabase) GetTimeSeriesPoints(ctx context.Context, name string, begin, end time.Time, duration time.Duration) ([]TimeSeriesPoint, error) {\n\tvar points []TimeSeriesPoint\n\tswitch db.driver {\n\tcase Postgres:\n\t\tif err := db.gormDB.WithContext(ctx).\n\t\t\tRaw(fmt.Sprintf(\"SELECT name, bucket_timestamp AS timestamp, value FROM (\"+\n\t\t\t\t\"SELECT *, TO_TIMESTAMP((EXTRACT(epoch FROM timestamp)::int / ?) * ?) AS bucket_timestamp,\"+\n\t\t\t\t\"ROW_NUMBER() OVER (PARTITION BY (EXTRACT(epoch FROM timestamp)::int / ?) ORDER BY timestamp DESC) AS rn \"+\n\t\t\t\t\"FROM %s WHERE name = ? and timestamp >= ? and timestamp <= ?) AS t WHERE rn = 1\",\n\t\t\t\tdb.PointsTable()), int(duration.Seconds()), int(duration.Seconds()), int(duration.Seconds()), name, begin, end).\n\t\t\tScan(&points).Error; err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\tcase MySQL:\n\t\tif err := db.gormDB.WithContext(ctx).\n\t\t\tRaw(fmt.Sprintf(\"SELECT name, bucket_timestamp AS timestamp, value FROM(\"+\n\t\t\t\t\"SELECT *, FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(timestamp) / ?) * ?) AS bucket_timestamp,\"+\n\t\t\t\t\"ROW_NUMBER() OVER (PARTITION BY FLOOR(UNIX_TIMESTAMP(timestamp) / ?) ORDER BY timestamp DESC) AS rn \"+\n\t\t\t\t\"FROM %s WHERE name = ? and timestamp >= ? and timestamp <= ?) AS t WHERE rn = 1;\",\n\t\t\t\tdb.PointsTable()), int(duration.Seconds()), int(duration.Seconds()), int(duration.Seconds()), name, begin, end).\n\t\t\tScan(&points).Error; err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\tcase SQLite:\n\t\trows, err := db.gormDB.WithContext(ctx).\n\t\t\tRaw(fmt.Sprintf(\"select name, bucket_timestamp as timestamp, value from (\"+\n\t\t\t\t\"select *, datetime(strftime('%%s', substr(timestamp, 0, 20)) / ? * ?, 'unixepoch') as bucket_timestamp,\"+\n\t\t\t\t\"row_number() over (partition by strftime('%%s', substr(timestamp, 0, 20)) / ? order by timestamp desc) as rn \"+\n\t\t\t\t\"from %s where name = ? and timestamp >= ? and timestamp <= ?) where rn = 1\",\n\t\t\t\tdb.PointsTable()), int(duration.Seconds()), int(duration.Seconds()), int(duration.Seconds()), name, begin, end).\n\t\t\tRows()\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tdefer rows.Close()\n\t\tfor rows.Next() {\n\t\t\tvar point TimeSeriesPoint\n\t\t\tvar timestamp string\n\t\t\tif err := rows.Scan(&point.Name, &timestamp, &point.Value); err != nil {\n\t\t\t\treturn nil, errors.Trace(err)\n\t\t\t}\n\t\t\tpoint.Timestamp, err = dateparse.ParseAny(timestamp)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Trace(err)\n\t\t\t}\n\t\t\tpoints = append(points, point)\n\t\t}\n\t}\n\treturn points, nil\n}\n"
  },
  {
    "path": "storage/cache/sql_test.go",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cache\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/storage\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\nvar (\n\tmySqlDSN    string\n\tpostgresDSN string\n)\n\nfunc init() {\n\t// get environment variables\n\tenv := func(key, defaultValue string) string {\n\t\tif value := os.Getenv(key); value != \"\" {\n\t\t\treturn value\n\t\t}\n\t\treturn defaultValue\n\t}\n\tmySqlDSN = env(\"MYSQL_URI\", \"mysql://root:password@tcp(127.0.0.1:3306)/\")\n\tpostgresDSN = env(\"POSTGRES_URI\", \"postgres://gorse:gorse_pass@127.0.0.1/\")\n}\n\ntype PostgresTestSuite struct {\n\tbaseTestSuite\n}\n\nfunc (suite *PostgresTestSuite) SetupSuite() {\n\tvar err error\n\t// create database\n\tdatabaseComm, err := sql.Open(\"postgres\", postgresDSN+\"?sslmode=disable\")\n\tsuite.NoError(err)\n\tconst dbName = \"gorse_cache_test\"\n\t_, err = databaseComm.Exec(\"DROP DATABASE IF EXISTS \" + dbName)\n\tsuite.NoError(err)\n\t_, err = databaseComm.Exec(\"CREATE DATABASE \" + dbName)\n\tsuite.NoError(err)\n\terr = databaseComm.Close()\n\tsuite.NoError(err)\n\t// connect database\n\tsuite.Database, err = Open(postgresDSN+strings.ToLower(dbName)+\"?sslmode=disable\", \"gorse_\")\n\tsuite.NoError(err)\n\t// create schema\n\terr = suite.Database.Init()\n\tsuite.NoError(err)\n}\n\nfunc TestPostgres(t *testing.T) {\n\tsuite.Run(t, new(PostgresTestSuite))\n}\n\ntype MySQLTestSuite struct {\n\tbaseTestSuite\n}\n\nfunc (suite *MySQLTestSuite) SetupSuite() {\n\t// create database\n\tdatabaseComm, err := sql.Open(\"mysql\", mySqlDSN[len(storage.MySQLPrefix):])\n\tsuite.NoError(err)\n\tconst dbName = \"gorse_cache_test\"\n\t_, err = databaseComm.Exec(\"DROP DATABASE IF EXISTS \" + dbName)\n\tsuite.NoError(err)\n\t_, err = databaseComm.Exec(\"CREATE DATABASE \" + dbName)\n\tsuite.NoError(err)\n\terr = databaseComm.Close()\n\tsuite.NoError(err)\n\t// connect database\n\tsuite.Database, err = Open(mySqlDSN+dbName, \"gorse_\")\n\tsuite.NoError(err)\n\t// create schema\n\terr = suite.Database.Init()\n\tsuite.NoError(err)\n}\n\nfunc (suite *MySQLTestSuite) TestInit() {\n\terr := suite.Database.Init()\n\tsuite.NoError(err)\n\n\tname, err := storage.ProbeMySQLIsolationVariableName(mySqlDSN[len(storage.MySQLPrefix):])\n\tsuite.NoError(err)\n\tconnection := suite.Database.(*SQLDatabase).client\n\tassertQuery(suite.T(), connection, fmt.Sprintf(\"SELECT @@%s\", name), \"READ-UNCOMMITTED\")\n}\n\nfunc TestMySQL(t *testing.T) {\n\tsuite.Run(t, new(MySQLTestSuite))\n}\n\ntype SQLiteTestSuite struct {\n\tbaseTestSuite\n}\n\nfunc (suite *SQLiteTestSuite) SetupSuite() {\n\tvar err error\n\t// create database\n\tpath := fmt.Sprintf(\"sqlite://%s/sqlite.db\", suite.T().TempDir())\n\tsuite.Database, err = Open(path, \"gorse_\")\n\tsuite.NoError(err)\n\t// create schema\n\terr = suite.Database.Init()\n\tsuite.NoError(err)\n}\n\nfunc (suite *SQLiteTestSuite) TearDownSuite() {\n\tsuite.NoError(suite.Database.Close())\n}\n\nfunc TestSQLite(t *testing.T) {\n\tsuite.Run(t, new(SQLiteTestSuite))\n}\n\nfunc assertQuery(t *testing.T, connection *sql.DB, sql string, expected string) {\n\trows, err := connection.Query(sql)\n\tassert.NoError(t, err)\n\tassert.True(t, rows.Next())\n\tvar result string\n\terr = rows.Scan(&result)\n\tassert.NoError(t, err)\n\tassert.Equal(t, expected, result)\n}\n\nfunc BenchmarkPostgres(b *testing.B) {\n\tlog.CloseLogger()\n\t// create database\n\tdatabaseComm, err := sql.Open(\"postgres\", postgresDSN+\"?sslmode=disable\")\n\tassert.NoError(b, err)\n\tconst dbName = \"gorse_cache_benchmark\"\n\t_, err = databaseComm.Exec(\"DROP DATABASE IF EXISTS \" + dbName)\n\tassert.NoError(b, err)\n\t_, err = databaseComm.Exec(\"CREATE DATABASE \" + dbName)\n\tassert.NoError(b, err)\n\terr = databaseComm.Close()\n\tassert.NoError(b, err)\n\t// connect database\n\tdatabase, err := Open(postgresDSN+strings.ToLower(dbName)+\"?sslmode=disable\", \"gorse_\")\n\tassert.NoError(b, err)\n\t// create schema\n\terr = database.Init()\n\tassert.NoError(b, err)\n\t// benchmark\n\tbenchmark(b, database)\n\t// close database\n\terr = database.Close()\n\tassert.NoError(b, err)\n}\n\nfunc BenchmarkMySQL(b *testing.B) {\n\tlog.CloseLogger()\n\t// create database\n\tdatabaseComm, err := sql.Open(\"mysql\", mySqlDSN[len(storage.MySQLPrefix):])\n\tassert.NoError(b, err)\n\tconst dbName = \"gorse_cache_benchmark\"\n\t_, err = databaseComm.Exec(\"DROP DATABASE IF EXISTS \" + dbName)\n\tassert.NoError(b, err)\n\t_, err = databaseComm.Exec(\"CREATE DATABASE \" + dbName)\n\tassert.NoError(b, err)\n\terr = databaseComm.Close()\n\tassert.NoError(b, err)\n\t// connect database\n\tdatabase, err := Open(mySqlDSN+dbName, \"gorse_\")\n\tassert.NoError(b, err)\n\t// create schema\n\terr = database.Init()\n\tassert.NoError(b, err)\n\t// benchmark\n\tbenchmark(b, database)\n}\n\nfunc BenchmarkSQLite(b *testing.B) {\n\tlog.CloseLogger()\n\t// create database\n\tpath := fmt.Sprintf(\"sqlite://%s/sqlite.db\", b.TempDir())\n\tdatabase, err := Open(path, \"gorse_\")\n\tassert.NoError(b, err)\n\t// create schema\n\terr = database.Init()\n\tassert.NoError(b, err)\n\t// benchmark\n\tbenchmark(b, database)\n}\n"
  },
  {
    "path": "storage/data/database.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage data\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"reflect\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/common/expression\"\n\t\"github.com/gorse-io/gorse/common/jsonutil\"\n\t\"github.com/gorse-io/gorse/storage\"\n\t\"github.com/juju/errors\"\n)\n\nvar (\n\tErrUserNotExist = errors.NotFoundf(\"user\")\n\tErrItemNotExist = errors.NotFoundf(\"item\")\n\tErrNoDatabase   = errors.NotAssignedf(\"database\")\n)\n\n// ValidateLabels checks if labels are valid. Labels are valid if consists of:\n// - []string\t\t\tslice of strings\n// - []float64\t\t\tslice of numbers\n// - map[string]any\t\tmap of strings to valid labels or float64\nfunc ValidateLabels(o any) error {\n\tif o == nil {\n\t\treturn nil\n\t}\n\tswitch labels := o.(type) {\n\tcase []any: // must be []string or []float64\n\t\tif len(labels) == 0 {\n\t\t\treturn nil\n\t\t}\n\t\tswitch labels[0].(type) {\n\t\tcase string:\n\t\t\tfor _, val := range labels {\n\t\t\t\tif _, ok := val.(string); !ok {\n\t\t\t\t\treturn errors.Errorf(\"unsupported labels: %v\", jsonutil.MustMarshal(labels))\n\t\t\t\t}\n\t\t\t}\n\t\tcase json.Number:\n\t\t\tfor _, val := range labels {\n\t\t\t\tif _, ok := val.(json.Number); !ok {\n\t\t\t\t\treturn errors.Errorf(\"unsupported labels: %v\", jsonutil.MustMarshal(labels))\n\t\t\t\t}\n\t\t\t}\n\t\tdefault:\n\t\t\treturn errors.Errorf(\"unsupported labels: %v\", jsonutil.MustMarshal(labels))\n\t\t}\n\t\treturn nil\n\tcase map[string]any:\n\t\tfor _, val := range labels {\n\t\t\tif err := ValidateLabels(val); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\tcase string, json.Number:\n\t\treturn nil\n\tdefault:\n\t\treturn errors.Errorf(\"unsupported type in labels: %v\", reflect.TypeOf(labels))\n\t}\n}\n\n// Item stores meta data about item.\ntype Item struct {\n\tItemId     string    `gorm:\"primaryKey\" mapstructure:\"item_id\"`\n\tIsHidden   bool      `mapstructure:\"is_hidden\"`\n\tCategories []string  `gorm:\"serializer:json\" mapstructure:\"categories\"`\n\tTimestamp  time.Time `gorm:\"column:time_stamp\" mapstructure:\"timestamp\"`\n\tLabels     any       `gorm:\"serializer:json\" mapstructure:\"labels\"`\n\tComment    string    `mapstructure:\"comment\"`\n}\n\n// ItemPatch is the modification on an item.\ntype ItemPatch struct {\n\tIsHidden   *bool\n\tCategories []string\n\tTimestamp  *time.Time\n\tLabels     any\n\tComment    *string\n}\n\n// User stores meta data about user.\ntype User struct {\n\tUserId  string `gorm:\"primaryKey\" mapstructure:\"user_id\"`\n\tLabels  any    `gorm:\"serializer:json\" mapstructure:\"labels\"`\n\tComment string `mapstructure:\"comment\"`\n}\n\n// UserPatch is the modification on a user.\ntype UserPatch struct {\n\tLabels  any\n\tComment *string\n}\n\n// FeedbackKey identifies feedback.\ntype FeedbackKey struct {\n\tFeedbackType string `gorm:\"column:feedback_type\" mapstructure:\"feedback_type\"`\n\tUserId       string `gorm:\"column:user_id\" mapstructure:\"user_id\"`\n\tItemId       string `gorm:\"column:item_id\" mapstructure:\"item_id\"`\n}\n\n// Feedback stores feedback.\ntype Feedback struct {\n\tFeedbackKey `gorm:\"embedded\" mapstructure:\",squash\"`\n\tValue       float64   `gorm:\"column:value\" mapstructure:\"value\"`\n\tTimestamp   time.Time `gorm:\"column:time_stamp\" mapstructure:\"timestamp\"`\n\tUpdated     time.Time `gorm:\"column:updated\" mapstructure:\"updated\"`\n\tComment     string    `gorm:\"column:comment\" mapstructure:\"comment\"`\n}\n\ntype UserFeedback Feedback\n\ntype ItemFeedback Feedback\n\n// SortFeedbacks sorts feedback from latest to oldest.\nfunc SortFeedbacks(feedback []Feedback) {\n\tsort.Sort(feedbackSorter(feedback))\n}\n\ntype feedbackSorter []Feedback\n\nfunc (sorter feedbackSorter) Len() int {\n\treturn len(sorter)\n}\n\nfunc (sorter feedbackSorter) Less(i, j int) bool {\n\treturn sorter[i].Timestamp.After(sorter[j].Timestamp)\n}\n\nfunc (sorter feedbackSorter) Swap(i, j int) {\n\tsorter[i], sorter[j] = sorter[j], sorter[i]\n}\n\ntype ScanOptions struct {\n\tBeginUserId   *string\n\tEndUserId     *string\n\tBeginItemId   *string\n\tEndItemId     *string\n\tBeginTime     *time.Time\n\tEndTime       *time.Time\n\tFeedbackTypes []expression.FeedbackTypeExpression\n\tOrderByItemId bool\n}\n\ntype ScanOption func(options *ScanOptions)\n\n// WithBeginUserId sets the begin user id. The begin user id is included in the result.\nfunc WithBeginUserId(userId string) ScanOption {\n\treturn func(options *ScanOptions) {\n\t\toptions.BeginUserId = &userId\n\t}\n}\n\n// WithEndUserId sets the end user id. The end user id is included in the result.\nfunc WithEndUserId(userId string) ScanOption {\n\treturn func(options *ScanOptions) {\n\t\toptions.EndUserId = &userId\n\t}\n}\n\n// WithBeginItemId sets the beginning item id. The beginning item id is included in the result.\nfunc WithBeginItemId(itemId string) ScanOption {\n\treturn func(options *ScanOptions) {\n\t\toptions.BeginItemId = &itemId\n\t}\n}\n\n// WithEndItemId sets the end item id. The end item id is included in the result.\nfunc WithEndItemId(itemId string) ScanOption {\n\treturn func(options *ScanOptions) {\n\t\toptions.EndItemId = &itemId\n\t}\n}\n\n// WithBeginTime sets the begin time. The begin time is included in the result.\nfunc WithBeginTime(t time.Time) ScanOption {\n\treturn func(options *ScanOptions) {\n\t\toptions.BeginTime = &t\n\t}\n}\n\n// WithEndTime sets the end time. The end time is included in the result.\nfunc WithEndTime(t time.Time) ScanOption {\n\treturn func(options *ScanOptions) {\n\t\toptions.EndTime = &t\n\t}\n}\n\n// WithFeedbackTypes sets the feedback types.\nfunc WithFeedbackTypes(feedbackTypes ...expression.FeedbackTypeExpression) ScanOption {\n\treturn func(options *ScanOptions) {\n\t\toptions.FeedbackTypes = feedbackTypes\n\t}\n}\n\n// WithOrderByItemId sets the order by item id.\nfunc WithOrderByItemId() ScanOption {\n\treturn func(options *ScanOptions) {\n\t\toptions.OrderByItemId = true\n\t}\n}\n\nfunc NewScanOptions(opts ...ScanOption) ScanOptions {\n\toptions := ScanOptions{}\n\tfor _, opt := range opts {\n\t\tif opt != nil {\n\t\t\topt(&options)\n\t\t}\n\t}\n\treturn options\n}\n\ntype Database interface {\n\tInit() error\n\tPing() error\n\tClose() error\n\tOptimize() error\n\tPurge() error\n\tBatchInsertItems(ctx context.Context, items []Item) error\n\tBatchGetItems(ctx context.Context, itemIds []string) ([]Item, error)\n\tDeleteItem(ctx context.Context, itemId string) error\n\tGetItem(ctx context.Context, itemId string) (Item, error)\n\tModifyItem(ctx context.Context, itemId string, patch ItemPatch) error\n\tGetItems(ctx context.Context, cursor string, n int, beginTime *time.Time) (string, []Item, error)\n\tGetLatestItems(ctx context.Context, n int, categories []string) ([]Item, error)\n\tGetItemFeedback(ctx context.Context, itemId string, feedbackTypes ...string) ([]Feedback, error)\n\tBatchInsertUsers(ctx context.Context, users []User) error\n\tDeleteUser(ctx context.Context, userId string) error\n\tGetUser(ctx context.Context, userId string) (User, error)\n\tModifyUser(ctx context.Context, userId string, patch UserPatch) error\n\tGetUsers(ctx context.Context, cursor string, n int) (string, []User, error)\n\tGetUserFeedback(ctx context.Context, userId string, endTime *time.Time, feedbackTypes ...expression.FeedbackTypeExpression) ([]Feedback, error)\n\tGetUserItemFeedback(ctx context.Context, userId, itemId string, feedbackTypes ...string) ([]Feedback, error)\n\tDeleteUserItemFeedback(ctx context.Context, userId, itemId string, feedbackTypes ...string) (int, error)\n\tBatchInsertFeedback(ctx context.Context, feedback []Feedback, insertUser, insertItem, overwrite bool) error\n\tGetFeedback(ctx context.Context, cursor string, n int, beginTime, endTime *time.Time, feedbackTypes ...string) (string, []Feedback, error)\n\tGetUserStream(ctx context.Context, batchSize int) (chan []User, chan error)\n\tGetItemStream(ctx context.Context, batchSize int, timeLimit *time.Time) (chan []Item, chan error)\n\tGetFeedbackStream(ctx context.Context, batchSize int, options ...ScanOption) (chan []Feedback, chan error)\n\tCountUsers(ctx context.Context) (int, error)\n\tCountItems(ctx context.Context) (int, error)\n\tCountFeedback(ctx context.Context) (int, error)\n}\n\n// Creator creates a database instance.\ntype Creator func(path, tablePrefix string, opts ...storage.Option) (Database, error)\n\nvar creators = make(map[string]Creator)\n\n// Register a database creator.\nfunc Register(prefixes []string, creator Creator) {\n\tfor _, p := range prefixes {\n\t\tcreators[p] = creator\n\t}\n}\n\n// Open a connection to a database.\nfunc Open(path, tablePrefix string, opts ...storage.Option) (Database, error) {\n\tfor prefix, creator := range creators {\n\t\tif strings.HasPrefix(path, prefix) {\n\t\t\treturn creator(path, tablePrefix, opts...)\n\t\t}\n\t}\n\treturn nil, errors.Errorf(\"Unknown database: %s\", path)\n}\n"
  },
  {
    "path": "storage/data/database_test.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage data\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/common/expression\"\n\t\"github.com/jaswdr/faker\"\n\t\"github.com/juju/errors\"\n\t\"github.com/samber/lo\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\nvar (\n\tpositiveFeedbackType  = \"positiveFeedbackType\"\n\tpositiveFeedbackType1 = \"positiveFeedbackType1\"\n\tpositiveFeedbackType2 = \"positiveFeedbackType2\"\n\tnegativeFeedbackType  = \"negativeFeedbackType\"\n\tduplicateFeedbackType = \"duplicateFeedbackType\"\n\tdateTime64Zero        = time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC)\n)\n\ntype baseTestSuite struct {\n\tsuite.Suite\n\tDatabase\n}\n\nfunc (suite *baseTestSuite) getUsers(ctx context.Context, batchSize int) []User {\n\tusers := make([]User, 0)\n\tvar err error\n\tvar data []User\n\tcursor := \"\"\n\tfor {\n\t\tcursor, data, err = suite.Database.GetUsers(ctx, cursor, batchSize)\n\t\tsuite.NoError(err)\n\t\tusers = append(users, data...)\n\t\tif cursor == \"\" {\n\t\t\tsuite.LessOrEqual(len(data), batchSize)\n\t\t\treturn users\n\t\t} else {\n\t\t\tsuite.Equal(batchSize, len(data))\n\t\t}\n\t}\n}\n\nfunc (suite *baseTestSuite) getUsersStream(ctx context.Context, batchSize int) []User {\n\tvar users []User\n\tuserChan, errChan := suite.Database.GetUserStream(ctx, batchSize)\n\tfor batchUsers := range userChan {\n\t\tusers = append(users, batchUsers...)\n\t}\n\tsuite.NoError(<-errChan)\n\treturn users\n}\n\nfunc (suite *baseTestSuite) getItems(ctx context.Context, batchSize int) []Item {\n\titems := make([]Item, 0)\n\tvar err error\n\tvar data []Item\n\tcursor := \"\"\n\tfor {\n\t\tcursor, data, err = suite.Database.GetItems(ctx, cursor, batchSize, nil)\n\t\tsuite.NoError(err)\n\t\titems = append(items, data...)\n\t\tif cursor == \"\" {\n\t\t\tsuite.LessOrEqual(len(data), batchSize)\n\t\t\treturn items\n\t\t} else {\n\t\t\tsuite.Equal(batchSize, len(data))\n\t\t}\n\t}\n}\n\nfunc (suite *baseTestSuite) getItemStream(ctx context.Context, batchSize int) []Item {\n\tvar items []Item\n\titemChan, errChan := suite.Database.GetItemStream(ctx, batchSize, nil)\n\tfor batchUsers := range itemChan {\n\t\titems = append(items, batchUsers...)\n\t}\n\tsuite.NoError(<-errChan)\n\treturn items\n}\n\nfunc (suite *baseTestSuite) getFeedback(ctx context.Context, batchSize int, beginTime, endTime *time.Time, feedbackTypes ...string) []Feedback {\n\tfeedback := make([]Feedback, 0)\n\tvar err error\n\tvar data []Feedback\n\tcursor := \"\"\n\tfor {\n\t\tcursor, data, err = suite.Database.GetFeedback(ctx, cursor, batchSize, beginTime, endTime, feedbackTypes...)\n\t\tsuite.NoError(err)\n\t\tfeedback = append(feedback, data...)\n\t\tif cursor == \"\" {\n\t\t\tsuite.LessOrEqual(len(data), batchSize)\n\t\t\treturn feedback\n\t\t} else {\n\t\t\tsuite.Equal(batchSize, len(data))\n\t\t}\n\t}\n}\n\nfunc (suite *baseTestSuite) getFeedbackStream(ctx context.Context, batchSize int, scanOptions ...ScanOption) []Feedback {\n\tvar feedbacks []Feedback\n\tfeedbackChan, errChan := suite.Database.GetFeedbackStream(ctx, batchSize, scanOptions...)\n\tfor batchFeedback := range feedbackChan {\n\t\tfeedbacks = append(feedbacks, batchFeedback...)\n\t}\n\tsuite.NoError(<-errChan)\n\treturn feedbacks\n}\n\nfunc (suite *baseTestSuite) isClickHouse() bool {\n\tif sqlDB, isSQL := suite.Database.(*SQLDatabase); !isSQL {\n\t\treturn false\n\t} else {\n\t\treturn sqlDB.driver == ClickHouse\n\t}\n}\n\nfunc (suite *baseTestSuite) analyzeTables() {\n\tsqlDatabase, ok := suite.Database.(*SQLDatabase)\n\tif ok && sqlDatabase.driver == Postgres {\n\t\tsqlDatabase := suite.Database.(*SQLDatabase)\n\t\terr := sqlDatabase.gormDB.Exec(fmt.Sprintf(\"ANALYZE %s\", sqlDatabase.ItemsTable())).Error\n\t\tsuite.NoError(err)\n\t\terr = sqlDatabase.gormDB.Exec(fmt.Sprintf(\"ANALYZE %s\", sqlDatabase.UsersTable())).Error\n\t\tsuite.NoError(err)\n\t\terr = sqlDatabase.gormDB.Exec(fmt.Sprintf(\"ANALYZE %s\", sqlDatabase.FeedbackTable())).Error\n\t\tsuite.NoError(err)\n\t}\n}\n\nfunc (suite *baseTestSuite) TearDownSuite() {\n\terr := suite.Database.Close()\n\tsuite.NoError(err)\n}\n\nfunc (suite *baseTestSuite) SetupTest() {\n\terr := suite.Database.Ping()\n\tsuite.NoError(err)\n\terr = suite.Database.Purge()\n\tsuite.NoError(err)\n}\n\nfunc (suite *baseTestSuite) TearDownTest() {\n\terr := suite.Database.Purge()\n\tsuite.NoError(err)\n}\n\nfunc (suite *baseTestSuite) TestInit() {\n\terr := suite.Database.Init()\n\tsuite.NoError(err)\n}\n\nfunc (suite *baseTestSuite) TestUsers() {\n\tctx := suite.T().Context()\n\t// Insert users\n\tvar insertedUsers []User\n\tfake := faker.New()\n\tfor i := 9; i >= 0; i-- {\n\t\tinsertedUsers = append(insertedUsers, User{\n\t\t\tUserId: strconv.Itoa(i),\n\t\t\tLabels: map[string]any{\n\t\t\t\t\"color\": fake.Color().ColorName(),\n\t\t\t\t\"company\": lo.Map(lo.Range(3), func(_, _ int) any {\n\t\t\t\t\treturn fake.Genre().Name()\n\t\t\t\t}),\n\t\t\t},\n\t\t\tComment: fmt.Sprintf(\"comment %d\", i),\n\t\t})\n\t}\n\terr := suite.Database.BatchInsertUsers(ctx, insertedUsers)\n\tsuite.NoError(err)\n\t// Count users\n\tsuite.analyzeTables()\n\tcount, err := suite.Database.CountUsers(ctx)\n\tsuite.NoError(err)\n\tsuite.Equal(10, count)\n\t// Get users\n\tusers := suite.getUsers(ctx, 3)\n\tsuite.Equal(10, len(users))\n\tfor i, user := range users {\n\t\tsuite.Equal(insertedUsers[9-i], user)\n\t}\n\t// Get user stream\n\tusersFromStream := suite.getUsersStream(ctx, 3)\n\tsuite.ElementsMatch(insertedUsers, usersFromStream)\n\t// Get this user\n\tuser, err := suite.Database.GetUser(ctx, \"0\")\n\tsuite.NoError(err)\n\tsuite.Equal(\"0\", user.UserId)\n\t// Delete this user\n\terr = suite.Database.DeleteUser(ctx, \"0\")\n\tsuite.NoError(err)\n\t_, err = suite.Database.GetUser(ctx, \"0\")\n\tsuite.True(errors.Is(err, errors.NotFound), err)\n\t// test override\n\terr = suite.Database.BatchInsertUsers(ctx, []User{{UserId: \"1\", Comment: \"override\"}})\n\tsuite.NoError(err)\n\terr = suite.Database.Optimize()\n\tsuite.NoError(err)\n\tuser, err = suite.Database.GetUser(ctx, \"1\")\n\tsuite.NoError(err)\n\tsuite.Equal(\"override\", user.Comment)\n\t// test modify\n\terr = suite.Database.ModifyUser(ctx, \"1\", UserPatch{Comment: new(\"modify\")})\n\tsuite.NoError(err)\n\terr = suite.Database.ModifyUser(ctx, \"1\", UserPatch{Labels: []string{\"a\", \"b\", \"c\"}})\n\tsuite.NoError(err)\n\terr = suite.Database.Optimize()\n\tsuite.NoError(err)\n\tuser, err = suite.Database.GetUser(ctx, \"1\")\n\tsuite.NoError(err)\n\tsuite.Equal(\"modify\", user.Comment)\n\tsuite.Equal([]any{\"a\", \"b\", \"c\"}, user.Labels)\n\n\t// test insert empty\n\terr = suite.Database.BatchInsertUsers(ctx, nil)\n\tsuite.NoError(err)\n\n\t// insert duplicate users\n\terr = suite.Database.BatchInsertUsers(ctx, []User{{UserId: \"1\"}, {UserId: \"1\"}})\n\tsuite.NoError(err)\n}\n\nfunc (suite *baseTestSuite) TestFeedback() {\n\tctx := suite.T().Context()\n\t// users that already exists\n\terr := suite.Database.BatchInsertUsers(ctx, []User{{\"0\", []string{\"a\"}, \"comment\"}})\n\tsuite.NoError(err)\n\t// items that already exists\n\terr = suite.Database.BatchInsertItems(ctx, []Item{{ItemId: \"0\", Labels: []string{\"b\"}, Timestamp: time.Date(1996, 4, 8, 10, 0, 0, 0, time.UTC)}})\n\tsuite.NoError(err)\n\t// insert feedbacks\n\ttimestamp := time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC)\n\tfeedback := []Feedback{\n\t\t{FeedbackKey: FeedbackKey{positiveFeedbackType1, \"0\", \"8\"}, Value: 1, Timestamp: timestamp, Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{positiveFeedbackType1, \"1\", \"6\"}, Value: 1, Timestamp: timestamp, Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{positiveFeedbackType2, \"2\", \"4\"}, Value: 1, Timestamp: timestamp, Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{positiveFeedbackType2, \"3\", \"2\"}, Value: 1, Timestamp: timestamp, Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{positiveFeedbackType2, \"4\", \"0\"}, Value: 1, Timestamp: timestamp, Comment: \"comment\"},\n\t}\n\terr = suite.Database.BatchInsertFeedback(ctx, feedback, true, true, true)\n\tsuite.NoError(err)\n\t// set Updated for comparison\n\tfor i := range feedback {\n\t\tfeedback[i].Updated = feedback[i].Timestamp\n\t}\n\t// other type\n\terr = suite.Database.BatchInsertFeedback(ctx, []Feedback{{FeedbackKey: FeedbackKey{negativeFeedbackType, \"0\", \"2\"}}}, true, true, true)\n\tsuite.NoError(err)\n\terr = suite.Database.BatchInsertFeedback(ctx, []Feedback{{FeedbackKey: FeedbackKey{negativeFeedbackType, \"2\", \"4\"}}}, true, true, true)\n\tsuite.NoError(err)\n\t// future feedback\n\tfutureFeedback := []Feedback{\n\t\t{FeedbackKey: FeedbackKey{duplicateFeedbackType, \"0\", \"0\"}, Value: 0, Timestamp: time.Now().Add(time.Hour), Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{duplicateFeedbackType, \"1\", \"2\"}, Value: 0, Timestamp: time.Now().Add(time.Hour), Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{duplicateFeedbackType, \"2\", \"4\"}, Value: 0, Timestamp: time.Now().Add(time.Hour), Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{duplicateFeedbackType, \"3\", \"6\"}, Value: 0, Timestamp: time.Now().Add(time.Hour), Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{duplicateFeedbackType, \"4\", \"8\"}, Value: 0, Timestamp: time.Now().Add(time.Hour), Comment: \"comment\"},\n\t}\n\terr = suite.Database.BatchInsertFeedback(ctx, futureFeedback, true, true, true)\n\tsuite.NoError(err)\n\t// Count feedback\n\tsuite.analyzeTables()\n\tcount, err := suite.Database.CountFeedback(ctx)\n\tsuite.NoError(err)\n\tsuite.Equal(12, count)\n\t// Get feedback\n\tret := suite.getFeedback(ctx, 3, nil, lo.ToPtr(time.Now()), positiveFeedbackType1, positiveFeedbackType2)\n\tsuite.Equal(feedback, ret)\n\tret = suite.getFeedback(ctx, 2, nil, lo.ToPtr(time.Now()))\n\tsuite.Equal(len(feedback)+2, len(ret))\n\tret = suite.getFeedback(ctx, 2, lo.ToPtr(timestamp.Add(time.Second)), lo.ToPtr(time.Now()))\n\tsuite.Empty(ret)\n\t// Get feedback stream\n\tfeedbackFromStream := suite.getFeedbackStream(ctx, 3,\n\t\tWithEndTime(time.Now()),\n\t\tWithFeedbackTypes(\n\t\t\texpression.MustParseFeedbackTypeExpression(positiveFeedbackType1),\n\t\t\texpression.MustParseFeedbackTypeExpression(positiveFeedbackType2)))\n\tsuite.ElementsMatch(feedback, feedbackFromStream)\n\tfeedbackFromStream = suite.getFeedbackStream(ctx, 3, WithEndTime(time.Now()))\n\tsuite.Equal(len(feedback)+2, len(feedbackFromStream))\n\tfeedbackFromStream = suite.getFeedbackStream(ctx, 3, WithBeginTime(timestamp.Add(time.Second)), WithEndTime(time.Now()))\n\tsuite.Empty(feedbackFromStream)\n\tfeedbackFromStream = suite.getFeedbackStream(ctx, 3,\n\t\tWithBeginUserId(\"1\"),\n\t\tWithEndUserId(\"3\"),\n\t\tWithEndTime(time.Now()),\n\t\tWithFeedbackTypes(\n\t\t\texpression.MustParseFeedbackTypeExpression(positiveFeedbackType1),\n\t\t\texpression.MustParseFeedbackTypeExpression(positiveFeedbackType2)))\n\tsuite.Equal(feedback[1:4], feedbackFromStream)\n\tfeedbackFromStream = suite.getFeedbackStream(ctx, 3,\n\t\tWithBeginItemId(\"2\"),\n\t\tWithEndItemId(\"6\"),\n\t\tWithEndTime(time.Now()),\n\t\tWithFeedbackTypes(\n\t\t\texpression.MustParseFeedbackTypeExpression(positiveFeedbackType1),\n\t\t\texpression.MustParseFeedbackTypeExpression(positiveFeedbackType2)),\n\t\tWithOrderByItemId())\n\tsuite.Equal([]Feedback{feedback[3], feedback[2], feedback[1]}, feedbackFromStream)\n\t// Get items\n\terr = suite.Database.Optimize()\n\tsuite.NoError(err)\n\titems := suite.getItems(ctx, 3)\n\tsuite.Equal(5, len(items))\n\tfor i, item := range items {\n\t\tsuite.Equal(strconv.Itoa(i*2), item.ItemId)\n\t\tif item.ItemId != \"0\" {\n\t\t\tif suite.isClickHouse() {\n\t\t\t\t// ClickHouse returns 1900-01-01 00:00:00 +0000 UTC as zero date.\n\t\t\t\tsuite.Equal(dateTime64Zero, item.Timestamp)\n\t\t\t} else {\n\t\t\t\tsuite.Zero(item.Timestamp)\n\t\t\t}\n\t\t\tsuite.Empty(item.Labels)\n\t\t\tsuite.Empty(item.Comment)\n\t\t}\n\t}\n\t// Get users\n\tusers := suite.getUsers(ctx, 2)\n\tsuite.Equal(5, len(users))\n\tfor i, user := range users {\n\t\tsuite.Equal(strconv.Itoa(i), user.UserId)\n\t\tif user.UserId != \"0\" {\n\t\t\tsuite.Empty(user.Labels)\n\t\t\tsuite.Empty(user.Comment)\n\t\t}\n\t}\n\t// check users that already exists\n\tuser, err := suite.Database.GetUser(ctx, \"0\")\n\tsuite.NoError(err)\n\tsuite.Equal(User{\"0\", []any{\"a\"}, \"comment\"}, user)\n\t// check items that already exists\n\titem, err := suite.Database.GetItem(ctx, \"0\")\n\tsuite.NoError(err)\n\tsuite.Equal(Item{ItemId: \"0\", Labels: []any{\"b\"}, Timestamp: time.Date(1996, 4, 8, 10, 0, 0, 0, time.UTC)}, item)\n\t// Get typed feedback by user\n\tret, err = suite.Database.GetUserFeedback(ctx, \"2\", lo.ToPtr(time.Now()),\n\t\texpression.MustParseFeedbackTypeExpression(positiveFeedbackType1),\n\t\texpression.MustParseFeedbackTypeExpression(positiveFeedbackType2))\n\tsuite.NoError(err)\n\tif suite.Equal(1, len(ret)) {\n\t\tsuite.Equal(\"2\", ret[0].UserId)\n\t\tsuite.Equal(\"4\", ret[0].ItemId)\n\t}\n\t// Get all feedback by user\n\tret, err = suite.Database.GetUserFeedback(ctx, \"2\", lo.ToPtr(time.Now()))\n\tsuite.NoError(err)\n\tsuite.Equal(2, len(ret))\n\t// Get typed feedback by item\n\tret, err = suite.Database.GetItemFeedback(ctx, \"4\", positiveFeedbackType1, positiveFeedbackType2)\n\tsuite.NoError(err)\n\tsuite.Equal(1, len(ret))\n\tsuite.Equal(\"2\", ret[0].UserId)\n\tsuite.Equal(\"4\", ret[0].ItemId)\n\t// Get all feedback by item\n\tret, err = suite.Database.GetItemFeedback(ctx, \"4\")\n\tsuite.NoError(err)\n\tsuite.Equal(2, len(ret))\n\t// test override\n\terr = suite.Database.BatchInsertFeedback(ctx, []Feedback{{\n\t\tFeedbackKey: FeedbackKey{positiveFeedbackType, \"0\", \"8\"},\n\t\tValue:       100,\n\t\tTimestamp:   time.Date(1996, 4, 8, 0, 0, 0, 0, time.UTC),\n\t\tComment:     \"override\",\n\t}}, true, true, true)\n\tsuite.NoError(err)\n\terr = suite.Database.Optimize()\n\tsuite.NoError(err)\n\t// Get feedback by user with value filter\n\tret, err = suite.Database.GetUserFeedback(ctx, \"0\", lo.ToPtr(time.Now()),\n\t\texpression.FeedbackTypeExpression{FeedbackType: positiveFeedbackType, Value: 50, ExprType: expression.Greater})\n\tsuite.NoError(err)\n\tsuite.Equal(1, len(ret))\n\tsuite.Equal(float64(100), ret[0].Value)\n\tret, err = suite.Database.GetUserFeedback(ctx, \"0\", lo.ToPtr(time.Now()), expression.MustParseFeedbackTypeExpression(positiveFeedbackType))\n\tsuite.NoError(err)\n\tsuite.Equal(1, len(ret))\n\tsuite.Equal(float64(100), ret[0].Value)\n\tsuite.Equal(time.Date(1996, 4, 8, 0, 0, 0, 0, time.UTC), ret[0].Timestamp)\n\tsuite.Equal(\"override\", ret[0].Comment)\n\t// test not overwrite\n\terr = suite.Database.BatchInsertFeedback(ctx, []Feedback{{\n\t\tFeedbackKey: FeedbackKey{positiveFeedbackType, \"0\", \"8\"},\n\t\tValue:       80,\n\t\tTimestamp:   time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC),\n\t\tComment:     \"not_override\",\n\t}}, true, true, false)\n\tsuite.NoError(err)\n\terr = suite.Database.Optimize()\n\tsuite.NoError(err)\n\tret, err = suite.Database.GetUserFeedback(ctx, \"0\", lo.ToPtr(time.Now()), expression.MustParseFeedbackTypeExpression(positiveFeedbackType))\n\tsuite.NoError(err)\n\tsuite.Equal(1, len(ret))\n\tsuite.Equal(float64(180), ret[0].Value)\n\tsuite.Equal(time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC), ret[0].Timestamp)\n\tsuite.Equal(\"not_override\", ret[0].Comment)\n\n\t// insert no feedback\n\terr = suite.Database.BatchInsertFeedback(ctx, nil, true, true, true)\n\tsuite.NoError(err)\n\n\t// not insert users or items\n\terr = suite.Database.BatchInsertFeedback(ctx, []Feedback{\n\t\t{FeedbackKey: FeedbackKey{\"a\", \"100\", \"200\"}},\n\t\t{FeedbackKey: FeedbackKey{\"a\", \"0\", \"200\"}},\n\t\t{FeedbackKey: FeedbackKey{\"a\", \"100\", \"8\"}},\n\t}, false, false, false)\n\tsuite.NoError(err)\n\tresult, err := suite.Database.GetUserItemFeedback(ctx, \"100\", \"200\")\n\tsuite.NoError(err)\n\tsuite.Empty(result)\n\tresult, err = suite.Database.GetUserItemFeedback(ctx, \"0\", \"200\")\n\tsuite.NoError(err)\n\tsuite.Empty(result)\n\tresult, err = suite.Database.GetUserItemFeedback(ctx, \"100\", \"8\")\n\tsuite.NoError(err)\n\tsuite.Empty(result)\n\n\t// insert valid feedback and invalid feedback at the same time\n\terr = suite.Database.BatchInsertFeedback(ctx, []Feedback{\n\t\t{FeedbackKey: FeedbackKey{\"a\", \"0\", \"8\"}},\n\t\t{FeedbackKey: FeedbackKey{\"a\", \"100\", \"200\"}},\n\t}, false, false, false)\n\tsuite.NoError(err)\n\n\t// insert duplicate feedback\n\terr = suite.Database.BatchInsertFeedback(ctx, []Feedback{\n\t\t{FeedbackKey: FeedbackKey{\"a\", \"0\", \"0\"}, Value: 1, Timestamp: timestamp},\n\t\t{FeedbackKey: FeedbackKey{\"a\", \"0\", \"0\"}, Value: 1, Timestamp: timestamp},\n\t}, true, true, true)\n\tsuite.NoError(err)\n\terr = suite.Database.BatchInsertFeedback(ctx, []Feedback{\n\t\t{FeedbackKey: FeedbackKey{\"a\", \"0\", \"0\"}, Value: 1, Timestamp: timestamp},\n\t}, true, true, false)\n\tsuite.NoError(err)\n\t// check duplicate feedback\n\tret, err = suite.Database.GetUserItemFeedback(ctx, \"0\", \"0\", \"a\")\n\tsuite.NoError(err)\n\tsuite.Equal([]Feedback{{FeedbackKey: FeedbackKey{\"a\", \"0\", \"0\"}, Value: 2, Timestamp: timestamp, Updated: timestamp, Comment: \"\"}}, ret)\n\t// put duplicate feedback\n\terr = suite.Database.BatchInsertFeedback(ctx, []Feedback{\n\t\t{FeedbackKey: FeedbackKey{\"a\", \"0\", \"0\"}, Value: 1, Timestamp: timestamp},\n\t}, true, true, true)\n\tsuite.NoError(err)\n\t// check duplicate feedback again\n\tret, err = suite.Database.GetUserItemFeedback(ctx, \"0\", \"0\", \"a\")\n\tsuite.NoError(err)\n\tif suite.isClickHouse() {\n\t\tsuite.Equal([]Feedback{{FeedbackKey: FeedbackKey{\"a\", \"0\", \"0\"}, Value: 3, Timestamp: timestamp, Updated: timestamp, Comment: \"\"}}, ret)\n\t} else {\n\t\tsuite.Equal([]Feedback{{FeedbackKey: FeedbackKey{\"a\", \"0\", \"0\"}, Value: 1, Timestamp: timestamp, Updated: timestamp, Comment: \"\"}}, ret)\n\t}\n}\n\nfunc (suite *baseTestSuite) TestItems() {\n\tctx := suite.T().Context()\n\t// Items\n\titems := []Item{\n\t\t{\n\t\t\tItemId:     \"0\",\n\t\t\tIsHidden:   true,\n\t\t\tCategories: []string{\"a\"},\n\t\t\tTimestamp:  time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC),\n\t\t\tLabels:     []any{\"a\"},\n\t\t\tComment:    \"comment 0\",\n\t\t},\n\t\t{\n\t\t\tItemId:     \"2\",\n\t\t\tCategories: []string{\"b\"},\n\t\t\tTimestamp:  time.Date(1997, 3, 15, 0, 0, 0, 0, time.UTC),\n\t\t\tLabels:     []any{\"a\"},\n\t\t\tComment:    \"comment 2\",\n\t\t},\n\t\t{\n\t\t\tItemId:     \"4\",\n\t\t\tIsHidden:   true,\n\t\t\tCategories: []string{\"a\"},\n\t\t\tTimestamp:  time.Date(1998, 3, 15, 0, 0, 0, 0, time.UTC),\n\t\t\tLabels:     []any{\"a\", \"b\"},\n\t\t\tComment:    \"comment 4\",\n\t\t},\n\t\t{\n\t\t\tItemId:     \"6\",\n\t\t\tCategories: []string{\"b\"},\n\t\t\tTimestamp:  time.Date(1999, 3, 15, 0, 0, 0, 0, time.UTC),\n\t\t\tLabels:     []any{\"b\"},\n\t\t\tComment:    \"comment 6\",\n\t\t},\n\t\t{\n\t\t\tItemId:     \"8\",\n\t\t\tIsHidden:   true,\n\t\t\tCategories: []string{\"a\"},\n\t\t\tTimestamp:  time.Date(2000, 3, 15, 0, 0, 0, 0, time.UTC),\n\t\t\tLabels:     []any{\"b\"},\n\t\t\tComment:    \"comment 8\",\n\t\t},\n\t}\n\t// Insert item\n\terr := suite.Database.BatchInsertItems(ctx, items)\n\tsuite.NoError(err)\n\t// Count items\n\tsuite.analyzeTables()\n\tcount, err := suite.Database.CountItems(ctx)\n\tsuite.NoError(err)\n\tsuite.Equal(5, count)\n\t// Get items\n\ttotalItems := suite.getItems(ctx, 3)\n\tsuite.Equal(items, totalItems)\n\t// Get item stream\n\titemsFromStream := suite.getItemStream(ctx, 3)\n\tsuite.ElementsMatch(items, itemsFromStream)\n\t// Get item\n\tfor _, item := range items {\n\t\tret, err := suite.Database.GetItem(ctx, item.ItemId)\n\t\tsuite.NoError(err)\n\t\tsuite.Equal(item, ret)\n\t}\n\t// batch get items\n\tbatchItem, err := suite.Database.BatchGetItems(ctx, []string{\"2\", \"6\"})\n\tsuite.NoError(err)\n\tsuite.Equal([]Item{items[1], items[3]}, batchItem)\n\t// Test GetLatestItems\n\tlatestItems, err := suite.Database.GetLatestItems(ctx, 3, nil)\n\tsuite.NoError(err)\n\tsuite.Equal([]Item{items[3], items[1]}, latestItems)\n\tlatestItemsWithCategory, err := suite.Database.GetLatestItems(ctx, 3, []string{\"b\"})\n\tsuite.NoError(err)\n\tsuite.Equal([]Item{items[3], items[1]}, latestItemsWithCategory)\n\t// Delete item\n\terr = suite.Database.DeleteItem(ctx, \"0\")\n\tsuite.NoError(err)\n\t_, err = suite.Database.GetItem(ctx, \"0\")\n\tsuite.True(errors.Is(err, errors.NotFound), err)\n\n\t// test override\n\terr = suite.Database.BatchInsertItems(ctx, []Item{{ItemId: \"4\", IsHidden: false, Categories: []string{\"b\"}, Labels: []string{\"o\"}, Comment: \"override\"}})\n\tsuite.NoError(err)\n\terr = suite.Database.Optimize()\n\tsuite.NoError(err)\n\titem, err := suite.Database.GetItem(ctx, \"4\")\n\tsuite.NoError(err)\n\tsuite.False(item.IsHidden)\n\tsuite.Equal([]string{\"b\"}, item.Categories)\n\tsuite.Equal([]any{\"o\"}, item.Labels)\n\tsuite.Equal(\"override\", item.Comment)\n\n\t// test modify\n\ttimestamp := time.Date(2000, 1, 1, 1, 1, 1, 0, time.UTC)\n\terr = suite.Database.ModifyItem(ctx, \"2\", ItemPatch{IsHidden: new(true)})\n\tsuite.NoError(err)\n\terr = suite.Database.ModifyItem(ctx, \"2\", ItemPatch{Categories: []string{\"a\"}})\n\tsuite.NoError(err)\n\terr = suite.Database.ModifyItem(ctx, \"2\", ItemPatch{Comment: new(\"modify\")})\n\tsuite.NoError(err)\n\terr = suite.Database.ModifyItem(ctx, \"2\", ItemPatch{Labels: []string{\"a\", \"b\", \"c\"}})\n\tsuite.NoError(err)\n\terr = suite.Database.ModifyItem(ctx, \"2\", ItemPatch{Timestamp: &timestamp})\n\tsuite.NoError(err)\n\terr = suite.Database.Optimize()\n\tsuite.NoError(err)\n\titem, err = suite.Database.GetItem(ctx, \"2\")\n\tsuite.NoError(err)\n\tsuite.True(item.IsHidden)\n\tsuite.Equal([]string{\"a\"}, item.Categories)\n\tsuite.Equal(\"modify\", item.Comment)\n\tsuite.Equal([]any{\"a\", \"b\", \"c\"}, item.Labels)\n\tsuite.Equal(timestamp, item.Timestamp)\n\n\t// test insert empty\n\terr = suite.Database.BatchInsertItems(ctx, nil)\n\tsuite.NoError(err)\n\t// test get empty\n\titems, err = suite.Database.BatchGetItems(ctx, nil)\n\tsuite.NoError(err)\n\tsuite.Empty(items)\n\n\t// test insert duplicate items\n\terr = suite.Database.BatchInsertItems(ctx, []Item{{ItemId: \"1\"}, {ItemId: \"1\"}})\n\tsuite.NoError(err)\n}\n\nfunc (suite *baseTestSuite) TestDeleteUser() {\n\tctx := suite.T().Context()\n\t// Insert ret\n\tfeedback := []Feedback{\n\t\t{FeedbackKey: FeedbackKey{positiveFeedbackType, \"a\", \"0\"}, Value: 0, Timestamp: time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC), Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{positiveFeedbackType, \"a\", \"2\"}, Value: 0, Timestamp: time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC), Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{positiveFeedbackType, \"a\", \"4\"}, Value: 0, Timestamp: time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC), Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{positiveFeedbackType, \"a\", \"6\"}, Value: 0, Timestamp: time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC), Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{positiveFeedbackType, \"a\", \"8\"}, Value: 0, Timestamp: time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC), Comment: \"comment\"},\n\t}\n\terr := suite.Database.BatchInsertFeedback(ctx, feedback, true, true, true)\n\tsuite.NoError(err)\n\t// Delete user\n\terr = suite.Database.DeleteUser(ctx, \"a\")\n\tsuite.NoError(err)\n\t_, err = suite.Database.GetUser(ctx, \"a\")\n\tsuite.NotNil(err, \"failed to delete user\")\n\tret, err := suite.Database.GetUserFeedback(ctx, \"a\", lo.ToPtr(time.Now()), expression.MustParseFeedbackTypeExpression(positiveFeedbackType))\n\tsuite.NoError(err)\n\tsuite.Equal(0, len(ret))\n\t_, ret, err = suite.Database.GetFeedback(ctx, \"\", 100, nil, lo.ToPtr(time.Now()), positiveFeedbackType)\n\tsuite.NoError(err)\n\tsuite.Empty(ret)\n}\n\nfunc (suite *baseTestSuite) TestDeleteItem() {\n\tctx := suite.T().Context()\n\t// Insert ret\n\tfeedbacks := []Feedback{\n\t\t{FeedbackKey: FeedbackKey{positiveFeedbackType, \"0\", \"b\"}, Value: 0, Timestamp: time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC), Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{positiveFeedbackType, \"1\", \"b\"}, Value: 0, Timestamp: time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC), Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{positiveFeedbackType, \"2\", \"b\"}, Value: 0, Timestamp: time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC), Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{positiveFeedbackType, \"3\", \"b\"}, Value: 0, Timestamp: time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC), Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{positiveFeedbackType, \"4\", \"b\"}, Value: 0, Timestamp: time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC), Comment: \"comment\"},\n\t}\n\terr := suite.Database.BatchInsertFeedback(ctx, feedbacks, true, true, true)\n\tsuite.NoError(err)\n\t// Delete item\n\terr = suite.Database.DeleteItem(ctx, \"b\")\n\tsuite.NoError(err)\n\t_, err = suite.Database.GetItem(ctx, \"b\")\n\tsuite.Error(err, \"failed to delete item\")\n\tret, err := suite.Database.GetItemFeedback(ctx, \"b\", positiveFeedbackType)\n\tsuite.NoError(err)\n\tsuite.Equal(0, len(ret))\n\t_, ret, err = suite.Database.GetFeedback(ctx, \"\", 100, nil, lo.ToPtr(time.Now()), positiveFeedbackType)\n\tsuite.NoError(err)\n\tsuite.Empty(ret)\n}\n\nfunc (suite *baseTestSuite) TestDeleteFeedback() {\n\tctx := suite.T().Context()\n\tfeedbacks := []Feedback{\n\t\t{FeedbackKey: FeedbackKey{\"type1\", \"2\", \"3\"}, Value: 0, Timestamp: time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC), Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{\"type2\", \"2\", \"3\"}, Value: 0, Timestamp: time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC), Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{\"type3\", \"2\", \"3\"}, Value: 0, Timestamp: time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC), Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{\"type1\", \"2\", \"4\"}, Value: 0, Timestamp: time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC), Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{\"type1\", \"1\", \"3\"}, Value: 0, Timestamp: time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC), Comment: \"comment\"},\n\t}\n\terr := suite.Database.BatchInsertFeedback(ctx, feedbacks, true, true, true)\n\tsuite.NoError(err)\n\t// set Updated for comparison\n\tfor i := range feedbacks {\n\t\tfeedbacks[i].Updated = feedbacks[i].Timestamp\n\t}\n\t// get user-item feedback\n\tret, err := suite.Database.GetUserItemFeedback(ctx, \"2\", \"3\")\n\tsuite.NoError(err)\n\tsuite.ElementsMatch([]Feedback{feedbacks[0], feedbacks[1], feedbacks[2]}, ret)\n\tfeedbackType2 := \"type2\"\n\tret, err = suite.Database.GetUserItemFeedback(ctx, \"2\", \"3\", feedbackType2)\n\tsuite.NoError(err)\n\tsuite.Equal([]Feedback{feedbacks[1]}, ret)\n\t// delete user-item feedback\n\tdeleteCount, err := suite.Database.DeleteUserItemFeedback(ctx, \"2\", \"3\")\n\tsuite.NoError(err)\n\tif !suite.isClickHouse() {\n\t\t// RowAffected isn't supported by ClickHouse,\n\t\tsuite.Equal(3, deleteCount)\n\t}\n\terr = suite.Database.Optimize()\n\tsuite.NoError(err)\n\tret, err = suite.Database.GetUserItemFeedback(ctx, \"2\", \"3\")\n\tsuite.NoError(err)\n\tsuite.Empty(ret)\n\tfeedbackType1 := \"type1\"\n\tdeleteCount, err = suite.Database.DeleteUserItemFeedback(ctx, \"1\", \"3\", feedbackType1)\n\tsuite.NoError(err)\n\tif !suite.isClickHouse() {\n\t\t// RowAffected isn't supported by ClickHouse,\n\t\tsuite.Equal(1, deleteCount)\n\t}\n\tret, err = suite.Database.GetUserItemFeedback(ctx, \"1\", \"3\", feedbackType2)\n\tsuite.NoError(err)\n\tsuite.Empty(ret)\n}\n\nfunc (suite *baseTestSuite) TestTimeLimit() {\n\tctx := suite.T().Context()\n\t// insert items\n\titems := []Item{\n\t\t{\n\t\t\tItemId:    \"0\",\n\t\t\tTimestamp: time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC),\n\t\t\tLabels:    []any{\"a\"},\n\t\t\tComment:   \"comment 0\",\n\t\t},\n\t\t{\n\t\t\tItemId:    \"2\",\n\t\t\tTimestamp: time.Date(1997, 3, 15, 0, 0, 0, 0, time.UTC),\n\t\t\tLabels:    []any{\"a\"},\n\t\t\tComment:   \"comment 2\",\n\t\t},\n\t\t{\n\t\t\tItemId:    \"4\",\n\t\t\tTimestamp: time.Date(1998, 3, 15, 0, 0, 0, 0, time.UTC),\n\t\t\tLabels:    []any{\"a\", \"b\"},\n\t\t\tComment:   \"comment 4\",\n\t\t},\n\t\t{\n\t\t\tItemId:    \"6\",\n\t\t\tTimestamp: time.Date(1999, 3, 15, 0, 0, 0, 0, time.UTC),\n\t\t\tLabels:    []any{\"b\"},\n\t\t\tComment:   \"comment 6\",\n\t\t},\n\t\t{\n\t\t\tItemId:    \"8\",\n\t\t\tTimestamp: time.Date(2000, 3, 15, 0, 0, 0, 0, time.UTC),\n\t\t\tLabels:    []any{\"b\"},\n\t\t\tComment:   \"comment 8\",\n\t\t},\n\t}\n\terr := suite.Database.BatchInsertItems(ctx, items)\n\tsuite.NoError(err)\n\ttimeLimit := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)\n\t_, ret, err := suite.Database.GetItems(ctx, \"\", 100, &timeLimit)\n\tsuite.NoError(err)\n\tsuite.Equal([]Item{items[2], items[3], items[4]}, ret)\n\n\t// insert feedback\n\tfeedbacks := []Feedback{\n\t\t{FeedbackKey: FeedbackKey{\"type1\", \"2\", \"3\"}, Value: 0, Timestamp: time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC), Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{\"type2\", \"2\", \"3\"}, Value: 0, Timestamp: time.Date(1997, 3, 15, 0, 0, 0, 0, time.UTC), Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{\"type3\", \"2\", \"3\"}, Value: 0, Timestamp: time.Date(1998, 3, 15, 0, 0, 0, 0, time.UTC), Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{\"type1\", \"2\", \"4\"}, Value: 0, Timestamp: time.Date(1999, 3, 15, 0, 0, 0, 0, time.UTC), Comment: \"comment\"},\n\t\t{FeedbackKey: FeedbackKey{\"type1\", \"1\", \"3\"}, Value: 0, Timestamp: time.Date(2000, 3, 15, 0, 0, 0, 0, time.UTC), Comment: \"comment\"},\n\t}\n\terr = suite.Database.BatchInsertFeedback(ctx, feedbacks, true, true, true)\n\tsuite.NoError(err)\n\t// set Updated for comparison\n\tfor i := range feedbacks {\n\t\tfeedbacks[i].Updated = feedbacks[i].Timestamp\n\t}\n\t_, retFeedback, err := suite.Database.GetFeedback(ctx, \"\", 100, &timeLimit, lo.ToPtr(time.Now()))\n\tsuite.NoError(err)\n\tsuite.Equal([]Feedback{feedbacks[4], feedbacks[3], feedbacks[2]}, retFeedback)\n\ttypeFilter := \"type1\"\n\t_, retFeedback, err = suite.Database.GetFeedback(ctx, \"\", 100, &timeLimit, lo.ToPtr(time.Now()), typeFilter)\n\tsuite.NoError(err)\n\tsuite.Equal([]Feedback{feedbacks[4], feedbacks[3]}, retFeedback)\n}\n\nfunc (suite *baseTestSuite) TestTimezone() {\n\tctx := suite.T().Context()\n\tloc, err := time.LoadLocation(\"Asia/Tokyo\")\n\tsuite.NoError(err)\n\t// insert feedbacks\n\terr = suite.Database.BatchInsertFeedback(ctx, []Feedback{\n\t\t{FeedbackKey: FeedbackKey{\"read\", \"1\", \"1\"}, Timestamp: time.Now().Add(-time.Second).In(loc)},\n\t\t{FeedbackKey: FeedbackKey{\"read\", \"1\", \"2\"}, Timestamp: time.Now().Add(-time.Second).In(loc)},\n\t\t{FeedbackKey: FeedbackKey{\"read\", \"2\", \"2\"}, Timestamp: time.Now().Add(-time.Second).In(loc)},\n\t\t{FeedbackKey: FeedbackKey{\"like\", \"1\", \"1\"}, Timestamp: time.Now().Add(time.Hour).In(loc)},\n\t\t{FeedbackKey: FeedbackKey{\"like\", \"1\", \"2\"}, Timestamp: time.Now().Add(time.Hour).In(loc)},\n\t\t{FeedbackKey: FeedbackKey{\"like\", \"2\", \"2\"}, Timestamp: time.Now().Add(time.Hour).In(loc)},\n\t}, true, true, true)\n\tsuite.NoError(err)\n\t// get feedback stream\n\tfeedback := suite.getFeedback(ctx, 10, nil, lo.ToPtr(time.Now()))\n\tsuite.Equal(3, len(feedback))\n\t// get feedback\n\t_, feedback, err = suite.Database.GetFeedback(ctx, \"\", 10, nil, lo.ToPtr(time.Now()))\n\tsuite.NoError(err)\n\tsuite.Equal(3, len(feedback))\n\t// get user feedback\n\tfeedback, err = suite.Database.GetUserFeedback(ctx, \"1\", lo.ToPtr(time.Now()))\n\tsuite.NoError(err)\n\tsuite.Equal(2, len(feedback))\n\t// get item feedback\n\tfeedback, err = suite.Database.GetItemFeedback(ctx, \"2\") // no future feedback by default\n\tsuite.NoError(err)\n\tsuite.Equal(2, len(feedback))\n\t// get user item feedback\n\tfeedback, err = suite.Database.GetUserItemFeedback(ctx, \"1\", \"1\") // return future feedback by default\n\tsuite.NoError(err)\n\tsuite.Equal(2, len(feedback))\n\n\t// insert items\n\tnow := time.Now().In(loc)\n\terr = suite.Database.BatchInsertItems(ctx, []Item{{ItemId: \"100\", Timestamp: now}, {ItemId: \"200\"}})\n\tsuite.NoError(err)\n\terr = suite.Database.ModifyItem(ctx, \"200\", ItemPatch{Timestamp: &now})\n\tsuite.NoError(err)\n\terr = suite.Database.Optimize()\n\tsuite.NoError(err)\n\tswitch database := suite.Database.(type) {\n\tcase *SQLDatabase:\n\t\tswitch suite.Database.(*SQLDatabase).driver {\n\t\tcase Postgres:\n\t\t\titem, err := suite.Database.GetItem(ctx, \"100\")\n\t\t\tsuite.NoError(err)\n\t\t\tsuite.Equal(now.Round(time.Microsecond).In(time.UTC), item.Timestamp)\n\t\t\titem, err = suite.Database.GetItem(ctx, \"200\")\n\t\t\tsuite.NoError(err)\n\t\t\tsuite.Equal(now.Round(time.Microsecond).In(time.UTC), item.Timestamp)\n\t\tcase ClickHouse:\n\t\t\titem, err := suite.Database.GetItem(ctx, \"100\")\n\t\t\tsuite.NoError(err)\n\t\t\tsuite.Equal(now.Truncate(time.Second).In(time.UTC), item.Timestamp)\n\t\t\titem, err = suite.Database.GetItem(ctx, \"200\")\n\t\t\tsuite.NoError(err)\n\t\t\tsuite.Equal(now.Truncate(time.Second).In(time.UTC), item.Timestamp)\n\t\tcase SQLite:\n\t\t\titem, err := suite.Database.GetItem(ctx, \"100\")\n\t\t\tsuite.NoError(err)\n\t\t\tsuite.Equal(now.In(time.UTC), item.Timestamp.In(time.UTC))\n\t\t\titem, err = suite.Database.GetItem(ctx, \"200\")\n\t\t\tsuite.NoError(err)\n\t\t\tsuite.Equal(now.In(time.UTC), item.Timestamp.In(time.UTC))\n\t\tdefault:\n\t\t\tsuite.T().Skipf(\"unknown sql database: %v\", database.driver)\n\t\t}\n\tcase *MongoDB:\n\t\titem, err := suite.Database.GetItem(ctx, \"100\")\n\t\tsuite.NoError(err)\n\t\tsuite.Equal(now.Truncate(time.Millisecond).In(time.UTC), item.Timestamp)\n\t\titem, err = suite.Database.GetItem(ctx, \"200\")\n\t\tsuite.NoError(err)\n\t\tsuite.Equal(now.Truncate(time.Millisecond).In(time.UTC), item.Timestamp)\n\tdefault:\n\t\tsuite.T().Skipf(\"unknown database: %v\", reflect.TypeOf(suite.Database))\n\t}\n}\n\nfunc (suite *baseTestSuite) TestCollation() {\n\tctx := suite.T().Context()\n\terr := suite.Database.BatchInsertFeedback(ctx, []Feedback{\n\t\t{FeedbackKey: FeedbackKey{\"type\", \"user\", \"A\"}},\n\t\t{FeedbackKey: FeedbackKey{\"type\", \"user\", \"a\"}},\n\t\t{FeedbackKey: FeedbackKey{\"type\", \"user\", \"B\"}},\n\t\t{FeedbackKey: FeedbackKey{\"type\", \"user\", \"b\"}},\n\t}, true, true, true)\n\tsuite.NoError(err)\n\tfeedbacks := suite.getFeedbackStream(ctx, 10, WithBeginItemId(\"a\"), WithEndItemId(\"B\"))\n\tsuite.Empty(feedbacks)\n}\n\nfunc (suite *baseTestSuite) TestPurge() {\n\tctx := suite.T().Context()\n\t// insert data\n\terr := suite.Database.BatchInsertFeedback(ctx, lo.Map(lo.Range(100), func(t int, i int) Feedback {\n\t\treturn Feedback{FeedbackKey: FeedbackKey{\n\t\t\tFeedbackType: \"click\",\n\t\t\tUserId:       strconv.Itoa(t),\n\t\t\tItemId:       strconv.Itoa(t),\n\t\t}}\n\t}), true, true, true)\n\tsuite.NoError(err)\n\t_, users, err := suite.Database.GetUsers(ctx, \"\", 100)\n\tsuite.NoError(err)\n\tsuite.Equal(100, len(users))\n\t_, items, err := suite.Database.GetItems(ctx, \"\", 100, nil)\n\tsuite.NoError(err)\n\tsuite.Equal(100, len(items))\n\t_, feedbacks, err := suite.Database.GetFeedback(ctx, \"\", 100, nil, lo.ToPtr(time.Now()))\n\tsuite.NoError(err)\n\tsuite.Equal(100, len(feedbacks))\n\t// purge data\n\terr = suite.Database.Purge()\n\tsuite.NoError(err)\n\t_, users, err = suite.Database.GetUsers(ctx, \"\", 100)\n\tsuite.NoError(err)\n\tsuite.Empty(users)\n\t_, items, err = suite.Database.GetItems(ctx, \"\", 100, nil)\n\tsuite.NoError(err)\n\tsuite.Empty(items)\n\t_, feedbacks, err = suite.Database.GetFeedback(ctx, \"\", 100, nil, lo.ToPtr(time.Now()))\n\tsuite.NoError(err)\n\tsuite.Empty(feedbacks)\n\t// purge empty database\n\terr = suite.Database.Purge()\n\tsuite.NoError(err)\n}\n\nfunc TestSortFeedbacks(t *testing.T) {\n\tfeedback := []Feedback{\n\t\t{FeedbackKey: FeedbackKey{\"star\", \"1\", \"1\"}, Timestamp: time.Date(2000, 10, 1, 0, 0, 0, 0, time.UTC)},\n\t\t{FeedbackKey: FeedbackKey{\"like\", \"1\", \"1\"}, Timestamp: time.Date(2001, 10, 1, 0, 0, 0, 0, time.UTC)},\n\t\t{FeedbackKey: FeedbackKey{\"read\", \"1\", \"1\"}, Timestamp: time.Date(2002, 10, 1, 0, 0, 0, 0, time.UTC)},\n\t}\n\tSortFeedbacks(feedback)\n\tassert.Equal(t, []Feedback{\n\t\t{FeedbackKey: FeedbackKey{\"read\", \"1\", \"1\"}, Timestamp: time.Date(2002, 10, 1, 0, 0, 0, 0, time.UTC)},\n\t\t{FeedbackKey: FeedbackKey{\"like\", \"1\", \"1\"}, Timestamp: time.Date(2001, 10, 1, 0, 0, 0, 0, time.UTC)},\n\t\t{FeedbackKey: FeedbackKey{\"star\", \"1\", \"1\"}, Timestamp: time.Date(2000, 10, 1, 0, 0, 0, 0, time.UTC)},\n\t}, feedback)\n}\n\nfunc TestValidateLabels(t *testing.T) {\n\tassert.NoError(t, ValidateLabels(nil))\n\tassert.NoError(t, ValidateLabels(json.Number(\"1\")))\n\tassert.NoError(t, ValidateLabels(\"label\"))\n\tassert.NoError(t, ValidateLabels([]any{json.Number(\"1\"), json.Number(\"2\"), json.Number(\"3\")}))\n\tassert.NoError(t, ValidateLabels([]any{\"1\", \"2\", \"3\"}))\n\tassert.NoError(t, ValidateLabels(map[string]any{\"city\": json.Number(\"1\"), \"tags\": []any{json.Number(\"1\"), json.Number(\"2\"), json.Number(\"3\")}}))\n\tassert.NoError(t, ValidateLabels(map[string]any{\"city\": \"wenzhou\", \"tags\": []any{\"1\", \"2\", \"3\"}}))\n\tassert.NoError(t, ValidateLabels(map[string]any{\"address\": map[string]any{\"province\": json.Number(\"1\"), \"city\": json.Number(\"2\")}}))\n\tassert.NoError(t, ValidateLabels(map[string]any{\"address\": map[string]any{\"province\": \"zhejiang\", \"city\": \"wenzhou\"}}))\n\n\tassert.Error(t, ValidateLabels(map[string]any{\"price\": 100, \"tags\": []any{json.Number(\"1\"), \"2\", \"3\"}}))\n\tassert.Error(t, ValidateLabels(map[string]any{\"city\": \"wenzhou\", \"tags\": []any{\"1\", json.Number(\"2\"), \"3\"}}))\n\tassert.Error(t, ValidateLabels(map[string]any{\"city\": \"wenzhou\", \"tags\": []any{\"1\", \"2\", json.Number(\"3\")}}))\n}\n\nfunc benchmarkCountItems(b *testing.B, db Database) {\n\tctx := b.Context()\n\t// Insert 10,000 items\n\titems := make([]Item, 100000)\n\tfor i := range items {\n\t\titems[i] = Item{ItemId: strconv.Itoa(i)}\n\t}\n\terr := db.BatchInsertItems(ctx, items)\n\trequire.NoError(b, err)\n\t// Benchmark count items\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tn, err := db.CountItems(ctx)\n\t\trequire.NoError(b, err)\n\t\trequire.Equal(b, 100000, n)\n\t}\n}\n"
  },
  {
    "path": "storage/data/mongodb.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage data\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"time\"\n\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/gorse-io/gorse/common/expression\"\n\t\"github.com/gorse-io/gorse/storage\"\n\t\"github.com/juju/errors\"\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/bson/primitive\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n\t\"go.mongodb.org/mongo-driver/x/mongo/driver/connstring\"\n\t\"go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo\"\n)\n\nfunc init() {\n\tRegister([]string{storage.MongoPrefix, storage.MongoSrvPrefix}, func(path, tablePrefix string, opts ...storage.Option) (Database, error) {\n\t\t// connect to database\n\t\tdatabase := new(MongoDB)\n\t\tclientOpts := options.Client()\n\t\tclientOpts.Monitor = otelmongo.NewMonitor()\n\t\tclientOpts.ApplyURI(path)\n\t\tvar err error\n\t\tif database.client, err = mongo.Connect(context.Background(), clientOpts); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\t// parse DSN and extract database name\n\t\tif cs, err := connstring.ParseAndValidate(path); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t} else {\n\t\t\tdatabase.dbName = cs.Database\n\t\t\tdatabase.TablePrefix = storage.TablePrefix(tablePrefix)\n\t\t}\n\t\treturn database, nil\n\t})\n}\n\nfunc feedbackKeyFromString(s string) (*FeedbackKey, error) {\n\tvar feedbackKey FeedbackKey\n\terr := json.Unmarshal([]byte(s), &feedbackKey)\n\treturn &feedbackKey, err\n}\n\nfunc (k *FeedbackKey) toString() (string, error) {\n\tb, err := json.Marshal(k)\n\treturn string(b), err\n}\n\nfunc unpack(o any) any {\n\tif o == nil {\n\t\treturn nil\n\t}\n\tswitch p := o.(type) {\n\tcase primitive.A:\n\t\treturn []any(p)\n\tcase primitive.D:\n\t\tm := make(map[string]any)\n\t\tfor _, e := range p {\n\t\t\tm[e.Key] = unpack(e.Value)\n\t\t}\n\t\treturn m\n\tdefault:\n\t\treturn p\n\t}\n}\n\nfunc FeedbackTypeExpressionToMongo(e expression.FeedbackTypeExpression) bson.M {\n\tfilter := bson.M{\"feedbackkey.feedbacktype\": e.FeedbackType}\n\tswitch e.ExprType {\n\tcase expression.Less:\n\t\tfilter[\"value\"] = bson.M{\"$lt\": e.Value}\n\tcase expression.LessOrEqual:\n\t\tfilter[\"value\"] = bson.M{\"$lte\": e.Value}\n\tcase expression.Greater:\n\t\tfilter[\"value\"] = bson.M{\"$gt\": e.Value}\n\tcase expression.GreaterOrEqual:\n\t\tfilter[\"value\"] = bson.M{\"$gte\": e.Value}\n\t}\n\treturn filter\n}\n\n// MongoDB is the data storage based on MongoDB.\ntype MongoDB struct {\n\tstorage.TablePrefix\n\tclient *mongo.Client\n\tdbName string\n}\n\n// Optimize is used by ClickHouse only.\nfunc (db *MongoDB) Optimize() error {\n\treturn nil\n}\n\n// Init collections and indices in MongoDB.\nfunc (db *MongoDB) Init() error {\n\tctx := context.Background()\n\td := db.client.Database(db.dbName)\n\t// list collections\n\tvar hasUsers, hasItems, hasFeedback bool\n\tcollections, err := d.ListCollectionNames(ctx, bson.M{})\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tfor _, collectionName := range collections {\n\t\tswitch collectionName {\n\t\tcase db.UsersTable():\n\t\t\thasUsers = true\n\t\tcase db.ItemsTable():\n\t\t\thasItems = true\n\t\tcase db.FeedbackTable():\n\t\t\thasFeedback = true\n\t\t}\n\t}\n\t// create collections\n\tif !hasUsers {\n\t\tif err = d.CreateCollection(ctx, db.UsersTable()); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\tif !hasItems {\n\t\tif err = d.CreateCollection(ctx, db.ItemsTable()); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\tif !hasFeedback {\n\t\tif err = d.CreateCollection(ctx, db.FeedbackTable()); err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\t// create index\n\t_, err = d.Collection(db.UsersTable()).Indexes().CreateOne(ctx, mongo.IndexModel{\n\t\tKeys: bson.M{\n\t\t\t\"userid\": 1,\n\t\t},\n\t\tOptions: options.Index().SetUnique(true),\n\t})\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t_, err = d.Collection(db.ItemsTable()).Indexes().CreateOne(ctx, mongo.IndexModel{\n\t\tKeys: bson.M{\n\t\t\t\"itemid\": 1,\n\t\t},\n\t\tOptions: options.Index().SetUnique(true),\n\t})\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t_, err = d.Collection(db.FeedbackTable()).Indexes().CreateOne(ctx, mongo.IndexModel{\n\t\tKeys: bson.M{\n\t\t\t\"feedbackkey\": 1,\n\t\t},\n\t\tOptions: options.Index().SetUnique(true),\n\t})\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t_, err = d.Collection(db.FeedbackTable()).Indexes().CreateOne(ctx, mongo.IndexModel{\n\t\tKeys: bson.M{\n\t\t\t\"feedbackkey.userid\": 1,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t_, err = d.Collection(db.FeedbackTable()).Indexes().CreateOne(ctx, mongo.IndexModel{\n\t\tKeys: bson.M{\n\t\t\t\"feedbackkey.itemid\": 1,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t_, err = d.Collection(db.ItemsTable()).Indexes().CreateOne(ctx, mongo.IndexModel{\n\t\tKeys: bson.M{\n\t\t\t\"timestamp\": 1,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\treturn nil\n}\n\nfunc (db *MongoDB) Ping() error {\n\treturn db.client.Ping(context.Background(), nil)\n}\n\n// Close connection to MongoDB.\nfunc (db *MongoDB) Close() error {\n\treturn db.client.Disconnect(context.Background())\n}\n\nfunc (db *MongoDB) Purge() error {\n\ttables := []string{db.ItemsTable(), db.FeedbackTable(), db.UsersTable()}\n\tfor _, tableName := range tables {\n\t\tc := db.client.Database(db.dbName).Collection(tableName)\n\t\t_, err := c.DeleteMany(context.Background(), bson.D{})\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// BatchInsertItems insert items into MongoDB.\nfunc (db *MongoDB) BatchInsertItems(ctx context.Context, items []Item) error {\n\tif len(items) == 0 {\n\t\treturn nil\n\t}\n\tc := db.client.Database(db.dbName).Collection(db.ItemsTable())\n\tvar models []mongo.WriteModel\n\tfor _, item := range items {\n\t\tmodels = append(models, mongo.NewUpdateOneModel().\n\t\t\tSetUpsert(true).\n\t\t\tSetFilter(bson.M{\"itemid\": bson.M{\"$eq\": item.ItemId}}).\n\t\t\tSetUpdate(bson.M{\"$set\": item}))\n\t}\n\t_, err := c.BulkWrite(ctx, models)\n\treturn errors.Trace(err)\n}\n\nfunc (db *MongoDB) BatchGetItems(ctx context.Context, itemIds []string) ([]Item, error) {\n\tif len(itemIds) == 0 {\n\t\treturn nil, nil\n\t}\n\tc := db.client.Database(db.dbName).Collection(db.ItemsTable())\n\tr, err := c.Find(ctx, bson.M{\"itemid\": bson.M{\"$in\": itemIds}})\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\titems := make([]Item, 0)\n\tdefer r.Close(ctx)\n\tfor r.Next(ctx) {\n\t\tvar item Item\n\t\tif err = r.Decode(&item); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\titem.Labels = unpack(item.Labels)\n\t\titems = append(items, item)\n\t}\n\treturn items, nil\n}\n\n// ModifyItem modify an item in MongoDB.\nfunc (db *MongoDB) ModifyItem(ctx context.Context, itemId string, patch ItemPatch) error {\n\t// create update\n\tupdate := bson.M{}\n\tif patch.IsHidden != nil {\n\t\tupdate[\"ishidden\"] = patch.IsHidden\n\t}\n\tif patch.Categories != nil {\n\t\tupdate[\"categories\"] = patch.Categories\n\t}\n\tif patch.Comment != nil {\n\t\tupdate[\"comment\"] = patch.Comment\n\t}\n\tif patch.Labels != nil {\n\t\tupdate[\"labels\"] = patch.Labels\n\t}\n\tif patch.Timestamp != nil {\n\t\tupdate[\"timestamp\"] = patch.Timestamp\n\t}\n\t// execute\n\tc := db.client.Database(db.dbName).Collection(db.ItemsTable())\n\t_, err := c.UpdateOne(ctx, bson.M{\"itemid\": bson.M{\"$eq\": itemId}}, bson.M{\"$set\": update})\n\treturn errors.Trace(err)\n}\n\n// DeleteItem deletes a item from MongoDB.\nfunc (db *MongoDB) DeleteItem(ctx context.Context, itemId string) error {\n\tc := db.client.Database(db.dbName).Collection(db.ItemsTable())\n\t_, err := c.DeleteOne(ctx, bson.M{\"itemid\": itemId})\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tc = db.client.Database(db.dbName).Collection(db.FeedbackTable())\n\t_, err = c.DeleteMany(ctx, bson.M{\n\t\t\"feedbackkey.itemid\": bson.M{\"$eq\": itemId},\n\t})\n\treturn errors.Trace(err)\n}\n\n// GetItem returns a item from MongoDB.\nfunc (db *MongoDB) GetItem(ctx context.Context, itemId string) (item Item, err error) {\n\tc := db.client.Database(db.dbName).Collection(db.ItemsTable())\n\tr := c.FindOne(ctx, bson.M{\"itemid\": itemId})\n\tif r.Err() == mongo.ErrNoDocuments {\n\t\terr = errors.Annotate(ErrItemNotExist, itemId)\n\t\treturn\n\t}\n\terr = r.Decode(&item)\n\titem.Labels = unpack(item.Labels)\n\treturn\n}\n\n// GetItems returns items from MongoDB.\nfunc (db *MongoDB) GetItems(ctx context.Context, cursor string, n int, timeLimit *time.Time) (string, []Item, error) {\n\tbuf, err := base64.StdEncoding.DecodeString(cursor)\n\tif err != nil {\n\t\treturn \"\", nil, errors.Trace(err)\n\t}\n\tcursorItem := string(buf)\n\tc := db.client.Database(db.dbName).Collection(db.ItemsTable())\n\topt := options.Find()\n\topt.SetLimit(int64(n))\n\topt.SetSort(bson.D{{\"itemid\", 1}})\n\tfilter := bson.M{\"itemid\": bson.M{\"$gt\": cursorItem}}\n\tif timeLimit != nil {\n\t\tfilter[\"timestamp\"] = bson.M{\"$gt\": *timeLimit}\n\t}\n\tr, err := c.Find(ctx, filter, opt)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\titems := make([]Item, 0)\n\tdefer r.Close(ctx)\n\tfor r.Next(ctx) {\n\t\tvar item Item\n\t\tif err = r.Decode(&item); err != nil {\n\t\t\treturn \"\", nil, err\n\t\t}\n\t\titem.Labels = unpack(item.Labels)\n\t\titems = append(items, item)\n\t}\n\tif len(items) == n {\n\t\tcursor = items[n-1].ItemId\n\t} else {\n\t\tcursor = \"\"\n\t}\n\treturn base64.StdEncoding.EncodeToString([]byte(cursor)), items, nil\n}\n\n// GetLatestItems returns the latest items from MongoDB.\nfunc (db *MongoDB) GetLatestItems(ctx context.Context, n int, categories []string) ([]Item, error) {\n\tc := db.client.Database(db.dbName).Collection(db.ItemsTable())\n\topt := options.Find()\n\topt.SetLimit(int64(n))\n\topt.SetSort(bson.D{{\"timestamp\", -1}})\n\tfilter := bson.M{\"ishidden\": bson.M{\"$ne\": true}}\n\tif len(categories) > 0 {\n\t\tfilter[\"categories\"] = bson.M{\"$all\": categories}\n\t}\n\tr, err := c.Find(ctx, filter, opt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\titems := make([]Item, 0)\n\tdefer r.Close(ctx)\n\tfor r.Next(ctx) {\n\t\tvar item Item\n\t\tif err = r.Decode(&item); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\titem.Labels = unpack(item.Labels)\n\t\titems = append(items, item)\n\t}\n\treturn items, nil\n}\n\n// GetItemStream read items from MongoDB by stream.\nfunc (db *MongoDB) GetItemStream(ctx context.Context, batchSize int, timeLimit *time.Time) (chan []Item, chan error) {\n\titemChan := make(chan []Item, bufSize)\n\terrChan := make(chan error, 1)\n\tgo func() {\n\t\tdefer close(itemChan)\n\t\tdefer close(errChan)\n\t\t// send query\n\t\tctx := context.Background()\n\t\tc := db.client.Database(db.dbName).Collection(db.ItemsTable())\n\t\topt := options.Find()\n\t\tfilter := bson.M{}\n\t\tif timeLimit != nil {\n\t\t\tfilter[\"timestamp\"] = bson.M{\"$gt\": *timeLimit}\n\t\t}\n\t\tr, err := c.Find(ctx, filter, opt)\n\t\tif err != nil {\n\t\t\terrChan <- errors.Trace(err)\n\t\t\treturn\n\t\t}\n\t\t// fetch result\n\t\titems := make([]Item, 0, batchSize)\n\t\tdefer r.Close(ctx)\n\t\tfor r.Next(ctx) {\n\t\t\tvar item Item\n\t\t\tif err = r.Decode(&item); err != nil {\n\t\t\t\terrChan <- errors.Trace(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\titem.Labels = unpack(item.Labels)\n\t\t\titems = append(items, item)\n\t\t\tif len(items) == batchSize {\n\t\t\t\titemChan <- items\n\t\t\t\titems = make([]Item, 0, batchSize)\n\t\t\t}\n\t\t}\n\t\tif len(items) > 0 {\n\t\t\titemChan <- items\n\t\t}\n\t\terrChan <- nil\n\t}()\n\treturn itemChan, errChan\n}\n\n// GetItemFeedback returns feedback of a item from MongoDB.\nfunc (db *MongoDB) GetItemFeedback(ctx context.Context, itemId string, feedbackTypes ...string) ([]Feedback, error) {\n\tc := db.client.Database(db.dbName).Collection(db.FeedbackTable())\n\tvar r *mongo.Cursor\n\tvar err error\n\tfilter := bson.M{\n\t\t\"feedbackkey.itemid\": bson.M{\"$eq\": itemId},\n\t\t\"timestamp\":          bson.M{\"$lte\": time.Now()},\n\t}\n\tif len(feedbackTypes) > 0 {\n\t\tvar conditions []bson.M\n\t\tfor _, feedbackType := range feedbackTypes {\n\t\t\tconditions = append(conditions, bson.M{\n\t\t\t\t\"feedbackkey.feedbacktype\": bson.M{\"$eq\": feedbackType},\n\t\t\t})\n\t\t}\n\t\tfilter[\"$or\"] = conditions\n\t}\n\tr, err = c.Find(ctx, filter)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfeedbacks := make([]Feedback, 0)\n\tdefer r.Close(ctx)\n\tfor r.Next(ctx) {\n\t\tvar feedback Feedback\n\t\tif err = r.Decode(&feedback); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfeedbacks = append(feedbacks, feedback)\n\t}\n\treturn feedbacks, nil\n}\n\n// BatchInsertUsers inserts a user into MongoDB.\nfunc (db *MongoDB) BatchInsertUsers(ctx context.Context, users []User) error {\n\tif len(users) == 0 {\n\t\treturn nil\n\t}\n\tc := db.client.Database(db.dbName).Collection(db.UsersTable())\n\tvar models []mongo.WriteModel\n\tfor _, user := range users {\n\t\tmodels = append(models, mongo.NewUpdateOneModel().\n\t\t\tSetUpsert(true).\n\t\t\tSetFilter(bson.M{\"userid\": bson.M{\"$eq\": user.UserId}}).\n\t\t\tSetUpdate(bson.M{\"$set\": user}))\n\t}\n\t_, err := c.BulkWrite(ctx, models)\n\treturn errors.Trace(err)\n}\n\n// ModifyUser modify a user in MongoDB.\nfunc (db *MongoDB) ModifyUser(ctx context.Context, userId string, patch UserPatch) error {\n\t// create patch\n\tupdate := bson.M{}\n\tif patch.Labels != nil {\n\t\tupdate[\"labels\"] = patch.Labels\n\t}\n\tif patch.Comment != nil {\n\t\tupdate[\"comment\"] = patch.Comment\n\t}\n\t// execute\n\tc := db.client.Database(db.dbName).Collection(db.UsersTable())\n\t_, err := c.UpdateOne(ctx, bson.M{\"userid\": bson.M{\"$eq\": userId}}, bson.M{\"$set\": update})\n\treturn errors.Trace(err)\n}\n\n// DeleteUser deletes a user from MongoDB.\nfunc (db *MongoDB) DeleteUser(ctx context.Context, userId string) error {\n\tc := db.client.Database(db.dbName).Collection(db.UsersTable())\n\t_, err := c.DeleteOne(ctx, bson.M{\"userid\": userId})\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tc = db.client.Database(db.dbName).Collection(db.FeedbackTable())\n\t_, err = c.DeleteMany(ctx, bson.M{\n\t\t\"feedbackkey.userid\": bson.M{\"$eq\": userId},\n\t})\n\treturn errors.Trace(err)\n}\n\n// GetUser returns a user from MongoDB.\nfunc (db *MongoDB) GetUser(ctx context.Context, userId string) (user User, err error) {\n\tc := db.client.Database(db.dbName).Collection(db.UsersTable())\n\tr := c.FindOne(ctx, bson.M{\"userid\": userId})\n\tif r.Err() == mongo.ErrNoDocuments {\n\t\terr = errors.Annotate(ErrUserNotExist, userId)\n\t\treturn\n\t}\n\terr = r.Decode(&user)\n\tuser.Labels = unpack(user.Labels)\n\treturn\n}\n\n// GetUsers returns users from MongoDB.\nfunc (db *MongoDB) GetUsers(ctx context.Context, cursor string, n int) (string, []User, error) {\n\tbuf, err := base64.StdEncoding.DecodeString(cursor)\n\tif err != nil {\n\t\treturn \"\", nil, errors.Trace(err)\n\t}\n\tcursorUser := string(buf)\n\tc := db.client.Database(db.dbName).Collection(db.UsersTable())\n\topt := options.Find()\n\topt.SetLimit(int64(n))\n\topt.SetSort(bson.D{{\"userid\", 1}})\n\tr, err := c.Find(ctx, bson.M{\"userid\": bson.M{\"$gt\": cursorUser}}, opt)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tusers := make([]User, 0)\n\tdefer r.Close(ctx)\n\tfor r.Next(ctx) {\n\t\tvar user User\n\t\tif err = r.Decode(&user); err != nil {\n\t\t\treturn \"\", nil, err\n\t\t}\n\t\tuser.Labels = unpack(user.Labels)\n\t\tusers = append(users, user)\n\t}\n\tif len(users) == n {\n\t\tcursor = users[n-1].UserId\n\t} else {\n\t\tcursor = \"\"\n\t}\n\treturn base64.StdEncoding.EncodeToString([]byte(cursor)), users, nil\n}\n\n// GetUserStream reads users from MongoDB by stream.\nfunc (db *MongoDB) GetUserStream(ctx context.Context, batchSize int) (chan []User, chan error) {\n\tuserChan := make(chan []User, bufSize)\n\terrChan := make(chan error, 1)\n\tgo func() {\n\t\tdefer close(userChan)\n\t\tdefer close(errChan)\n\t\t// send query\n\t\tctx := context.Background()\n\t\tc := db.client.Database(db.dbName).Collection(db.UsersTable())\n\t\topt := options.Find()\n\t\tr, err := c.Find(ctx, bson.M{}, opt)\n\t\tif err != nil {\n\t\t\terrChan <- errors.Trace(err)\n\t\t\treturn\n\t\t}\n\t\tusers := make([]User, 0, batchSize)\n\t\tdefer r.Close(ctx)\n\t\tfor r.Next(ctx) {\n\t\t\tvar user User\n\t\t\tif err = r.Decode(&user); err != nil {\n\t\t\t\terrChan <- errors.Trace(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tuser.Labels = unpack(user.Labels)\n\t\t\tusers = append(users, user)\n\t\t\tif len(users) == batchSize {\n\t\t\t\tuserChan <- users\n\t\t\t\tusers = make([]User, 0, batchSize)\n\t\t\t}\n\t\t}\n\t\tif len(users) > 0 {\n\t\t\tuserChan <- users\n\t\t}\n\t\terrChan <- nil\n\t}()\n\treturn userChan, errChan\n}\n\n// GetUserFeedback returns feedback of a user from MongoDB.\nfunc (db *MongoDB) GetUserFeedback(ctx context.Context, userId string, endTime *time.Time, feedbackTypes ...expression.FeedbackTypeExpression) ([]Feedback, error) {\n\tc := db.client.Database(db.dbName).Collection(db.FeedbackTable())\n\tvar r *mongo.Cursor\n\tvar err error\n\tfilter := bson.M{\n\t\t\"feedbackkey.userid\": bson.M{\"$eq\": userId},\n\t}\n\tif endTime != nil {\n\t\tfilter[\"timestamp\"] = bson.M{\"$lte\": endTime}\n\t}\n\tif len(feedbackTypes) > 0 {\n\t\tvar conditions []bson.M\n\t\tfor _, feedbackType := range feedbackTypes {\n\t\t\tconditions = append(conditions, FeedbackTypeExpressionToMongo(feedbackType))\n\t\t}\n\t\tfilter[\"$or\"] = conditions\n\t}\n\tr, err = c.Find(ctx, filter)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfeedbacks := make([]Feedback, 0)\n\tdefer r.Close(ctx)\n\tfor r.Next(ctx) {\n\t\tvar feedback Feedback\n\t\tif err = r.Decode(&feedback); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfeedbacks = append(feedbacks, feedback)\n\t}\n\treturn feedbacks, nil\n}\n\n// BatchInsertFeedback returns multiple feedback into MongoDB.\nfunc (db *MongoDB) BatchInsertFeedback(ctx context.Context, feedback []Feedback, insertUser, insertItem, overwrite bool) error {\n\t// skip empty list\n\tif len(feedback) == 0 {\n\t\treturn nil\n\t}\n\t// collect users and items\n\tusers := mapset.NewSet[string]()\n\titems := mapset.NewSet[string]()\n\tfor _, v := range feedback {\n\t\tusers.Add(v.UserId)\n\t\titems.Add(v.ItemId)\n\t}\n\t// insert users\n\tuserList := users.ToSlice()\n\tif insertUser {\n\t\tvar models []mongo.WriteModel\n\t\tfor _, userId := range userList {\n\t\t\tmodels = append(models, mongo.NewUpdateOneModel().\n\t\t\t\tSetUpsert(true).\n\t\t\t\tSetFilter(bson.M{\"userid\": bson.M{\"$eq\": userId}}).\n\t\t\t\tSetUpdate(bson.M{\"$setOnInsert\": User{UserId: userId}}))\n\t\t}\n\t\tc := db.client.Database(db.dbName).Collection(db.UsersTable())\n\t\t_, err := c.BulkWrite(ctx, models)\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t} else {\n\t\tfor _, userId := range userList {\n\t\t\t_, err := db.GetUser(ctx, userId)\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, errors.NotFound) {\n\t\t\t\t\tusers.Remove(userId)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t}\n\t}\n\t// insert items\n\titemList := items.ToSlice()\n\tif insertItem {\n\t\tvar models []mongo.WriteModel\n\t\tfor _, itemId := range itemList {\n\t\t\tmodels = append(models, mongo.NewUpdateOneModel().\n\t\t\t\tSetUpsert(true).\n\t\t\t\tSetFilter(bson.M{\"itemid\": bson.M{\"$eq\": itemId}}).\n\t\t\t\tSetUpdate(bson.M{\"$setOnInsert\": Item{ItemId: itemId}}))\n\t\t}\n\t\tc := db.client.Database(db.dbName).Collection(db.ItemsTable())\n\t\t_, err := c.BulkWrite(ctx, models)\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t} else {\n\t\tfor _, itemId := range itemList {\n\t\t\t_, err := db.GetItem(ctx, itemId)\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, errors.NotFound) {\n\t\t\t\t\titems.Remove(itemId)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t}\n\t}\n\t// insert feedback\n\tc := db.client.Database(db.dbName).Collection(db.FeedbackTable())\n\tvar models []mongo.WriteModel\n\tfor _, f := range feedback {\n\t\tif users.Contains(f.UserId) && items.Contains(f.ItemId) {\n\t\t\tf.Updated = f.Timestamp\n\t\t\tmodel := mongo.NewUpdateOneModel().\n\t\t\t\tSetUpsert(true).\n\t\t\t\tSetFilter(bson.M{\n\t\t\t\t\t\"feedbackkey\": f.FeedbackKey,\n\t\t\t\t})\n\t\t\tif overwrite {\n\t\t\t\tmodel.SetUpdate(bson.M{\"$set\": f})\n\t\t\t} else {\n\t\t\t\tmodel.SetUpdate(bson.M{\n\t\t\t\t\t\"$setOnInsert\": bson.M{\n\t\t\t\t\t\t\"feedbackkey\": f.FeedbackKey,\n\t\t\t\t\t},\n\t\t\t\t\t\"$inc\": bson.M{\n\t\t\t\t\t\t\"value\": f.Value,\n\t\t\t\t\t},\n\t\t\t\t\t\"$min\": bson.M{\n\t\t\t\t\t\t\"timestamp\": f.Timestamp,\n\t\t\t\t\t},\n\t\t\t\t\t\"$max\": bson.M{\n\t\t\t\t\t\t\"updated\": f.Updated,\n\t\t\t\t\t},\n\t\t\t\t\t\"$set\": bson.M{\n\t\t\t\t\t\t\"comment\": f.Comment,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t\tmodels = append(models, model)\n\t\t}\n\t}\n\tif len(models) == 0 {\n\t\treturn nil\n\t}\n\t_, err := c.BulkWrite(ctx, models)\n\treturn errors.Trace(err)\n}\n\n// GetFeedback returns multiple feedback from MongoDB.\nfunc (db *MongoDB) GetFeedback(ctx context.Context, cursor string, n int, beginTime, endTime *time.Time, feedbackTypes ...string) (string, []Feedback, error) {\n\tbuf, err := base64.StdEncoding.DecodeString(cursor)\n\tif err != nil {\n\t\treturn \"\", nil, errors.Trace(err)\n\t}\n\tc := db.client.Database(db.dbName).Collection(db.FeedbackTable())\n\topt := options.Find()\n\topt.SetLimit(int64(n))\n\topt.SetSort(bson.D{{\"feedbackkey\", 1}})\n\tfilter := make(bson.M)\n\t// pass cursor to filter\n\tif len(buf) > 0 {\n\t\tfeedbackKey, err := feedbackKeyFromString(string(buf))\n\t\tif err != nil {\n\t\t\treturn \"\", nil, err\n\t\t}\n\t\tfilter[\"feedbackkey\"] = bson.M{\"$gt\": feedbackKey}\n\t}\n\t// pass feedback type to filter\n\tif len(feedbackTypes) > 0 {\n\t\tvar conditions []bson.M\n\t\tfor _, feedbackType := range feedbackTypes {\n\t\t\tconditions = append(conditions, bson.M{\n\t\t\t\t\"feedbackkey.feedbacktype\": bson.M{\"$eq\": feedbackType},\n\t\t\t})\n\t\t}\n\t\tfilter[\"$or\"] = conditions\n\t}\n\t// pass time limit to filter\n\ttimestampConditions := bson.M{}\n\tif beginTime != nil {\n\t\ttimestampConditions[\"$gt\"] = *beginTime\n\t}\n\tif endTime != nil {\n\t\ttimestampConditions[\"$lte\"] = *endTime\n\t}\n\tfilter[\"timestamp\"] = timestampConditions\n\tr, err := c.Find(ctx, filter, opt)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tfeedbacks := make([]Feedback, 0)\n\tdefer r.Close(ctx)\n\tfor r.Next(ctx) {\n\t\tvar feedback Feedback\n\t\tif err = r.Decode(&feedback); err != nil {\n\t\t\treturn \"\", nil, err\n\t\t}\n\t\tfeedbacks = append(feedbacks, feedback)\n\t}\n\tif len(feedbacks) == n {\n\t\tcursor, err = feedbacks[n-1].toString()\n\t\tif err != nil {\n\t\t\treturn \"\", nil, err\n\t\t}\n\t} else {\n\t\tcursor = \"\"\n\t}\n\treturn base64.StdEncoding.EncodeToString([]byte(cursor)), feedbacks, nil\n}\n\n// GetFeedbackStream reads feedback from MongoDB by stream.\nfunc (db *MongoDB) GetFeedbackStream(ctx context.Context, batchSize int, scanOptions ...ScanOption) (chan []Feedback, chan error) {\n\tscan := NewScanOptions(scanOptions...)\n\tfeedbackChan := make(chan []Feedback, bufSize)\n\terrChan := make(chan error, 1)\n\tgo func() {\n\t\tdefer close(feedbackChan)\n\t\tdefer close(errChan)\n\t\t// send query\n\t\tctx := context.Background()\n\t\tc := db.client.Database(db.dbName).Collection(db.FeedbackTable())\n\t\topt := options.Find()\n\t\tfilter := make(bson.M)\n\t\t// pass feedback type to filter\n\t\tif len(scan.FeedbackTypes) > 0 {\n\t\t\tvar conditions []bson.M\n\t\t\tfor _, feedbackType := range scan.FeedbackTypes {\n\t\t\t\tconditions = append(conditions, FeedbackTypeExpressionToMongo(feedbackType))\n\t\t\t}\n\t\t\tfilter[\"$or\"] = conditions\n\t\t}\n\t\t// pass time limit to filter\n\t\tif scan.BeginTime != nil || scan.EndTime != nil {\n\t\t\ttimestampConditions := bson.M{}\n\t\t\tif scan.BeginTime != nil {\n\t\t\t\ttimestampConditions[\"$gt\"] = *scan.BeginTime\n\t\t\t}\n\t\t\tif scan.EndTime != nil {\n\t\t\t\ttimestampConditions[\"$lte\"] = *scan.EndTime\n\t\t\t}\n\t\t\tfilter[\"timestamp\"] = timestampConditions\n\t\t}\n\t\t// pass user id to filter\n\t\tif scan.BeginUserId != nil || scan.EndUserId != nil {\n\t\t\tuserIdConditions := bson.M{}\n\t\t\tif scan.BeginUserId != nil {\n\t\t\t\tuserIdConditions[\"$gte\"] = *scan.BeginUserId\n\t\t\t}\n\t\t\tif scan.EndUserId != nil {\n\t\t\t\tuserIdConditions[\"$lte\"] = *scan.EndUserId\n\t\t\t}\n\t\t\tfilter[\"feedbackkey.userid\"] = userIdConditions\n\t\t}\n\t\tif scan.BeginItemId != nil || scan.EndItemId != nil {\n\t\t\titemIdConditions := bson.M{}\n\t\t\tif scan.BeginItemId != nil {\n\t\t\t\titemIdConditions[\"$gte\"] = *scan.BeginItemId\n\t\t\t}\n\t\t\tif scan.EndItemId != nil {\n\t\t\t\titemIdConditions[\"$lte\"] = *scan.EndItemId\n\t\t\t}\n\t\t\tfilter[\"feedbackkey.itemid\"] = itemIdConditions\n\t\t}\n\t\tif scan.OrderByItemId {\n\t\t\topt.SetSort(bson.D{{\"feedbackkey.itemid\", 1}})\n\t\t}\n\n\t\tr, err := c.Find(ctx, filter, opt)\n\t\tif err != nil {\n\t\t\terrChan <- errors.Trace(err)\n\t\t\treturn\n\t\t}\n\t\tfeedbacks := make([]Feedback, 0, batchSize)\n\t\tdefer r.Close(ctx)\n\t\tfor r.Next(ctx) {\n\t\t\tvar feedback Feedback\n\t\t\tif err = r.Decode(&feedback); err != nil {\n\t\t\t\terrChan <- errors.Trace(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfeedbacks = append(feedbacks, feedback)\n\t\t\tif len(feedbacks) == batchSize {\n\t\t\t\tfeedbackChan <- feedbacks\n\t\t\t\tfeedbacks = make([]Feedback, 0, batchSize)\n\t\t\t}\n\t\t}\n\t\tif len(feedbacks) > 0 {\n\t\t\tfeedbackChan <- feedbacks\n\t\t}\n\t\terrChan <- nil\n\t}()\n\treturn feedbackChan, errChan\n}\n\n// GetUserItemFeedback returns a feedback return the user id and item id from MongoDB.\nfunc (db *MongoDB) GetUserItemFeedback(ctx context.Context, userId, itemId string, feedbackTypes ...string) ([]Feedback, error) {\n\tc := db.client.Database(db.dbName).Collection(db.FeedbackTable())\n\tvar filter = bson.M{\n\t\t\"feedbackkey.userid\": bson.M{\"$eq\": userId},\n\t\t\"feedbackkey.itemid\": bson.M{\"$eq\": itemId},\n\t}\n\tif len(feedbackTypes) > 0 {\n\t\tvar conditions []bson.M\n\t\tfor _, feedbackType := range feedbackTypes {\n\t\t\tconditions = append(conditions, bson.M{\n\t\t\t\t\"feedbackkey.feedbacktype\": bson.M{\"$eq\": feedbackType},\n\t\t\t})\n\t\t}\n\t\tfilter[\"$or\"] = conditions\n\t}\n\tr, err := c.Find(ctx, filter)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfeedbacks := make([]Feedback, 0)\n\tdefer r.Close(ctx)\n\tfor r.Next(ctx) {\n\t\tvar feedback Feedback\n\t\tif err = r.Decode(&feedback); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfeedbacks = append(feedbacks, feedback)\n\t}\n\treturn feedbacks, nil\n}\n\n// DeleteUserItemFeedback deletes a feedback return the user id and item id from MongoDB.\nfunc (db *MongoDB) DeleteUserItemFeedback(ctx context.Context, userId, itemId string, feedbackTypes ...string) (int, error) {\n\tc := db.client.Database(db.dbName).Collection(db.FeedbackTable())\n\tvar filter = bson.M{\n\t\t\"feedbackkey.userid\": bson.M{\"$eq\": userId},\n\t\t\"feedbackkey.itemid\": bson.M{\"$eq\": itemId},\n\t}\n\tif len(feedbackTypes) > 0 {\n\t\tfilter[\"feedbackkey.feedbacktype\"] = bson.M{\"$in\": feedbackTypes}\n\t}\n\tr, err := c.DeleteMany(ctx, filter)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn int(r.DeletedCount), nil\n}\n\nfunc (db *MongoDB) CountUsers(ctx context.Context) (int, error) {\n\tn, err := db.client.Database(db.dbName).Collection(db.UsersTable()).EstimatedDocumentCount(ctx)\n\treturn int(n), err\n}\n\nfunc (db *MongoDB) CountItems(ctx context.Context) (int, error) {\n\tn, err := db.client.Database(db.dbName).Collection(db.ItemsTable()).EstimatedDocumentCount(ctx)\n\treturn int(n), err\n}\n\nfunc (db *MongoDB) CountFeedback(ctx context.Context) (int, error) {\n\tn, err := db.client.Database(db.dbName).Collection(db.FeedbackTable()).EstimatedDocumentCount(ctx)\n\treturn int(n), err\n}\n"
  },
  {
    "path": "storage/data/mongodb_test.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage data\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\nvar (\n\tmongoUri string\n)\n\nfunc init() {\n\t// get environment variables\n\tenv := func(key, defaultValue string) string {\n\t\tif value := os.Getenv(key); value != \"\" {\n\t\t\treturn value\n\t\t}\n\t\treturn defaultValue\n\t}\n\tmongoUri = env(\"MONGO_URI\", \"mongodb://root:password@127.0.0.1:27017/\")\n}\n\ntype MongoTestSuite struct {\n\tbaseTestSuite\n}\n\nfunc (suite *MongoTestSuite) SetupSuite() {\n\tctx := suite.T().Context()\n\tvar err error\n\t// create database\n\tsuite.Database, err = Open(mongoUri, \"gorse_\")\n\tsuite.NoError(err)\n\tdbName := \"gorse_data_test\"\n\tdatabaseComm := suite.getMongoDB()\n\terr = databaseComm.client.Database(dbName).Drop(ctx)\n\tif err == nil {\n\t\tsuite.T().Log(\"delete existed database:\", dbName)\n\t}\n\terr = suite.Database.Close()\n\tsuite.NoError(err)\n\t// create schema\n\tsuite.Database, err = Open(mongoUri+dbName+\"?authSource=admin&connect=direct\", \"gorse_\")\n\tsuite.NoError(err)\n\terr = suite.Database.Init()\n\tsuite.NoError(err)\n}\n\nfunc (suite *MongoTestSuite) getMongoDB() *MongoDB {\n\tvar mongoDatabase *MongoDB\n\tvar ok bool\n\tmongoDatabase, ok = suite.Database.(*MongoDB)\n\tsuite.True(ok)\n\treturn mongoDatabase\n}\n\nfunc TestMongo(t *testing.T) {\n\tsuite.Run(t, new(MongoTestSuite))\n}\n\nfunc BenchmarkMongo_CountItems(b *testing.B) {\n\tctx := b.Context()\n\tvar err error\n\n\t// create database\n\tdatabase, err := Open(mongoUri, \"gorse_\")\n\trequire.NoError(b, err)\n\tdbName := \"gorse_data_test\"\n\tdatabaseComm := database.(*MongoDB)\n\terr = databaseComm.client.Database(dbName).Drop(ctx)\n\trequire.NoError(b, err)\n\tdatabase, err = Open(mongoUri+dbName+\"?authSource=admin&connect=direct\", \"gorse_\")\n\trequire.NoError(b, err)\n\terr = database.Init()\n\trequire.NoError(b, err)\n\n\t// benchmark\n\tbenchmarkCountItems(b, database)\n\n\t// close database\n\terr = database.Close()\n\trequire.NoError(b, err)\n}\n"
  },
  {
    "path": "storage/data/no_database.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage data\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/common/expression\"\n)\n\n// NoDatabase means that no database used.\ntype NoDatabase struct{}\n\n// Optimize is used by ClickHouse only.\nfunc (NoDatabase) Optimize() error {\n\treturn ErrNoDatabase\n}\n\n// Init method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) Init() error {\n\treturn ErrNoDatabase\n}\n\nfunc (NoDatabase) Ping() error {\n\treturn ErrNoDatabase\n}\n\n// Close method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) Close() error {\n\treturn ErrNoDatabase\n}\n\nfunc (NoDatabase) Purge() error {\n\treturn ErrNoDatabase\n}\n\n// BatchInsertItems method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) BatchInsertItems(_ context.Context, _ []Item) error {\n\treturn ErrNoDatabase\n}\n\n// BatchGetItems method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) BatchGetItems(_ context.Context, _ []string) ([]Item, error) {\n\treturn nil, ErrNoDatabase\n}\n\n// DeleteItem method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) DeleteItem(_ context.Context, _ string) error {\n\treturn ErrNoDatabase\n}\n\n// GetItem method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) GetItem(_ context.Context, _ string) (Item, error) {\n\treturn Item{}, ErrNoDatabase\n}\n\n// GetItems method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) GetItems(_ context.Context, _ string, _ int, _ *time.Time) (string, []Item, error) {\n\treturn \"\", nil, ErrNoDatabase\n}\n\n// GetLatestItems method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) GetLatestItems(_ context.Context, _ int, _ []string) ([]Item, error) {\n\treturn nil, ErrNoDatabase\n}\n\n// GetItemStream method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) GetItemStream(_ context.Context, _ int, _ *time.Time) (chan []Item, chan error) {\n\titemChan := make(chan []Item, bufSize)\n\terrChan := make(chan error, 1)\n\tgo func() {\n\t\tdefer close(itemChan)\n\t\tdefer close(errChan)\n\t\terrChan <- ErrNoDatabase\n\t}()\n\treturn itemChan, errChan\n}\n\n// GetItemFeedback method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) GetItemFeedback(_ context.Context, _ string, _ ...string) ([]Feedback, error) {\n\treturn nil, ErrNoDatabase\n}\n\n// BatchInsertUsers method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) BatchInsertUsers(_ context.Context, _ []User) error {\n\treturn ErrNoDatabase\n}\n\n// DeleteUser method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) DeleteUser(_ context.Context, _ string) error {\n\treturn ErrNoDatabase\n}\n\n// GetUser method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) GetUser(_ context.Context, _ string) (User, error) {\n\treturn User{}, ErrNoDatabase\n}\n\n// GetUsers method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) GetUsers(_ context.Context, _ string, _ int) (string, []User, error) {\n\treturn \"\", nil, ErrNoDatabase\n}\n\n// GetUserStream method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) GetUserStream(_ context.Context, _ int) (chan []User, chan error) {\n\tuserChan := make(chan []User, bufSize)\n\terrChan := make(chan error, 1)\n\tgo func() {\n\t\tdefer close(userChan)\n\t\tdefer close(errChan)\n\t\terrChan <- ErrNoDatabase\n\t}()\n\treturn userChan, errChan\n}\n\n// GetUserFeedback method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) GetUserFeedback(context.Context, string, *time.Time, ...expression.FeedbackTypeExpression) ([]Feedback, error) {\n\treturn nil, ErrNoDatabase\n}\n\n// GetUserItemFeedback method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) GetUserItemFeedback(_ context.Context, _, _ string, _ ...string) ([]Feedback, error) {\n\treturn nil, ErrNoDatabase\n}\n\n// DeleteUserItemFeedback method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) DeleteUserItemFeedback(_ context.Context, _, _ string, _ ...string) (int, error) {\n\treturn 0, ErrNoDatabase\n}\n\n// BatchInsertFeedback method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) BatchInsertFeedback(_ context.Context, _ []Feedback, _, _, _ bool) error {\n\treturn ErrNoDatabase\n}\n\n// GetFeedback method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) GetFeedback(_ context.Context, _ string, _ int, _, _ *time.Time, _ ...string) (string, []Feedback, error) {\n\treturn \"\", nil, ErrNoDatabase\n}\n\n// GetFeedbackStream method of NoDatabase returns ErrNoDatabase.\nfunc (NoDatabase) GetFeedbackStream(_ context.Context, _ int, _ ...ScanOption) (chan []Feedback, chan error) {\n\tfeedbackChan := make(chan []Feedback, bufSize)\n\terrChan := make(chan error, 1)\n\tgo func() {\n\t\tdefer close(feedbackChan)\n\t\tdefer close(errChan)\n\t\terrChan <- ErrNoDatabase\n\t}()\n\treturn feedbackChan, errChan\n}\n\nfunc (d NoDatabase) ModifyItem(_ context.Context, _ string, _ ItemPatch) error {\n\treturn ErrNoDatabase\n}\n\nfunc (d NoDatabase) ModifyUser(_ context.Context, _ string, _ UserPatch) error {\n\treturn ErrNoDatabase\n}\n\nfunc (d NoDatabase) CountUsers(_ context.Context) (int, error) {\n\treturn 0, ErrNoDatabase\n}\n\nfunc (d NoDatabase) CountItems(_ context.Context) (int, error) {\n\treturn 0, ErrNoDatabase\n}\n\nfunc (d NoDatabase) CountFeedback(_ context.Context) (int, error) {\n\treturn 0, ErrNoDatabase\n}\n"
  },
  {
    "path": "storage/data/no_database_test.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage data\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/samber/lo\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNoDatabase(t *testing.T) {\n\tctx := t.Context()\n\tvar database NoDatabase\n\n\terr := database.Close()\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\terr = database.Optimize()\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\terr = database.Init()\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\terr = database.Ping()\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\terr = database.Purge()\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\n\terr = database.BatchInsertItems(ctx, nil)\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, err = database.BatchGetItems(ctx, nil)\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\terr = database.ModifyItem(ctx, \"\", ItemPatch{})\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, err = database.GetItem(ctx, \"\")\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, _, err = database.GetItems(ctx, \"\", 0, nil)\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\terr = database.DeleteItem(ctx, \"\")\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, c := database.GetItemStream(ctx, 0, nil)\n\tassert.ErrorIs(t, <-c, ErrNoDatabase)\n\n\terr = database.BatchInsertUsers(ctx, nil)\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, err = database.GetUser(ctx, \"\")\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\terr = database.ModifyUser(ctx, \"\", UserPatch{})\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, _, err = database.GetUsers(ctx, \"\", 0)\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\terr = database.DeleteUser(ctx, \"\")\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, c = database.GetUserStream(ctx, 0)\n\tassert.ErrorIs(t, <-c, ErrNoDatabase)\n\n\terr = database.BatchInsertFeedback(ctx, nil, false, false, false)\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\terr = database.BatchInsertFeedback(ctx, nil, false, false, false)\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, err = database.GetUserFeedback(ctx, \"\", lo.ToPtr(time.Now()))\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, err = database.GetItemFeedback(ctx, \"\")\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, _, err = database.GetFeedback(ctx, \"\", 0, nil, lo.ToPtr(time.Now()))\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, err = database.GetUserItemFeedback(ctx, \"\", \"\")\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, err = database.DeleteUserItemFeedback(ctx, \"\", \"\")\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, c = database.GetFeedbackStream(ctx, 0)\n\tassert.ErrorIs(t, <-c, ErrNoDatabase)\n\n\t_, err = database.CountUsers(ctx)\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, err = database.CountItems(ctx)\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n\t_, err = database.CountFeedback(ctx)\n\tassert.ErrorIs(t, err, ErrNoDatabase)\n}\n"
  },
  {
    "path": "storage/data/proxy.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage data\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/common/expression\"\n\t\"github.com/gorse-io/gorse/protocol\"\n\t\"github.com/juju/errors\"\n\t\"github.com/samber/lo\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n)\n\ntype ProxyServer struct {\n\tprotocol.UnimplementedDataStoreServer\n\tdatabase Database\n\tserver   *grpc.Server\n}\n\nfunc NewProxyServer(database Database) *ProxyServer {\n\treturn &ProxyServer{database: database}\n}\n\nfunc (p *ProxyServer) Serve(lis net.Listener) error {\n\tp.server = grpc.NewServer()\n\tprotocol.RegisterDataStoreServer(p.server, p)\n\treturn p.server.Serve(lis)\n}\n\nfunc (p *ProxyServer) Stop() {\n\tp.server.Stop()\n}\n\nfunc (p *ProxyServer) Ping(_ context.Context, _ *protocol.PingRequest) (*protocol.PingResponse, error) {\n\treturn &protocol.PingResponse{}, p.database.Ping()\n}\n\nfunc (p *ProxyServer) BatchInsertItems(ctx context.Context, in *protocol.BatchInsertItemsRequest) (*protocol.BatchInsertItemsResponse, error) {\n\titems := make([]Item, len(in.Items))\n\tfor i, item := range in.Items {\n\t\tvar labels any\n\t\terr := json.Unmarshal(item.Labels, &labels)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\titems[i] = Item{\n\t\t\tItemId:     item.ItemId,\n\t\t\tIsHidden:   item.IsHidden,\n\t\t\tCategories: item.Categories,\n\t\t\tTimestamp:  item.Timestamp.AsTime(),\n\t\t\tLabels:     labels,\n\t\t\tComment:    item.Comment,\n\t\t}\n\t}\n\terr := p.database.BatchInsertItems(ctx, items)\n\treturn &protocol.BatchInsertItemsResponse{}, err\n}\n\nfunc (p *ProxyServer) BatchGetItems(ctx context.Context, in *protocol.BatchGetItemsRequest) (*protocol.BatchGetItemsResponse, error) {\n\titems, err := p.database.BatchGetItems(ctx, in.ItemIds)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpbItems := make([]*protocol.Item, len(items))\n\tfor i, item := range items {\n\t\tlabels, err := json.Marshal(item.Labels)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpbItems[i] = &protocol.Item{\n\t\t\tItemId:     item.ItemId,\n\t\t\tIsHidden:   item.IsHidden,\n\t\t\tCategories: item.Categories,\n\t\t\tTimestamp:  timestamppb.New(item.Timestamp),\n\t\t\tLabels:     labels,\n\t\t\tComment:    item.Comment,\n\t\t}\n\t}\n\treturn &protocol.BatchGetItemsResponse{Items: pbItems}, nil\n}\n\nfunc (p *ProxyServer) DeleteItem(ctx context.Context, in *protocol.DeleteItemRequest) (*protocol.DeleteItemResponse, error) {\n\terr := p.database.DeleteItem(ctx, in.ItemId)\n\treturn &protocol.DeleteItemResponse{}, err\n}\n\nfunc (p *ProxyServer) GetItem(ctx context.Context, in *protocol.GetItemRequest) (*protocol.GetItemResponse, error) {\n\titem, err := p.database.GetItem(ctx, in.ItemId)\n\tif err != nil {\n\t\tif errors.Is(err, errors.NotFound) {\n\t\t\treturn &protocol.GetItemResponse{}, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\tlabels, err := json.Marshal(item.Labels)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &protocol.GetItemResponse{\n\t\tItem: &protocol.Item{\n\t\t\tItemId:     item.ItemId,\n\t\t\tIsHidden:   item.IsHidden,\n\t\t\tCategories: item.Categories,\n\t\t\tTimestamp:  timestamppb.New(item.Timestamp),\n\t\t\tLabels:     labels,\n\t\t\tComment:    item.Comment,\n\t\t},\n\t}, nil\n}\n\nfunc (p *ProxyServer) ModifyItem(ctx context.Context, in *protocol.ModifyItemRequest) (*protocol.ModifyItemResponse, error) {\n\tvar labels any\n\tif in.Patch.Labels != nil {\n\t\terr := json.Unmarshal(in.Patch.Labels, &labels)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tvar timestamp *time.Time\n\tif in.Patch.Timestamp != nil {\n\t\ttimestamp = lo.ToPtr(in.Patch.Timestamp.AsTime())\n\t}\n\terr := p.database.ModifyItem(ctx, in.ItemId, ItemPatch{\n\t\tIsHidden:   in.Patch.IsHidden,\n\t\tCategories: in.Patch.Categories,\n\t\tLabels:     labels,\n\t\tComment:    in.Patch.Comment,\n\t\tTimestamp:  timestamp,\n\t})\n\treturn &protocol.ModifyItemResponse{}, err\n}\n\nfunc (p *ProxyServer) GetItems(ctx context.Context, in *protocol.GetItemsRequest) (*protocol.GetItemsResponse, error) {\n\tvar beginTime *time.Time\n\tif in.BeginTime != nil {\n\t\tbeginTime = lo.ToPtr(in.BeginTime.AsTime())\n\t}\n\tcursor, items, err := p.database.GetItems(ctx, in.Cursor, int(in.N), beginTime)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpbItems := make([]*protocol.Item, len(items))\n\tfor i, item := range items {\n\t\tlabels, err := json.Marshal(item.Labels)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpbItems[i] = &protocol.Item{\n\t\t\tItemId:     item.ItemId,\n\t\t\tIsHidden:   item.IsHidden,\n\t\t\tCategories: item.Categories,\n\t\t\tTimestamp:  timestamppb.New(item.Timestamp),\n\t\t\tLabels:     labels,\n\t\t\tComment:    item.Comment,\n\t\t}\n\t}\n\treturn &protocol.GetItemsResponse{Cursor: cursor, Items: pbItems}, nil\n}\n\nfunc (p *ProxyServer) GetItemFeedback(ctx context.Context, in *protocol.GetItemFeedbackRequest) (*protocol.GetFeedbackResponse, error) {\n\tvar types []string\n\tfor _, t := range in.FeedbackTypes {\n\t\ttypes = append(types, t.FeedbackType)\n\t}\n\tfeedback, err := p.database.GetItemFeedback(ctx, in.ItemId, types...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpbFeedback := make([]*protocol.Feedback, len(feedback))\n\tfor i, f := range feedback {\n\t\tpbFeedback[i] = &protocol.Feedback{\n\t\t\tFeedbackType: f.FeedbackType,\n\t\t\tUserId:       f.UserId,\n\t\t\tItemId:       f.ItemId,\n\t\t\tValue:        f.Value,\n\t\t\tTimestamp:    timestamppb.New(f.Timestamp),\n\t\t\tUpdated:      timestamppb.New(f.Updated),\n\t\t\tComment:      f.Comment,\n\t\t}\n\t}\n\treturn &protocol.GetFeedbackResponse{Feedback: pbFeedback}, nil\n}\n\nfunc (p *ProxyServer) BatchInsertUsers(ctx context.Context, in *protocol.BatchInsertUsersRequest) (*protocol.BatchInsertUsersResponse, error) {\n\tusers := make([]User, len(in.Users))\n\tfor i, user := range in.Users {\n\t\tvar labels any\n\t\terr := json.Unmarshal(user.Labels, &labels)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tusers[i] = User{\n\t\t\tUserId:  user.UserId,\n\t\t\tLabels:  labels,\n\t\t\tComment: user.Comment,\n\t\t}\n\t}\n\terr := p.database.BatchInsertUsers(ctx, users)\n\treturn &protocol.BatchInsertUsersResponse{}, err\n}\n\nfunc (p *ProxyServer) DeleteUser(ctx context.Context, in *protocol.DeleteUserRequest) (*protocol.DeleteUserResponse, error) {\n\terr := p.database.DeleteUser(ctx, in.UserId)\n\treturn &protocol.DeleteUserResponse{}, err\n}\n\nfunc (p *ProxyServer) GetUser(ctx context.Context, in *protocol.GetUserRequest) (*protocol.GetUserResponse, error) {\n\tuser, err := p.database.GetUser(ctx, in.UserId)\n\tif err != nil {\n\t\tif errors.Is(err, errors.NotFound) {\n\t\t\treturn &protocol.GetUserResponse{}, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\tlabels, err := json.Marshal(user.Labels)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &protocol.GetUserResponse{\n\t\tUser: &protocol.User{\n\t\t\tUserId:  user.UserId,\n\t\t\tLabels:  labels,\n\t\t\tComment: user.Comment,\n\t\t},\n\t}, nil\n}\n\nfunc (p *ProxyServer) ModifyUser(ctx context.Context, in *protocol.ModifyUserRequest) (*protocol.ModifyUserResponse, error) {\n\tvar labels any\n\tif in.Patch.Labels != nil {\n\t\terr := json.Unmarshal(in.Patch.Labels, &labels)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\terr := p.database.ModifyUser(ctx, in.UserId, UserPatch{\n\t\tLabels:  labels,\n\t\tComment: in.Patch.Comment,\n\t})\n\treturn &protocol.ModifyUserResponse{}, err\n}\n\nfunc (p *ProxyServer) GetUsers(ctx context.Context, in *protocol.GetUsersRequest) (*protocol.GetUsersResponse, error) {\n\tcursor, users, err := p.database.GetUsers(ctx, in.Cursor, int(in.N))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpbUsers := make([]*protocol.User, len(users))\n\tfor i, user := range users {\n\t\tlabels, err := json.Marshal(user.Labels)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpbUsers[i] = &protocol.User{\n\t\t\tUserId:  user.UserId,\n\t\t\tLabels:  labels,\n\t\t\tComment: user.Comment,\n\t\t}\n\t}\n\treturn &protocol.GetUsersResponse{Cursor: cursor, Users: pbUsers}, nil\n}\n\nfunc (p *ProxyServer) GetUserFeedback(ctx context.Context, in *protocol.GetUserFeedbackRequest) (*protocol.GetFeedbackResponse, error) {\n\tvar endTime *time.Time\n\tif in.EndTime != nil {\n\t\tendTime = lo.ToPtr(in.EndTime.AsTime())\n\t}\n\ttypes := make([]expression.FeedbackTypeExpression, len(in.FeedbackTypes))\n\tfor i, t := range in.FeedbackTypes {\n\t\ttypes[i].FromPB(t)\n\t}\n\tfeedback, err := p.database.GetUserFeedback(ctx, in.UserId, endTime, types...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpbFeedback := make([]*protocol.Feedback, len(feedback))\n\tfor i, f := range feedback {\n\t\tpbFeedback[i] = &protocol.Feedback{\n\t\t\tFeedbackType: f.FeedbackType,\n\t\t\tUserId:       f.UserId,\n\t\t\tItemId:       f.ItemId,\n\t\t\tValue:        f.Value,\n\t\t\tTimestamp:    timestamppb.New(f.Timestamp),\n\t\t\tUpdated:      timestamppb.New(f.Updated),\n\t\t\tComment:      f.Comment,\n\t\t}\n\t}\n\treturn &protocol.GetFeedbackResponse{Feedback: pbFeedback}, nil\n}\n\nfunc (p *ProxyServer) GetUserItemFeedback(ctx context.Context, in *protocol.GetUserItemFeedbackRequest) (*protocol.GetFeedbackResponse, error) {\n\tvar types []string\n\tfor _, t := range in.FeedbackTypes {\n\t\ttypes = append(types, t.FeedbackType)\n\t}\n\tfeedback, err := p.database.GetUserItemFeedback(ctx, in.UserId, in.ItemId, types...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpbFeedback := make([]*protocol.Feedback, len(feedback))\n\tfor i, f := range feedback {\n\t\tpbFeedback[i] = &protocol.Feedback{\n\t\t\tFeedbackType: f.FeedbackType,\n\t\t\tUserId:       f.UserId,\n\t\t\tItemId:       f.ItemId,\n\t\t\tValue:        f.Value,\n\t\t\tTimestamp:    timestamppb.New(f.Timestamp),\n\t\t\tUpdated:      timestamppb.New(f.Updated),\n\t\t\tComment:      f.Comment,\n\t\t}\n\t}\n\treturn &protocol.GetFeedbackResponse{Feedback: pbFeedback}, nil\n}\n\nfunc (p *ProxyServer) DeleteUserItemFeedback(ctx context.Context, in *protocol.DeleteUserItemFeedbackRequest) (*protocol.DeleteUserItemFeedbackResponse, error) {\n\tcount, err := p.database.DeleteUserItemFeedback(ctx, in.UserId, in.ItemId, in.FeedbackTypes...)\n\treturn &protocol.DeleteUserItemFeedbackResponse{Count: int32(count)}, err\n}\n\nfunc (p *ProxyServer) BatchInsertFeedback(ctx context.Context, in *protocol.BatchInsertFeedbackRequest) (*protocol.BatchInsertFeedbackResponse, error) {\n\tfeedback := make([]Feedback, len(in.Feedback))\n\tfor i, f := range in.Feedback {\n\t\tfeedback[i] = Feedback{\n\t\t\tFeedbackKey: FeedbackKey{\n\t\t\t\tFeedbackType: f.FeedbackType,\n\t\t\t\tUserId:       f.UserId,\n\t\t\t\tItemId:       f.ItemId,\n\t\t\t},\n\t\t\tValue:     f.Value,\n\t\t\tTimestamp: f.Timestamp.AsTime(),\n\t\t\tComment:   f.Comment,\n\t\t}\n\t}\n\terr := p.database.BatchInsertFeedback(ctx, feedback, in.InsertUser, in.InsertItem, in.Overwrite)\n\treturn &protocol.BatchInsertFeedbackResponse{}, err\n}\n\nfunc (p *ProxyServer) GetFeedback(ctx context.Context, in *protocol.GetFeedbackRequest) (*protocol.GetFeedbackResponse, error) {\n\tvar beginTime, endTime *time.Time\n\tif in.BeginTime != nil {\n\t\tbeginTime = lo.ToPtr(in.BeginTime.AsTime())\n\t}\n\tif in.EndTime != nil {\n\t\tendTime = lo.ToPtr(in.EndTime.AsTime())\n\t}\n\tvar types []string\n\tfor _, t := range in.FeedbackTypes {\n\t\ttypes = append(types, t.FeedbackType)\n\t}\n\tcursor, feedback, err := p.database.GetFeedback(ctx, in.Cursor, int(in.N), beginTime, endTime, types...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpbFeedback := make([]*protocol.Feedback, len(feedback))\n\tfor i, f := range feedback {\n\t\tpbFeedback[i] = &protocol.Feedback{\n\t\t\tFeedbackType: f.FeedbackType,\n\t\t\tUserId:       f.UserId,\n\t\t\tItemId:       f.ItemId,\n\t\t\tValue:        f.Value,\n\t\t\tTimestamp:    timestamppb.New(f.Timestamp),\n\t\t\tUpdated:      timestamppb.New(f.Updated),\n\t\t\tComment:      f.Comment,\n\t\t}\n\t}\n\treturn &protocol.GetFeedbackResponse{Cursor: cursor, Feedback: pbFeedback}, nil\n}\n\nfunc (p *ProxyServer) GetUserStream(in *protocol.GetUserStreamRequest, stream grpc.ServerStreamingServer[protocol.GetUserStreamResponse]) error {\n\tusersChan, errChan := p.database.GetUserStream(stream.Context(), int(in.BatchSize))\n\tfor users := range usersChan {\n\t\tpbUsers := make([]*protocol.User, len(users))\n\t\tfor i, user := range users {\n\t\t\tlabels, err := json.Marshal(user.Labels)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tpbUsers[i] = &protocol.User{\n\t\t\t\tUserId:  user.UserId,\n\t\t\t\tLabels:  labels,\n\t\t\t\tComment: user.Comment,\n\t\t\t}\n\t\t}\n\t\terr := stream.Send(&protocol.GetUserStreamResponse{Users: pbUsers})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn <-errChan\n}\n\nfunc (p *ProxyServer) GetItemStream(in *protocol.GetItemStreamRequest, stream grpc.ServerStreamingServer[protocol.GetItemStreamResponse]) error {\n\tvar timeLimit *time.Time\n\tif in.TimeLimit != nil {\n\t\ttimeLimit = lo.ToPtr(in.TimeLimit.AsTime())\n\t}\n\titemsChan, errChan := p.database.GetItemStream(stream.Context(), int(in.BatchSize), timeLimit)\n\tfor items := range itemsChan {\n\t\tpbItems := make([]*protocol.Item, len(items))\n\t\tfor i, item := range items {\n\t\t\tlabels, err := json.Marshal(item.Labels)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tpbItems[i] = &protocol.Item{\n\t\t\t\tItemId:     item.ItemId,\n\t\t\t\tIsHidden:   item.IsHidden,\n\t\t\t\tCategories: item.Categories,\n\t\t\t\tTimestamp:  timestamppb.New(item.Timestamp),\n\t\t\t\tLabels:     labels,\n\t\t\t\tComment:    item.Comment,\n\t\t\t}\n\t\t}\n\t\terr := stream.Send(&protocol.GetItemStreamResponse{Items: pbItems})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn <-errChan\n}\n\nfunc (p *ProxyServer) GetFeedbackStream(in *protocol.GetFeedbackStreamRequest, stream grpc.ServerStreamingServer[protocol.GetFeedbackStreamResponse]) error {\n\tvar opts []ScanOption\n\tif in.ScanOptions.BeginTime != nil {\n\t\topts = append(opts, WithBeginTime(in.ScanOptions.BeginTime.AsTime()))\n\t}\n\tif in.ScanOptions.EndTime != nil {\n\t\topts = append(opts, WithEndTime(in.ScanOptions.EndTime.AsTime()))\n\t}\n\tif in.ScanOptions.FeedbackTypes != nil {\n\t\ttypes := make([]expression.FeedbackTypeExpression, len(in.ScanOptions.FeedbackTypes))\n\t\tfor i, t := range in.ScanOptions.FeedbackTypes {\n\t\t\ttypes[i].FromPB(t)\n\t\t}\n\t\topts = append(opts, WithFeedbackTypes(types...))\n\t}\n\tif in.ScanOptions.BeginUserId != nil {\n\t\topts = append(opts, WithBeginUserId(*in.ScanOptions.BeginUserId))\n\t}\n\tif in.ScanOptions.EndUserId != nil {\n\t\topts = append(opts, WithEndUserId(*in.ScanOptions.EndUserId))\n\t}\n\tif in.ScanOptions.BeginItemId != nil {\n\t\topts = append(opts, WithBeginItemId(*in.ScanOptions.BeginItemId))\n\t}\n\tif in.ScanOptions.EndItemId != nil {\n\t\topts = append(opts, WithEndItemId(*in.ScanOptions.EndItemId))\n\t}\n\tif in.ScanOptions.OrderByItemId {\n\t\topts = append(opts, WithOrderByItemId())\n\t}\n\tfeedbackChan, errChan := p.database.GetFeedbackStream(stream.Context(), int(in.BatchSize), opts...)\n\tfor feedback := range feedbackChan {\n\t\tpbFeedback := make([]*protocol.Feedback, len(feedback))\n\t\tfor i, f := range feedback {\n\t\t\tpbFeedback[i] = &protocol.Feedback{\n\t\t\t\tFeedbackType: f.FeedbackType,\n\t\t\t\tUserId:       f.UserId,\n\t\t\t\tItemId:       f.ItemId,\n\t\t\t\tValue:        f.Value,\n\t\t\t\tTimestamp:    timestamppb.New(f.Timestamp),\n\t\t\t\tUpdated:      timestamppb.New(f.Updated),\n\t\t\t\tComment:      f.Comment,\n\t\t\t}\n\t\t}\n\t\terr := stream.Send(&protocol.GetFeedbackStreamResponse{Feedback: pbFeedback})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn <-errChan\n}\n\nfunc (p *ProxyServer) CountUsers(ctx context.Context, in *protocol.CountUsersRequest) (*protocol.CountUsersResponse, error) {\n\tcount, err := p.database.CountUsers(ctx)\n\treturn &protocol.CountUsersResponse{Count: int32(count)}, err\n}\n\nfunc (p *ProxyServer) CountItems(ctx context.Context, in *protocol.CountItemsRequest) (*protocol.CountItemsResponse, error) {\n\tcount, err := p.database.CountItems(ctx)\n\treturn &protocol.CountItemsResponse{Count: int32(count)}, err\n}\n\nfunc (p *ProxyServer) CountFeedback(ctx context.Context, in *protocol.CountFeedbackRequest) (*protocol.CountFeedbackResponse, error) {\n\tcount, err := p.database.CountFeedback(ctx)\n\treturn &protocol.CountFeedbackResponse{Count: int32(count)}, err\n}\n\nfunc (p *ProxyServer) GetLatestItems(ctx context.Context, in *protocol.GetLatestItemsRequest) (*protocol.GetLatestItemsResponse, error) {\n\titems, err := p.database.GetLatestItems(ctx, int(in.N), in.Categories)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpbItems := make([]*protocol.Item, len(items))\n\tfor i, item := range items {\n\t\tlabels, err := json.Marshal(item.Labels)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpbItems[i] = &protocol.Item{\n\t\t\tItemId:     item.ItemId,\n\t\t\tIsHidden:   item.IsHidden,\n\t\t\tCategories: item.Categories,\n\t\t\tTimestamp:  timestamppb.New(item.Timestamp),\n\t\t\tLabels:     labels,\n\t\t\tComment:    item.Comment,\n\t\t}\n\t}\n\treturn &protocol.GetLatestItemsResponse{Items: pbItems}, nil\n}\n\ntype ProxyClient struct {\n\tprotocol.DataStoreClient\n}\n\nfunc NewProxyClient(conn *grpc.ClientConn) *ProxyClient {\n\treturn &ProxyClient{\n\t\tDataStoreClient: protocol.NewDataStoreClient(conn),\n\t}\n}\n\nfunc (p ProxyClient) Init() error {\n\treturn errors.MethodNotAllowedf(\"method Init is not allowed in ProxyClient\")\n}\n\nfunc (p ProxyClient) Ping() error {\n\t_, err := p.DataStoreClient.Ping(context.Background(), &protocol.PingRequest{})\n\treturn err\n}\n\nfunc (p ProxyClient) Close() error {\n\treturn nil\n}\n\nfunc (p ProxyClient) Optimize() error {\n\treturn nil\n}\n\nfunc (p ProxyClient) Purge() error {\n\treturn errors.MethodNotAllowedf(\"method Purge is not allowed in ProxyClient\")\n}\n\nfunc (p ProxyClient) BatchInsertItems(ctx context.Context, items []Item) error {\n\tpbItems := make([]*protocol.Item, len(items))\n\tfor i, item := range items {\n\t\tlabels, err := json.Marshal(item.Labels)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tpbItems[i] = &protocol.Item{\n\t\t\tItemId:     item.ItemId,\n\t\t\tIsHidden:   item.IsHidden,\n\t\t\tCategories: item.Categories,\n\t\t\tTimestamp:  timestamppb.New(item.Timestamp),\n\t\t\tLabels:     labels,\n\t\t\tComment:    item.Comment,\n\t\t}\n\t}\n\t_, err := p.DataStoreClient.BatchInsertItems(ctx, &protocol.BatchInsertItemsRequest{Items: pbItems})\n\treturn err\n}\n\nfunc (p ProxyClient) BatchGetItems(ctx context.Context, itemIds []string) ([]Item, error) {\n\tresp, err := p.DataStoreClient.BatchGetItems(ctx, &protocol.BatchGetItemsRequest{ItemIds: itemIds})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\titems := make([]Item, len(resp.Items))\n\tfor i, item := range resp.Items {\n\t\tvar labels any\n\t\terr = json.Unmarshal(item.Labels, &labels)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\titems[i] = Item{\n\t\t\tItemId:     item.ItemId,\n\t\t\tIsHidden:   item.IsHidden,\n\t\t\tCategories: item.Categories,\n\t\t\tTimestamp:  item.Timestamp.AsTime(),\n\t\t\tLabels:     labels,\n\t\t\tComment:    item.Comment,\n\t\t}\n\t}\n\treturn items, nil\n}\n\nfunc (p ProxyClient) DeleteItem(ctx context.Context, itemId string) error {\n\t_, err := p.DataStoreClient.DeleteItem(ctx, &protocol.DeleteItemRequest{ItemId: itemId})\n\treturn err\n}\n\nfunc (p ProxyClient) GetItem(ctx context.Context, itemId string) (Item, error) {\n\tresp, err := p.DataStoreClient.GetItem(ctx, &protocol.GetItemRequest{ItemId: itemId})\n\tif err != nil {\n\t\treturn Item{}, err\n\t}\n\tif resp.Item == nil {\n\t\treturn Item{}, errors.Annotate(ErrItemNotExist, itemId)\n\t}\n\tvar labels any\n\tif err = json.Unmarshal(resp.Item.Labels, &labels); err != nil {\n\t\treturn Item{}, err\n\t}\n\treturn Item{\n\t\tItemId:     resp.Item.ItemId,\n\t\tIsHidden:   resp.Item.IsHidden,\n\t\tCategories: resp.Item.Categories,\n\t\tTimestamp:  resp.Item.Timestamp.AsTime(),\n\t\tLabels:     labels,\n\t\tComment:    resp.Item.Comment,\n\t}, nil\n}\n\nfunc (p ProxyClient) GetLatestItems(ctx context.Context, n int, categories []string) ([]Item, error) {\n\tresp, err := p.DataStoreClient.GetLatestItems(ctx, &protocol.GetLatestItemsRequest{N: int32(n), Categories: categories})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\titems := make([]Item, len(resp.Items))\n\tfor i, item := range resp.Items {\n\t\tvar labels any\n\t\tif err = json.Unmarshal(item.Labels, &labels); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\titems[i] = Item{\n\t\t\tItemId:     item.ItemId,\n\t\t\tIsHidden:   item.IsHidden,\n\t\t\tCategories: item.Categories,\n\t\t\tTimestamp:  item.Timestamp.AsTime(),\n\t\t\tLabels:     labels,\n\t\t\tComment:    item.Comment,\n\t\t}\n\t}\n\treturn items, nil\n}\n\nfunc (p ProxyClient) ModifyItem(ctx context.Context, itemId string, patch ItemPatch) error {\n\tvar labels []byte\n\tif patch.Labels != nil {\n\t\tvar err error\n\t\tlabels, err = json.Marshal(patch.Labels)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tvar timestamp *timestamppb.Timestamp\n\tif patch.Timestamp != nil {\n\t\ttimestamp = timestamppb.New(*patch.Timestamp)\n\t}\n\t_, err := p.DataStoreClient.ModifyItem(ctx, &protocol.ModifyItemRequest{\n\t\tItemId: itemId,\n\t\tPatch: &protocol.ItemPatch{\n\t\t\tIsHidden:   patch.IsHidden,\n\t\t\tCategories: patch.Categories,\n\t\t\tLabels:     labels,\n\t\t\tComment:    patch.Comment,\n\t\t\tTimestamp:  timestamp,\n\t\t},\n\t})\n\treturn err\n}\n\nfunc (p ProxyClient) GetItems(ctx context.Context, cursor string, n int, beginTime *time.Time) (string, []Item, error) {\n\tvar beginTimeProto *timestamppb.Timestamp\n\tif beginTime != nil {\n\t\tbeginTimeProto = timestamppb.New(*beginTime)\n\t}\n\tresp, err := p.DataStoreClient.GetItems(ctx, &protocol.GetItemsRequest{Cursor: cursor, N: int32(n), BeginTime: beginTimeProto})\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\titems := make([]Item, len(resp.Items))\n\tfor i, item := range resp.Items {\n\t\tvar labels any\n\t\terr = json.Unmarshal(item.Labels, &labels)\n\t\tif err != nil {\n\t\t\treturn \"\", nil, err\n\t\t}\n\t\titems[i] = Item{\n\t\t\tItemId:     item.ItemId,\n\t\t\tIsHidden:   item.IsHidden,\n\t\t\tCategories: item.Categories,\n\t\t\tTimestamp:  item.Timestamp.AsTime(),\n\t\t\tLabels:     labels,\n\t\t\tComment:    item.Comment,\n\t\t}\n\t}\n\treturn resp.Cursor, items, nil\n}\n\nfunc (p ProxyClient) GetItemFeedback(ctx context.Context, itemId string, feedbackTypes ...string) ([]Feedback, error) {\n\tvar types []*protocol.FeedbackTypeExpression\n\tfor _, t := range feedbackTypes {\n\t\ttypes = append(types, &protocol.FeedbackTypeExpression{FeedbackType: t})\n\t}\n\tresp, err := p.DataStoreClient.GetItemFeedback(ctx, &protocol.GetItemFeedbackRequest{\n\t\tItemId:        itemId,\n\t\tFeedbackTypes: types,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfeedback := make([]Feedback, len(resp.Feedback))\n\tfor i, f := range resp.Feedback {\n\t\tfeedback[i] = Feedback{\n\t\t\tFeedbackKey: FeedbackKey{\n\t\t\t\tFeedbackType: f.FeedbackType,\n\t\t\t\tUserId:       f.UserId,\n\t\t\t\tItemId:       f.ItemId,\n\t\t\t},\n\t\t\tValue:     f.Value,\n\t\t\tTimestamp: f.Timestamp.AsTime(),\n\t\t\tUpdated:   f.Updated.AsTime(),\n\t\t\tComment:   f.Comment,\n\t\t}\n\t}\n\treturn feedback, nil\n}\n\nfunc (p ProxyClient) BatchInsertUsers(ctx context.Context, users []User) error {\n\tpbUsers := make([]*protocol.User, len(users))\n\tfor i, user := range users {\n\t\tlabels, err := json.Marshal(user.Labels)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tpbUsers[i] = &protocol.User{\n\t\t\tUserId:  user.UserId,\n\t\t\tLabels:  labels,\n\t\t\tComment: user.Comment,\n\t\t}\n\t}\n\t_, err := p.DataStoreClient.BatchInsertUsers(ctx, &protocol.BatchInsertUsersRequest{Users: pbUsers})\n\treturn err\n}\n\nfunc (p ProxyClient) DeleteUser(ctx context.Context, userId string) error {\n\t_, err := p.DataStoreClient.DeleteUser(ctx, &protocol.DeleteUserRequest{UserId: userId})\n\treturn err\n}\n\nfunc (p ProxyClient) GetUser(ctx context.Context, userId string) (User, error) {\n\tresp, err := p.DataStoreClient.GetUser(ctx, &protocol.GetUserRequest{UserId: userId})\n\tif err != nil {\n\t\treturn User{}, err\n\t}\n\tif resp.User == nil {\n\t\treturn User{}, errors.Annotate(ErrUserNotExist, userId)\n\t}\n\tvar labels any\n\tif err = json.Unmarshal(resp.User.Labels, &labels); err != nil {\n\t\treturn User{}, err\n\t}\n\treturn User{\n\t\tUserId:  resp.User.UserId,\n\t\tLabels:  labels,\n\t\tComment: resp.User.Comment,\n\t}, nil\n}\n\nfunc (p ProxyClient) ModifyUser(ctx context.Context, userId string, patch UserPatch) error {\n\tvar labels []byte\n\tif patch.Labels != nil {\n\t\tvar err error\n\t\tlabels, err = json.Marshal(patch.Labels)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t_, err := p.DataStoreClient.ModifyUser(ctx, &protocol.ModifyUserRequest{\n\t\tUserId: userId,\n\t\tPatch: &protocol.UserPatch{\n\t\t\tLabels:  labels,\n\t\t\tComment: patch.Comment,\n\t\t},\n\t})\n\treturn err\n}\n\nfunc (p ProxyClient) GetUsers(ctx context.Context, cursor string, n int) (string, []User, error) {\n\tresp, err := p.DataStoreClient.GetUsers(ctx, &protocol.GetUsersRequest{Cursor: cursor, N: int32(n)})\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tusers := make([]User, len(resp.Users))\n\tfor i, user := range resp.Users {\n\t\tvar labels any\n\t\terr = json.Unmarshal(user.Labels, &labels)\n\t\tif err != nil {\n\t\t\treturn \"\", nil, err\n\t\t}\n\t\tusers[i] = User{\n\t\t\tUserId:  user.UserId,\n\t\t\tLabels:  labels,\n\t\t\tComment: user.Comment,\n\t\t}\n\t}\n\treturn resp.Cursor, users, nil\n}\n\nfunc (p ProxyClient) GetUserFeedback(ctx context.Context, userId string, endTime *time.Time, feedbackTypes ...expression.FeedbackTypeExpression) ([]Feedback, error) {\n\treq := &protocol.GetUserFeedbackRequest{UserId: userId}\n\tif endTime != nil {\n\t\treq.EndTime = timestamppb.New(*endTime)\n\t}\n\tif len(feedbackTypes) > 0 {\n\t\tvar types []*protocol.FeedbackTypeExpression\n\t\tfor _, t := range feedbackTypes {\n\t\t\ttypes = append(types, t.ToPB())\n\t\t}\n\t\treq.FeedbackTypes = types\n\t}\n\tresp, err := p.DataStoreClient.GetUserFeedback(ctx, req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfeedback := make([]Feedback, len(resp.Feedback))\n\tfor i, f := range resp.Feedback {\n\t\tfeedback[i] = Feedback{\n\t\t\tFeedbackKey: FeedbackKey{\n\t\t\t\tFeedbackType: f.FeedbackType,\n\t\t\t\tUserId:       f.UserId,\n\t\t\t\tItemId:       f.ItemId,\n\t\t\t},\n\t\t\tValue:     f.Value,\n\t\t\tTimestamp: f.Timestamp.AsTime(),\n\t\t\tUpdated:   f.Updated.AsTime(),\n\t\t\tComment:   f.Comment,\n\t\t}\n\t}\n\treturn feedback, nil\n}\n\nfunc (p ProxyClient) GetUserItemFeedback(ctx context.Context, userId, itemId string, feedbackTypes ...string) ([]Feedback, error) {\n\tvar types []*protocol.FeedbackTypeExpression\n\tfor _, t := range feedbackTypes {\n\t\ttypes = append(types, &protocol.FeedbackTypeExpression{FeedbackType: t})\n\t}\n\tresp, err := p.DataStoreClient.GetUserItemFeedback(ctx, &protocol.GetUserItemFeedbackRequest{\n\t\tUserId:        userId,\n\t\tItemId:        itemId,\n\t\tFeedbackTypes: types,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfeedback := make([]Feedback, len(resp.Feedback))\n\tfor i, f := range resp.Feedback {\n\t\tfeedback[i] = Feedback{\n\t\t\tFeedbackKey: FeedbackKey{\n\t\t\t\tFeedbackType: f.FeedbackType,\n\t\t\t\tUserId:       f.UserId,\n\t\t\t\tItemId:       f.ItemId,\n\t\t\t},\n\t\t\tValue:     f.Value,\n\t\t\tTimestamp: f.Timestamp.AsTime(),\n\t\t\tUpdated:   f.Updated.AsTime(),\n\t\t\tComment:   f.Comment,\n\t\t}\n\t}\n\treturn feedback, nil\n}\n\nfunc (p ProxyClient) DeleteUserItemFeedback(ctx context.Context, userId, itemId string, feedbackTypes ...string) (int, error) {\n\tresp, err := p.DataStoreClient.DeleteUserItemFeedback(ctx, &protocol.DeleteUserItemFeedbackRequest{\n\t\tUserId:        userId,\n\t\tItemId:        itemId,\n\t\tFeedbackTypes: feedbackTypes,\n\t})\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn int(resp.Count), nil\n}\n\nfunc (p ProxyClient) BatchInsertFeedback(ctx context.Context, feedback []Feedback, insertUser, insertItem, overwrite bool) error {\n\treqFeedback := make([]*protocol.Feedback, len(feedback))\n\tfor i, f := range feedback {\n\t\treqFeedback[i] = &protocol.Feedback{\n\t\t\tFeedbackType: f.FeedbackType,\n\t\t\tUserId:       f.UserId,\n\t\t\tItemId:       f.ItemId,\n\t\t\tValue:        f.Value,\n\t\t\tTimestamp:    timestamppb.New(f.Timestamp),\n\t\t\tComment:      f.Comment,\n\t\t}\n\t}\n\t_, err := p.DataStoreClient.BatchInsertFeedback(ctx, &protocol.BatchInsertFeedbackRequest{\n\t\tFeedback:   reqFeedback,\n\t\tInsertUser: insertUser,\n\t\tInsertItem: insertItem,\n\t\tOverwrite:  overwrite,\n\t})\n\treturn err\n}\n\nfunc (p ProxyClient) GetFeedback(ctx context.Context, cursor string, n int, beginTime, endTime *time.Time, feedbackTypes ...string) (string, []Feedback, error) {\n\treq := &protocol.GetFeedbackRequest{\n\t\tCursor: cursor,\n\t\tN:      int32(n),\n\t}\n\tif beginTime != nil {\n\t\treq.BeginTime = timestamppb.New(*beginTime)\n\t}\n\tif endTime != nil {\n\t\treq.EndTime = timestamppb.New(*endTime)\n\t}\n\tif len(feedbackTypes) > 0 {\n\t\tvar types []*protocol.FeedbackTypeExpression\n\t\tfor _, t := range feedbackTypes {\n\t\t\ttypes = append(types, &protocol.FeedbackTypeExpression{FeedbackType: t})\n\t\t}\n\t\treq.FeedbackTypes = types\n\t}\n\tresp, err := p.DataStoreClient.GetFeedback(ctx, req)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tfeedback := make([]Feedback, len(resp.Feedback))\n\tfor i, f := range resp.Feedback {\n\t\tfeedback[i] = Feedback{\n\t\t\tFeedbackKey: FeedbackKey{\n\t\t\t\tFeedbackType: f.FeedbackType,\n\t\t\t\tUserId:       f.UserId,\n\t\t\t\tItemId:       f.ItemId,\n\t\t\t},\n\t\t\tValue:     f.Value,\n\t\t\tTimestamp: f.Timestamp.AsTime(),\n\t\t\tUpdated:   f.Updated.AsTime(),\n\t\t\tComment:   f.Comment,\n\t\t}\n\t}\n\treturn resp.Cursor, feedback, nil\n}\n\nfunc (p ProxyClient) GetUserStream(ctx context.Context, batchSize int) (chan []User, chan error) {\n\tusersChan := make(chan []User, bufSize)\n\terrChan := make(chan error, 1)\n\tgo func() {\n\t\tdefer close(usersChan)\n\t\tdefer close(errChan)\n\t\tstream, err := p.DataStoreClient.GetUserStream(ctx, &protocol.GetUserStreamRequest{BatchSize: int32(batchSize)})\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\tfor {\n\t\t\tresp, err := stream.Recv()\n\t\t\tif err != nil {\n\t\t\t\tif err == io.EOF {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\terrChan <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t\tusers := make([]User, len(resp.Users))\n\t\t\tfor i, user := range resp.Users {\n\t\t\t\tvar labels any\n\t\t\t\tif err = json.Unmarshal(user.Labels, &labels); err != nil {\n\t\t\t\t\terrChan <- err\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tusers[i] = User{\n\t\t\t\t\tUserId:  user.UserId,\n\t\t\t\t\tLabels:  labels,\n\t\t\t\t\tComment: user.Comment,\n\t\t\t\t}\n\t\t\t}\n\t\t\tusersChan <- users\n\t\t}\n\t}()\n\treturn usersChan, errChan\n}\n\nfunc (p ProxyClient) GetItemStream(ctx context.Context, batchSize int, timeLimit *time.Time) (chan []Item, chan error) {\n\titemsChan := make(chan []Item, bufSize)\n\terrChan := make(chan error, 1)\n\tgo func() {\n\t\tdefer close(itemsChan)\n\t\tdefer close(errChan)\n\t\tstream, err := p.DataStoreClient.GetItemStream(ctx, &protocol.GetItemStreamRequest{BatchSize: int32(batchSize)})\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\tfor {\n\t\t\tresp, err := stream.Recv()\n\t\t\tif err != nil {\n\t\t\t\tif err == io.EOF {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\terrChan <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t\titems := make([]Item, len(resp.Items))\n\t\t\tfor i, item := range resp.Items {\n\t\t\t\tvar labels any\n\t\t\t\tif err = json.Unmarshal(item.Labels, &labels); err != nil {\n\t\t\t\t\terrChan <- err\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\titems[i] = Item{\n\t\t\t\t\tItemId:     item.ItemId,\n\t\t\t\t\tIsHidden:   item.IsHidden,\n\t\t\t\t\tCategories: item.Categories,\n\t\t\t\t\tTimestamp:  item.Timestamp.AsTime(),\n\t\t\t\t\tLabels:     labels,\n\t\t\t\t\tComment:    item.Comment,\n\t\t\t\t}\n\t\t\t}\n\t\t\titemsChan <- items\n\t\t}\n\t}()\n\treturn itemsChan, errChan\n}\n\nfunc (p ProxyClient) GetFeedbackStream(ctx context.Context, batchSize int, options ...ScanOption) (chan []Feedback, chan error) {\n\tvar o ScanOptions\n\tfor _, opt := range options {\n\t\topt(&o)\n\t}\n\tvar types []*protocol.FeedbackTypeExpression\n\tfor _, t := range o.FeedbackTypes {\n\t\ttypes = append(types, t.ToPB())\n\t}\n\tpbOptions := &protocol.ScanOptions{\n\t\tBeginUserId:   o.BeginUserId,\n\t\tEndUserId:     o.EndUserId,\n\t\tBeginItemId:   o.BeginItemId,\n\t\tEndItemId:     o.EndItemId,\n\t\tFeedbackTypes: types,\n\t\tOrderByItemId: o.OrderByItemId,\n\t}\n\tif o.BeginTime != nil {\n\t\tpbOptions.BeginTime = timestamppb.New(*o.BeginTime)\n\t}\n\tif o.EndTime != nil {\n\t\tpbOptions.EndTime = timestamppb.New(*o.EndTime)\n\t}\n\n\tfeedbackChan := make(chan []Feedback, bufSize)\n\terrChan := make(chan error, 1)\n\tgo func() {\n\t\tdefer close(feedbackChan)\n\t\tdefer close(errChan)\n\t\treq := &protocol.GetFeedbackStreamRequest{\n\t\t\tBatchSize:   int32(batchSize),\n\t\t\tScanOptions: pbOptions,\n\t\t}\n\n\t\tstream, err := p.DataStoreClient.GetFeedbackStream(ctx, req)\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\t\tfor {\n\t\t\tresp, err := stream.Recv()\n\t\t\tif err != nil {\n\t\t\t\tif err == io.EOF {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\terrChan <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfeedback := make([]Feedback, len(resp.Feedback))\n\t\t\tfor i, f := range resp.Feedback {\n\t\t\t\tfeedback[i] = Feedback{\n\t\t\t\t\tFeedbackKey: FeedbackKey{\n\t\t\t\t\t\tFeedbackType: f.FeedbackType,\n\t\t\t\t\t\tUserId:       f.UserId,\n\t\t\t\t\t\tItemId:       f.ItemId,\n\t\t\t\t\t},\n\t\t\t\t\tValue:     f.Value,\n\t\t\t\t\tTimestamp: f.Timestamp.AsTime(),\n\t\t\t\t\tUpdated:   f.Updated.AsTime(),\n\t\t\t\t\tComment:   f.Comment,\n\t\t\t\t}\n\t\t\t}\n\t\t\tfeedbackChan <- feedback\n\t\t}\n\t}()\n\treturn feedbackChan, errChan\n}\n\nfunc (p ProxyClient) CountUsers(ctx context.Context) (int, error) {\n\tresp, err := p.DataStoreClient.CountUsers(ctx, &protocol.CountUsersRequest{})\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn int(resp.Count), nil\n}\n\nfunc (p ProxyClient) CountItems(ctx context.Context) (int, error) {\n\tresp, err := p.DataStoreClient.CountItems(ctx, &protocol.CountItemsRequest{})\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn int(resp.Count), nil\n}\n\nfunc (p ProxyClient) CountFeedback(ctx context.Context) (int, error) {\n\tresp, err := p.DataStoreClient.CountFeedback(ctx, &protocol.CountFeedbackRequest{})\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn int(resp.Count), nil\n}\n"
  },
  {
    "path": "storage/data/proxy_test.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage data\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/suite\"\n\t\"google.golang.org/grpc\"\n)\n\ntype ProxyTestSuite struct {\n\tbaseTestSuite\n\tsqlite     Database\n\tserver     *ProxyServer\n\tclientConn *grpc.ClientConn\n}\n\nfunc (suite *ProxyTestSuite) SetupSuite() {\n\t// create database\n\tvar err error\n\tpath := fmt.Sprintf(\"sqlite://%s/sqlite.db\", suite.T().TempDir())\n\tsuite.sqlite, err = Open(path, \"gorse_\")\n\tsuite.NoError(err)\n\t// create schema\n\terr = suite.sqlite.Init()\n\tsuite.NoError(err)\n\t// start server\n\tlis, err := net.Listen(\"tcp\", \"localhost:0\")\n\tsuite.NoError(err)\n\tsuite.server = NewProxyServer(suite.sqlite)\n\tgo func() {\n\t\terr = suite.server.Serve(lis)\n\t\tsuite.NoError(err)\n\t}()\n\t// create proxy client\n\tsuite.clientConn, err = grpc.Dial(lis.Addr().String(), grpc.WithInsecure())\n\tsuite.NoError(err)\n\tsuite.Database = NewProxyClient(suite.clientConn)\n}\n\nfunc (suite *ProxyTestSuite) TearDownSuite() {\n\tsuite.server.Stop()\n\tsuite.NoError(suite.clientConn.Close())\n\tsuite.NoError(suite.sqlite.Close())\n}\n\nfunc (suite *ProxyTestSuite) SetupTest() {\n\terr := suite.sqlite.Ping()\n\tsuite.NoError(err)\n\terr = suite.sqlite.Purge()\n\tsuite.NoError(err)\n}\n\nfunc (suite *ProxyTestSuite) TearDownTest() {\n\terr := suite.sqlite.Purge()\n\tsuite.NoError(err)\n}\n\nfunc (suite *ProxyTestSuite) TestInit() {\n\tsuite.T().Skip()\n}\n\nfunc (suite *ProxyTestSuite) TestPurge() {\n\tsuite.T().Skip()\n}\n\nfunc TestProxy(t *testing.T) {\n\tsuite.Run(t, new(ProxyTestSuite))\n}\n"
  },
  {
    "path": "storage/data/sql.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage data\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/XSAM/otelsql\"\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\t\"github.com/gorse-io/gorse/common/expression\"\n\t\"github.com/gorse-io/gorse/common/jsonutil\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/storage\"\n\t\"github.com/juju/errors\"\n\t_ \"github.com/lib/pq\"\n\t_ \"github.com/mailru/go-clickhouse/v2\"\n\t\"github.com/samber/lo\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.12.0\"\n\t\"gorm.io/driver/clickhouse\"\n\t\"gorm.io/driver/mysql\"\n\t\"gorm.io/driver/postgres\"\n\t\"gorm.io/driver/sqlite\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n\t_ \"modernc.org/sqlite\"\n)\n\nconst bufSize = 1\n\nfunc init() {\n\tRegister([]string{storage.MySQLPrefix}, func(path, tablePrefix string, opts ...storage.Option) (Database, error) {\n\t\tname := path[len(storage.MySQLPrefix):]\n\t\toption := storage.NewOptions(opts...)\n\t\t// probe isolation variable name\n\t\tisolationVarName, err := storage.ProbeMySQLIsolationVariableName(name)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\t// append parameters\n\t\tif name, err = storage.AppendMySQLParams(name, map[string]string{\n\t\t\t\"sql_mode\":       \"'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'\",\n\t\t\tisolationVarName: fmt.Sprintf(\"'%s'\", option.IsolationLevel),\n\t\t\t\"parseTime\":      \"true\",\n\t\t}); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\t// connect to database\n\t\tdatabase := new(SQLDatabase)\n\t\tdatabase.driver = MySQL\n\t\tdatabase.TablePrefix = storage.TablePrefix(tablePrefix)\n\t\tif database.client, err = otelsql.Open(\"mysql\", name,\n\t\t\totelsql.WithAttributes(semconv.DBSystemMySQL),\n\t\t\totelsql.WithSpanOptions(otelsql.SpanOptions{DisableErrSkip: true}),\n\t\t); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tstorage.ApplySQLPool(database.client, option)\n\t\tdatabase.gormDB, err = gorm.Open(mysql.New(mysql.Config{Conn: database.client}), storage.NewGORMConfig(tablePrefix))\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\treturn database, nil\n\t})\n\tRegister([]string{storage.PostgresPrefix, storage.PostgreSQLPrefix}, func(path, tablePrefix string, opts ...storage.Option) (Database, error) {\n\t\tdatabase := new(SQLDatabase)\n\t\tdatabase.driver = Postgres\n\t\tdatabase.TablePrefix = storage.TablePrefix(tablePrefix)\n\t\toption := storage.NewOptions(opts...)\n\t\tvar err error\n\t\tif database.client, err = otelsql.Open(\"postgres\", path,\n\t\t\totelsql.WithAttributes(semconv.DBSystemPostgreSQL),\n\t\t\totelsql.WithSpanOptions(otelsql.SpanOptions{DisableErrSkip: true}),\n\t\t); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tstorage.ApplySQLPool(database.client, option)\n\t\tdatabase.gormDB, err = gorm.Open(postgres.New(postgres.Config{Conn: database.client}), storage.NewGORMConfig(tablePrefix))\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\treturn database, nil\n\t})\n\tRegister([]string{storage.ClickhousePrefix, storage.CHHTTPPrefix, storage.CHHTTPSPrefix}, func(path, tablePrefix string, opts ...storage.Option) (Database, error) {\n\t\t// replace schema\n\t\tparsed, err := url.Parse(path)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tif strings.HasPrefix(path, storage.CHHTTPSPrefix) {\n\t\t\tparsed.Scheme = \"https\"\n\t\t} else {\n\t\t\tparsed.Scheme = \"http\"\n\t\t}\n\t\turi := parsed.String()\n\t\tdatabase := new(SQLDatabase)\n\t\tdatabase.driver = ClickHouse\n\t\tdatabase.TablePrefix = storage.TablePrefix(tablePrefix)\n\t\tif database.client, err = otelsql.Open(\"chhttp\", uri,\n\t\t\totelsql.WithAttributes(semconv.DBSystemKey.String(\"clickhouse\")),\n\t\t\totelsql.WithSpanOptions(otelsql.SpanOptions{DisableErrSkip: true}),\n\t\t); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tdatabase.gormDB, err = gorm.Open(clickhouse.New(clickhouse.Config{Conn: database.client}), storage.NewGORMConfig(tablePrefix))\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\treturn database, nil\n\t})\n\tRegister([]string{storage.SQLitePrefix}, func(path, tablePrefix string, opts ...storage.Option) (Database, error) {\n\t\tdataSourceName := path[len(storage.SQLitePrefix):]\n\t\t// append parameters\n\t\tvar err error\n\t\tif dataSourceName, err = storage.AppendURLParams(dataSourceName, []lo.Tuple2[string, string]{\n\t\t\t{\"_pragma\", \"busy_timeout(10000)\"},\n\t\t\t{\"_pragma\", \"journal_mode(wal)\"},\n\t\t}); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\t// connect to database\n\t\tdatabase := new(SQLDatabase)\n\t\tdatabase.driver = SQLite\n\t\tdatabase.TablePrefix = storage.TablePrefix(tablePrefix)\n\t\tif database.client, err = otelsql.Open(\"sqlite\", dataSourceName,\n\t\t\totelsql.WithAttributes(semconv.DBSystemSqlite),\n\t\t\totelsql.WithSpanOptions(otelsql.SpanOptions{DisableErrSkip: true}),\n\t\t); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tdatabase.gormDB, err = gorm.Open(sqlite.Dialector{Conn: database.client}, storage.NewGORMConfig(tablePrefix))\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\treturn database, nil\n\t})\n}\n\ntype SQLDriver int\n\nconst (\n\tMySQL SQLDriver = iota\n\tPostgres\n\tClickHouse\n\tSQLite\n)\n\ntype SQLItem struct {\n\tItemId     string    `gorm:\"column:item_id;primaryKey\"`\n\tIsHidden   bool      `gorm:\"column:is_hidden\"`\n\tCategories string    `gorm:\"column:categories\"`\n\tTimestamp  time.Time `gorm:\"column:time_stamp\"`\n\tLabels     string    `gorm:\"column:labels\"`\n\tComment    string    `gorm:\"column:comment\"`\n}\n\nfunc NewSQLItem(item Item) (sqlItem SQLItem) {\n\tvar buf []byte\n\tsqlItem.ItemId = item.ItemId\n\tsqlItem.IsHidden = item.IsHidden\n\tbuf, _ = jsonutil.Marshal(item.Categories)\n\tsqlItem.Categories = string(buf)\n\tsqlItem.Timestamp = item.Timestamp\n\tbuf, _ = jsonutil.Marshal(item.Labels)\n\tsqlItem.Labels = string(buf)\n\tsqlItem.Comment = item.Comment\n\treturn\n}\n\ntype SQLUser struct {\n\tUserId  string `gorm:\"column:user_id;primaryKey\"`\n\tLabels  string `gorm:\"column:labels\"`\n\tComment string `gorm:\"column:comment\"`\n}\n\nfunc NewSQLUser(user User) (sqlUser SQLUser) {\n\tvar buf []byte\n\tsqlUser.UserId = user.UserId\n\tbuf, _ = jsonutil.Marshal(user.Labels)\n\tsqlUser.Labels = string(buf)\n\tsqlUser.Comment = user.Comment\n\treturn\n}\n\ntype ClickHouseItem struct {\n\tSQLItem `gorm:\"embedded\"`\n\tVersion time.Time `gorm:\"column:version\"`\n}\n\nfunc NewClickHouseItem(item Item) (clickHouseItem ClickHouseItem) {\n\tclickHouseItem.SQLItem = NewSQLItem(item)\n\tclickHouseItem.Timestamp = item.Timestamp.In(time.UTC)\n\tclickHouseItem.Version = time.Now().In(time.UTC)\n\treturn\n}\n\ntype ClickhouseUser struct {\n\tSQLUser `gorm:\"embedded\"`\n\tVersion time.Time `gorm:\"column:version\"`\n}\n\nfunc NewClickhouseUser(user User) (clickhouseUser ClickhouseUser) {\n\tclickhouseUser.SQLUser = NewSQLUser(user)\n\tclickhouseUser.Version = time.Now().In(time.UTC)\n\treturn\n}\n\nfunc FeedbackTypeExpressionToSQL(db *gorm.DB, e expression.FeedbackTypeExpression) *gorm.DB {\n\tswitch e.ExprType {\n\tcase expression.Less:\n\t\treturn db.Or(\"feedback_type = ? AND value < ?\", e.FeedbackType, e.Value)\n\tcase expression.LessOrEqual:\n\t\treturn db.Or(\"feedback_type = ? AND value <= ?\", e.FeedbackType, e.Value)\n\tcase expression.Greater:\n\t\treturn db.Or(\"feedback_type = ? AND value > ?\", e.FeedbackType, e.Value)\n\tcase expression.GreaterOrEqual:\n\t\treturn db.Or(\"feedback_type = ? AND value >= ?\", e.FeedbackType, e.Value)\n\tdefault:\n\t\treturn db.Or(\"feedback_type = ?\", e.FeedbackType)\n\t}\n}\n\n// SQLDatabase use MySQL as data storage.\ntype SQLDatabase struct {\n\tstorage.TablePrefix\n\tgormDB *gorm.DB\n\tclient *sql.DB\n\tdriver SQLDriver\n}\n\n// Optimize is used by ClickHouse only.\nfunc (d *SQLDatabase) Optimize() error {\n\tif d.driver == ClickHouse {\n\t\tfor _, tableName := range []string{d.UsersTable(), d.ItemsTable(), d.FeedbackTable(),\n\t\t\td.AggregatingFeedbackTable(), d.UserFeedbackTable(), d.ItemFeedbackTable(), d.LatestItemsTable()} {\n\t\t\t_, err := d.client.Exec(\"OPTIMIZE TABLE \" + tableName)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// Init tables and indices in MySQL.\nfunc (d *SQLDatabase) Init() error {\n\tswitch d.driver {\n\tcase MySQL:\n\t\t// create tables\n\t\ttype Items struct {\n\t\t\tItemId     string    `gorm:\"column:item_id;type:varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin not null;primaryKey\"`\n\t\t\tIsHidden   bool      `gorm:\"column:is_hidden;type:bool;not null\"`\n\t\t\tCategories []string  `gorm:\"column:categories;type:json;not null\"`\n\t\t\tTimestamp  time.Time `gorm:\"column:time_stamp;type:datetime;not null;index:time_stamp_index\"`\n\t\t\tLabels     []string  `gorm:\"column:labels;type:json;not null\"`\n\t\t\tComment    string    `gorm:\"column:comment;type:text;not null\"`\n\t\t}\n\t\ttype Users struct {\n\t\t\tUserId  string   `gorm:\"column:user_id;type:varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin not null;primaryKey\"`\n\t\t\tLabels  []string `gorm:\"column:labels;type:json;not null\"`\n\t\t\tComment string   `gorm:\"column:comment;type:text;not null\"`\n\t\t}\n\t\ttype Feedback struct {\n\t\t\tFeedbackType string    `gorm:\"column:feedback_type;type:varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin not null;primaryKey\"`\n\t\t\tUserId       string    `gorm:\"column:user_id;type:varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin not null;primaryKey;index:user_id\"`\n\t\t\tItemId       string    `gorm:\"column:item_id;type:varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin not null;primaryKey;index:item_id\"`\n\t\t\tValue        float64   `gorm:\"column:value;type:float;not null;default:0\"`\n\t\t\tTimestamp    time.Time `gorm:\"column:time_stamp;type:datetime;not null\"`\n\t\t\tUpdated      time.Time `gorm:\"column:updated;type:datetime;not null;default:'2000-01-01 00:00:00'\"`\n\t\t\tComment      string    `gorm:\"column:comment;type:text;not null\"`\n\t\t}\n\t\terr := d.gormDB.Set(\"gorm:table_options\", \"ENGINE=InnoDB\").AutoMigrate(Users{}, Items{}, Feedback{})\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\tcase Postgres:\n\t\t// create tables\n\t\ttype Items struct {\n\t\t\tItemId     string    `gorm:\"column:item_id;type:varchar(256) COLLATE \\\"C\\\";not null;primaryKey\"`\n\t\t\tIsHidden   bool      `gorm:\"column:is_hidden;type:bool;not null;default:false\"`\n\t\t\tCategories string    `gorm:\"column:categories;type:json;not null;default:'[]'\"`\n\t\t\tTimestamp  time.Time `gorm:\"column:time_stamp;type:timestamptz;not null;index:time_stamp_index\"`\n\t\t\tLabels     string    `gorm:\"column:labels;type:json;not null;default:'[]'\"`\n\t\t\tComment    string    `gorm:\"column:comment;type:text;not null;default:''\"`\n\t\t}\n\t\ttype Users struct {\n\t\t\tUserId  string `gorm:\"column:user_id;type:varchar(256) COLLATE \\\"C\\\" not null;primaryKey\"`\n\t\t\tLabels  string `gorm:\"column:labels;type:json;not null;default:'[]'\"`\n\t\t\tComment string `gorm:\"column:comment;type:text;not null;default:''\"`\n\t\t}\n\t\ttype Feedback struct {\n\t\t\tFeedbackType string    `gorm:\"column:feedback_type;type:varchar(256) COLLATE \\\"C\\\";not null;primaryKey\"`\n\t\t\tUserId       string    `gorm:\"column:user_id;type:varchar(256) COLLATE \\\"C\\\";not null;primaryKey;index:user_id_index\"`\n\t\t\tItemId       string    `gorm:\"column:item_id;type:varchar(256) COLLATE \\\"C\\\";not null;primaryKey;index:item_id_index\"`\n\t\t\tValue        float64   `gorm:\"column:value;type:float8;not null;default:0\"`\n\t\t\tTimestamp    time.Time `gorm:\"column:time_stamp;type:timestamptz;not null\"`\n\t\t\tUpdated      time.Time `gorm:\"column:updated;type:timestamptz;not null;default:'2000-01-01 00:00:00'\"`\n\t\t\tComment      string    `gorm:\"column:comment;type:text;not null;default:''\"`\n\t\t}\n\t\terr := d.gormDB.AutoMigrate(Users{}, Items{}, Feedback{})\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\tcase SQLite:\n\t\t// create tables\n\t\ttype Items struct {\n\t\t\tItemId     string `gorm:\"column:item_id;type:varchar(256);not null;primaryKey\"`\n\t\t\tIsHidden   bool   `gorm:\"column:is_hidden;type:bool;not null;default:false\"`\n\t\t\tCategories string `gorm:\"column:categories;type:json;not null;default:'[]'\"`\n\t\t\tTimestamp  string `gorm:\"column:time_stamp;type:datetime;not null;default:'0001-01-01';index:time_stamp_index\"`\n\t\t\tLabels     string `gorm:\"column:labels;type:json;not null;default:'[]'\"`\n\t\t\tComment    string `gorm:\"column:comment;type:text;not null;default:''\"`\n\t\t}\n\t\ttype Users struct {\n\t\t\tUserId  string `gorm:\"column:user_id;type:varchar(256) not null;primaryKey\"`\n\t\t\tLabels  string `gorm:\"column:labels;type:json;not null;default:'null'\"`\n\t\t\tComment string `gorm:\"column:comment;type:text;not null;default:''\"`\n\t\t}\n\t\ttype Feedback struct {\n\t\t\tFeedbackType string  `gorm:\"column:feedback_type;type:varchar(256);not null;primaryKey\"`\n\t\t\tUserId       string  `gorm:\"column:user_id;type:varchar(256);not null;primaryKey;index:user_id_index\"`\n\t\t\tItemId       string  `gorm:\"column:item_id;type:varchar(256);not null;primaryKey;index:item_id_index\"`\n\t\t\tValue        float64 `gorm:\"column:value;type:real;not null;default:0\"`\n\t\t\tTimestamp    string  `gorm:\"column:time_stamp;type:datetime;not null;default:'0001-01-01'\"`\n\t\t\tUpdated      string  `gorm:\"column:updated;type:datetime;not null;default:'0001-01-01'\"`\n\t\t\tComment      string  `gorm:\"column:comment;type:text;not null;default:''\"`\n\t\t}\n\t\terr := d.gormDB.AutoMigrate(Users{}, Items{}, Feedback{})\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\tcase ClickHouse:\n\t\t// create tables\n\t\ttype Items struct {\n\t\t\tItemId     string    `gorm:\"column:item_id;type:String\"`\n\t\t\tIsHidden   int       `gorm:\"column:is_hidden;type:Boolean;default:0\"`\n\t\t\tCategories string    `gorm:\"column:categories;type:String;default:'[]'\"`\n\t\t\tTimestamp  time.Time `gorm:\"column:time_stamp;type:Datetime64(9,'UTC')\"`\n\t\t\tLabels     string    `gorm:\"column:labels;type:String;default:'[]'\"`\n\t\t\tComment    string    `gorm:\"column:comment;type:String\"`\n\t\t\tVersion    struct{}  `gorm:\"column:version;type:DateTime\"`\n\t\t}\n\t\terr := d.gormDB.Set(\"gorm:table_options\", \"ENGINE = ReplacingMergeTree(version) ORDER BY item_id\").AutoMigrate(Items{})\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\ttype Users struct {\n\t\t\tUserId  string   `gorm:\"column:user_id;type:String\"`\n\t\t\tLabels  string   `gorm:\"column:labels;type:String;default:'[]'\"`\n\t\t\tComment string   `gorm:\"column:comment;type:String\"`\n\t\t\tVersion struct{} `gorm:\"column:version;type:DateTime\"`\n\t\t}\n\t\terr = d.gormDB.Set(\"gorm:table_options\", \"ENGINE = ReplacingMergeTree(version) ORDER BY user_id\").AutoMigrate(Users{})\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\ttype Feedback struct {\n\t\t\tFeedbackType string    `gorm:\"column:feedback_type;type:String\"`\n\t\t\tUserId       string    `gorm:\"column:user_id;type:String\"`\n\t\t\tItemId       string    `gorm:\"column:item_id;type:String\"`\n\t\t\tValue        float64   `gorm:\"column:value;type:Float64;default:0\"`\n\t\t\tTimestamp    time.Time `gorm:\"column:time_stamp;type:DateTime64(9,'UTC')\"`\n\t\t\tUpdated      time.Time `gorm:\"column:updated;type:DateTime64(9,'UTC')\"`\n\t\t\tComment      string    `gorm:\"column:comment;type:String\"`\n\t\t}\n\t\terr = d.gormDB.Set(\"gorm:table_options\", \"ENGINE = MergeTree ORDER BY (feedback_type, user_id, item_id)\").AutoMigrate(Feedback{})\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\t// create materialized views\n\t\ttype AggregatingFeedback struct {\n\t\t\tFeedbackType string    `gorm:\"column:feedback_type;type:String\"`\n\t\t\tUserId       string    `gorm:\"column:user_id;type:String\"`\n\t\t\tItemId       string    `gorm:\"column:item_id;type:String\"`\n\t\t\tValue        float64   `gorm:\"column:value;type:SimpleAggregateFunction(sum, Float64)\"`\n\t\t\tTimestamp    time.Time `gorm:\"column:time_stamp;type:SimpleAggregateFunction(min, DateTime64(9,'UTC'))\"`\n\t\t\tUpdated      time.Time `gorm:\"column:updated;type:SimpleAggregateFunction(max, DateTime64(9,'UTC'))\"`\n\t\t\tComment      string    `gorm:\"column:comment;type:SimpleAggregateFunction(anyLast, String)\"`\n\t\t}\n\t\terr = d.gormDB.Set(\"gorm:table_options\", \"ENGINE = AggregatingMergeTree() ORDER BY (user_id, item_id, feedback_type)\").AutoMigrate(AggregatingFeedback{})\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\terr = d.gormDB.Exec(fmt.Sprintf(\"CREATE MATERIALIZED VIEW IF NOT EXISTS %s_mv TO %s AS \"+\n\t\t\t\"SELECT feedback_type, user_id, item_id, sum(value) AS value, min(time_stamp) AS time_stamp, max(updated) AS updated, anyLast(comment) AS comment \"+\n\t\t\t\"FROM %s GROUP BY feedback_type, user_id, item_id\",\n\t\t\td.AggregatingFeedbackTable(), d.AggregatingFeedbackTable(), d.FeedbackTable())).Error\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\ttype UserFeedback AggregatingFeedback\n\t\terr = d.gormDB.Set(\"gorm:table_options\", \"ENGINE = AggregatingMergeTree() ORDER BY (user_id, item_id, feedback_type)\").AutoMigrate(UserFeedback{})\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\terr = d.gormDB.Exec(fmt.Sprintf(\"CREATE MATERIALIZED VIEW IF NOT EXISTS %s_mv TO %s AS \"+\n\t\t\t\"SELECT feedback_type, user_id, item_id, sum(value) AS value, min(time_stamp) AS time_stamp, max(updated) AS updated, anyLast(comment) AS comment \"+\n\t\t\t\"FROM %s GROUP BY feedback_type, user_id, item_id\",\n\t\t\td.UserFeedbackTable(), d.UserFeedbackTable(), d.FeedbackTable())).Error\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\ttype ItemFeedback AggregatingFeedback\n\t\terr = d.gormDB.Set(\"gorm:table_options\", \"ENGINE = AggregatingMergeTree() ORDER BY (item_id, user_id, feedback_type)\").AutoMigrate(ItemFeedback{})\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\terr = d.gormDB.Exec(fmt.Sprintf(\"CREATE MATERIALIZED VIEW IF NOT EXISTS %s_mv TO %s AS \"+\n\t\t\t\"SELECT feedback_type, user_id, item_id, sum(value) AS value, min(time_stamp) AS time_stamp, max(updated) AS updated, anyLast(comment) AS comment \"+\n\t\t\t\"FROM %s GROUP BY feedback_type, user_id, item_id\",\n\t\t\td.ItemFeedbackTable(), d.ItemFeedbackTable(), d.FeedbackTable())).Error\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\ttype LatestItems struct {\n\t\t\tItemId     string    `gorm:\"column:item_id;type:String\"`\n\t\t\tIsHidden   int       `gorm:\"column:is_hidden;type:Boolean;default:0\"`\n\t\t\tCategories string    `gorm:\"column:categories;type:String;default:'[]'\"`\n\t\t\tTimestamp  time.Time `gorm:\"column:time_stamp;type:Datetime64(9,'UTC')\"`\n\t\t\tLabels     string    `gorm:\"column:labels;type:String;default:'[]'\"`\n\t\t\tComment    string    `gorm:\"column:comment;type:String\"`\n\t\t\tVersion    time.Time `gorm:\"column:version;type:DateTime\"`\n\t\t}\n\t\terr = d.gormDB.Set(\"gorm:table_options\", \"ENGINE = ReplacingMergeTree(version) ORDER BY (time_stamp, item_id) SETTINGS index_granularity = 8192\").AutoMigrate(LatestItems{})\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\t// Create materialized view for latest items ordered by timestamp\n\t\terr = d.gormDB.Exec(fmt.Sprintf(\"CREATE MATERIALIZED VIEW IF NOT EXISTS %s_latest_mv TO %s AS \"+\n\t\t\t\"SELECT item_id, is_hidden, categories, time_stamp, labels, comment, version \"+\n\t\t\t\"FROM %s\",\n\t\t\td.ItemsTable(), d.LatestItemsTable(), d.ItemsTable())).Error\n\t\tif err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (d *SQLDatabase) Ping() error {\n\treturn d.client.Ping()\n}\n\n// Close MySQL connection.\nfunc (d *SQLDatabase) Close() error {\n\treturn d.client.Close()\n}\n\nfunc (d *SQLDatabase) Purge() error {\n\tif d.driver == ClickHouse {\n\t\ttables := []string{d.ItemsTable(), d.FeedbackTable(), d.UsersTable(), d.UserFeedbackTable(), d.ItemFeedbackTable(), d.LatestItemsTable()}\n\t\tfor _, tableName := range tables {\n\t\t\terr := d.gormDB.Exec(fmt.Sprintf(\"alter table %s delete where 1=1\", tableName)).Error\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t}\n\t} else {\n\t\ttables := []string{d.ItemsTable(), d.FeedbackTable(), d.UsersTable()}\n\t\tfor _, tableName := range tables {\n\t\t\terr := d.gormDB.Exec(fmt.Sprintf(\"DELETE FROM %s\", tableName)).Error\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// BatchInsertItems inserts a batch of items into MySQL.\nfunc (d *SQLDatabase) BatchInsertItems(ctx context.Context, items []Item) error {\n\tif len(items) == 0 {\n\t\treturn nil\n\t}\n\tif d.driver == ClickHouse {\n\t\trows := make([]ClickHouseItem, 0, len(items))\n\t\tmemo := mapset.NewSet[string]()\n\t\tfor _, item := range items {\n\t\t\tif !memo.Contains(item.ItemId) {\n\t\t\t\tmemo.Add(item.ItemId)\n\t\t\t\trows = append(rows, NewClickHouseItem(item))\n\t\t\t}\n\t\t}\n\t\terr := d.gormDB.Create(rows).Error\n\t\treturn errors.Trace(err)\n\t} else {\n\t\trows := make([]SQLItem, 0, len(items))\n\t\tmemo := mapset.NewSet[string]()\n\t\tfor _, item := range items {\n\t\t\tif !memo.Contains(item.ItemId) {\n\t\t\t\tmemo.Add(item.ItemId)\n\t\t\t\trow := NewSQLItem(item)\n\t\t\t\tif d.driver == SQLite {\n\t\t\t\t\trow.Timestamp = row.Timestamp.In(time.UTC)\n\t\t\t\t}\n\t\t\t\trows = append(rows, row)\n\t\t\t}\n\t\t}\n\t\terr := d.gormDB.WithContext(ctx).Clauses(clause.OnConflict{\n\t\t\tColumns:   []clause.Column{{Name: \"item_id\"}},\n\t\t\tDoUpdates: clause.AssignmentColumns([]string{\"is_hidden\", \"categories\", \"time_stamp\", \"labels\", \"comment\"}),\n\t\t}).Create(rows).Error\n\t\treturn errors.Trace(err)\n\t}\n}\n\nfunc (d *SQLDatabase) BatchGetItems(ctx context.Context, itemIds []string) ([]Item, error) {\n\tif len(itemIds) == 0 {\n\t\treturn nil, nil\n\t}\n\tresult, err := d.gormDB.WithContext(ctx).\n\t\tTable(d.ItemsTable()).\n\t\tSelect(\"item_id, is_hidden, categories, time_stamp, labels, comment\").\n\t\tWhere(\"item_id IN ?\", itemIds).Rows()\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\tdefer result.Close()\n\tvar items []Item\n\tfor result.Next() {\n\t\tvar item Item\n\t\tif err = d.gormDB.ScanRows(result, &item); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\titems = append(items, item)\n\t}\n\treturn items, nil\n}\n\n// DeleteItem deletes a item from MySQL.\nfunc (d *SQLDatabase) DeleteItem(ctx context.Context, itemId string) error {\n\tif err := d.gormDB.WithContext(ctx).Delete(&SQLItem{ItemId: itemId}).Error; err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif err := d.gormDB.WithContext(ctx).Delete(&Feedback{}, \"item_id = ?\", itemId).Error; err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif d.driver == ClickHouse {\n\t\tif err := d.gormDB.WithContext(ctx).Delete(&ItemFeedback{}, \"item_id = ?\", itemId).Error; err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\tif err := d.gormDB.WithContext(ctx).Delete(&UserFeedback{}, \"item_id = ?\", itemId).Error; err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// GetItem get a item from MySQL.\nfunc (d *SQLDatabase) GetItem(ctx context.Context, itemId string) (Item, error) {\n\tvar result *sql.Rows\n\tvar err error\n\tresult, err = d.gormDB.WithContext(ctx).\n\t\tTable(d.ItemsTable()).\n\t\tSelect(\"item_id, is_hidden, categories, time_stamp, labels, comment\").\n\t\tWhere(\"item_id = ?\", itemId).Rows()\n\tif err != nil {\n\t\treturn Item{}, errors.Trace(err)\n\t}\n\tdefer result.Close()\n\tif result.Next() {\n\t\tvar item Item\n\t\tif err = d.gormDB.ScanRows(result, &item); err != nil {\n\t\t\treturn Item{}, errors.Trace(err)\n\t\t}\n\t\treturn item, nil\n\t}\n\treturn Item{}, errors.Annotate(ErrItemNotExist, itemId)\n}\n\n// ModifyItem modify an item in MySQL.\nfunc (d *SQLDatabase) ModifyItem(ctx context.Context, itemId string, patch ItemPatch) error {\n\t// ignore empty patch\n\tif patch.IsHidden == nil && patch.Categories == nil && patch.Labels == nil && patch.Comment == nil && patch.Timestamp == nil {\n\t\tlog.Logger().Debug(\"empty item patch\")\n\t\treturn nil\n\t}\n\tattributes := make(map[string]any)\n\tif patch.IsHidden != nil {\n\t\tif *patch.IsHidden {\n\t\t\tattributes[\"is_hidden\"] = 1\n\t\t} else {\n\t\t\tattributes[\"is_hidden\"] = 0\n\t\t}\n\t}\n\tif patch.Categories != nil {\n\t\ttext, _ := jsonutil.Marshal(patch.Categories)\n\t\tattributes[\"categories\"] = string(text)\n\t}\n\tif patch.Comment != nil {\n\t\tattributes[\"comment\"] = *patch.Comment\n\t}\n\tif patch.Labels != nil {\n\t\ttext, _ := jsonutil.Marshal(patch.Labels)\n\t\tattributes[\"labels\"] = string(text)\n\t}\n\tif patch.Timestamp != nil {\n\t\tswitch d.driver {\n\t\tcase ClickHouse, SQLite:\n\t\t\tattributes[\"time_stamp\"] = patch.Timestamp.In(time.UTC)\n\t\tdefault:\n\t\t\tattributes[\"time_stamp\"] = patch.Timestamp\n\t\t}\n\t}\n\terr := d.gormDB.WithContext(ctx).Model(&SQLItem{ItemId: itemId}).Updates(attributes).Error\n\treturn errors.Trace(err)\n}\n\n// GetItems returns items from MySQL.\nfunc (d *SQLDatabase) GetItems(ctx context.Context, cursor string, n int, timeLimit *time.Time) (string, []Item, error) {\n\tbuf, err := base64.StdEncoding.DecodeString(cursor)\n\tif err != nil {\n\t\treturn \"\", nil, errors.Trace(err)\n\t}\n\tcursorItem := string(buf)\n\ttx := d.gormDB.WithContext(ctx).\n\t\tTable(d.ItemsTable()).\n\t\tSelect(\"item_id, is_hidden, categories, time_stamp, labels, comment\")\n\tif cursorItem != \"\" {\n\t\ttx.Where(\"item_id >= ?\", cursorItem)\n\t}\n\tif timeLimit != nil {\n\t\ttx.Where(\"time_stamp >= ?\", *timeLimit)\n\t}\n\tresult, err := tx.Order(\"item_id\").Limit(n + 1).Rows()\n\tif err != nil {\n\t\treturn \"\", nil, errors.Trace(err)\n\t}\n\titems := make([]Item, 0)\n\tdefer result.Close()\n\tfor result.Next() {\n\t\tvar item Item\n\t\tif err = d.gormDB.ScanRows(result, &item); err != nil {\n\t\t\treturn \"\", nil, errors.Trace(err)\n\t\t}\n\t\titems = append(items, item)\n\t}\n\tif len(items) == n+1 {\n\t\treturn base64.StdEncoding.EncodeToString([]byte(items[len(items)-1].ItemId)), items[:len(items)-1], nil\n\t}\n\treturn \"\", items, nil\n}\n\n// GetLatestItems returns the latest items from the database.\nfunc (d *SQLDatabase) GetLatestItems(ctx context.Context, n int, categories []string) ([]Item, error) {\n\tvar tableName string\n\tif d.driver == ClickHouse {\n\t\ttableName = d.LatestItemsTable()\n\t} else {\n\t\ttableName = d.ItemsTable()\n\t}\n\ttx := d.gormDB.WithContext(ctx).\n\t\tTable(tableName).\n\t\tSelect(\"item_id, is_hidden, categories, time_stamp, labels, comment\").\n\t\tWhere(\"is_hidden = ?\", false)\n\tif len(categories) > 0 {\n\t\tq, err := jsonutil.Marshal(categories)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tswitch d.driver {\n\t\tcase Postgres:\n\t\t\ttx = tx.Where(\"categories::jsonb @> ?::jsonb\", string(q))\n\t\tcase MySQL, SQLite:\n\t\t\ttx = tx.Where(\"JSON_CONTAINS(categories,?)\", string(q))\n\t\tcase ClickHouse:\n\t\t\ttx = tx.Where(\"hasAll(JSONExtractArrayRaw(categories),JSONExtractArrayRaw(?))\", string(q))\n\t\t}\n\t}\n\tresult, err := tx.Order(\"time_stamp DESC\").Limit(n).Rows()\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\titems := make([]Item, 0)\n\tdefer result.Close()\n\tfor result.Next() {\n\t\tvar item Item\n\t\tif err = d.gormDB.ScanRows(result, &item); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\titems = append(items, item)\n\t}\n\treturn items, nil\n}\n\n// GetItemStream reads items by stream.\nfunc (d *SQLDatabase) GetItemStream(ctx context.Context, batchSize int, timeLimit *time.Time) (chan []Item, chan error) {\n\titemChan := make(chan []Item, bufSize)\n\terrChan := make(chan error, 1)\n\tgo func() {\n\t\tdefer close(itemChan)\n\t\tdefer close(errChan)\n\t\t// send query\n\t\ttx := d.gormDB.WithContext(ctx).\n\t\t\tTable(d.ItemsTable()).\n\t\t\tSelect(\"item_id, is_hidden, categories, time_stamp, labels, comment\")\n\t\tif timeLimit != nil {\n\t\t\ttx.Where(\"time_stamp >= ?\", *timeLimit)\n\t\t}\n\t\tresult, err := tx.Rows()\n\t\tif err != nil {\n\t\t\terrChan <- errors.Trace(err)\n\t\t\treturn\n\t\t}\n\t\t// fetch result\n\t\titems := make([]Item, 0, batchSize)\n\t\tdefer result.Close()\n\t\tfor result.Next() {\n\t\t\tvar item Item\n\t\t\tif err = d.gormDB.ScanRows(result, &item); err != nil {\n\t\t\t\terrChan <- errors.Trace(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\titems = append(items, item)\n\t\t\tif len(items) == batchSize {\n\t\t\t\titemChan <- items\n\t\t\t\titems = make([]Item, 0, batchSize)\n\t\t\t}\n\t\t}\n\t\tif len(items) > 0 {\n\t\t\titemChan <- items\n\t\t}\n\t\terrChan <- nil\n\t}()\n\treturn itemChan, errChan\n}\n\n// GetItemFeedback returns feedback of a item from MySQL.\nfunc (d *SQLDatabase) GetItemFeedback(ctx context.Context, itemId string, feedbackTypes ...string) ([]Feedback, error) {\n\ttx := d.gormDB.WithContext(ctx)\n\tif d.driver == ClickHouse {\n\t\ttx = tx.Table(d.ItemFeedbackTable()).\n\t\t\tSelect(\"user_id, item_id, feedback_type, sum(value) AS value, min(time_stamp) AS time_stamp, max(updated) AS updated, anyLast(comment) AS comment\").\n\t\t\tGroup(\"user_id, item_id, feedback_type\")\n\t} else {\n\t\ttx = tx.Table(d.FeedbackTable()).\n\t\t\tSelect(\"user_id, item_id, feedback_type, value, time_stamp, updated, comment\")\n\t}\n\tswitch d.driver {\n\tcase SQLite:\n\t\ttx.Where(\"time_stamp <= DATETIME()\")\n\tcase ClickHouse:\n\t\ttx.Having(\"time_stamp <= NOW('UTC')\")\n\tdefault:\n\t\ttx.Where(\"time_stamp <= NOW()\")\n\t}\n\ttx.Where(\"item_id = ?\", itemId)\n\tif len(feedbackTypes) > 0 {\n\t\tdb := d.gormDB\n\t\tfor _, feedbackType := range feedbackTypes {\n\t\t\tdb = db.Or(\"feedback_type = ?\", feedbackType)\n\t\t}\n\t\ttx = tx.Where(db)\n\t}\n\tresult, err := tx.Rows()\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\tfeedbacks := make([]Feedback, 0)\n\tdefer result.Close()\n\tfor result.Next() {\n\t\tvar feedback Feedback\n\t\tif err = d.gormDB.ScanRows(result, &feedback); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tfeedbacks = append(feedbacks, feedback)\n\t}\n\treturn feedbacks, nil\n}\n\n// BatchInsertUsers inserts users into MySQL.\nfunc (d *SQLDatabase) BatchInsertUsers(ctx context.Context, users []User) error {\n\tif len(users) == 0 {\n\t\treturn nil\n\t}\n\tif d.driver == ClickHouse {\n\t\trows := make([]ClickhouseUser, 0, len(users))\n\t\tmemo := mapset.NewSet[string]()\n\t\tfor _, user := range users {\n\t\t\tif !memo.Contains(user.UserId) {\n\t\t\t\tmemo.Add(user.UserId)\n\t\t\t\trows = append(rows, NewClickhouseUser(user))\n\t\t\t}\n\t\t}\n\t\terr := d.gormDB.Create(rows).Error\n\t\treturn errors.Trace(err)\n\t} else {\n\t\trows := make([]SQLUser, 0, len(users))\n\t\tmemo := mapset.NewSet[string]()\n\t\tfor _, user := range users {\n\t\t\tif !memo.Contains(user.UserId) {\n\t\t\t\tmemo.Add(user.UserId)\n\t\t\t\trows = append(rows, NewSQLUser(user))\n\t\t\t}\n\t\t}\n\t\terr := d.gormDB.WithContext(ctx).Clauses(clause.OnConflict{\n\t\t\tColumns:   []clause.Column{{Name: \"user_id\"}},\n\t\t\tDoUpdates: clause.AssignmentColumns([]string{\"labels\", \"comment\"}),\n\t\t}).Create(rows).Error\n\t\treturn errors.Trace(err)\n\t}\n}\n\n// DeleteUser deletes a user from MySQL.\nfunc (d *SQLDatabase) DeleteUser(ctx context.Context, userId string) error {\n\tif err := d.gormDB.WithContext(ctx).Delete(&SQLUser{UserId: userId}).Error; err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif err := d.gormDB.WithContext(ctx).Delete(&Feedback{}, \"user_id = ?\", userId).Error; err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif d.driver == ClickHouse {\n\t\tif err := d.gormDB.WithContext(ctx).Delete(&ItemFeedback{}, \"user_id = ?\", userId).Error; err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\tif err := d.gormDB.WithContext(ctx).Delete(&UserFeedback{}, \"user_id = ?\", userId).Error; err != nil {\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// GetUser returns a user from MySQL.\nfunc (d *SQLDatabase) GetUser(ctx context.Context, userId string) (User, error) {\n\tvar result *sql.Rows\n\tvar err error\n\tresult, err = d.gormDB.WithContext(ctx).Table(d.UsersTable()).\n\t\tSelect(\"user_id, labels, comment\").\n\t\tWhere(\"user_id = ?\", userId).Rows()\n\tif err != nil {\n\t\treturn User{}, errors.Trace(err)\n\t}\n\tdefer result.Close()\n\tif result.Next() {\n\t\tvar user User\n\t\tif err = d.gormDB.ScanRows(result, &user); err != nil {\n\t\t\treturn User{}, errors.Trace(err)\n\t\t}\n\t\treturn user, nil\n\t}\n\treturn User{}, errors.Annotate(ErrUserNotExist, userId)\n}\n\n// ModifyUser modify a user in MySQL.\nfunc (d *SQLDatabase) ModifyUser(ctx context.Context, userId string, patch UserPatch) error {\n\t// ignore empty patch\n\tif patch.Labels == nil && patch.Comment == nil {\n\t\tlog.Logger().Debug(\"empty user patch\")\n\t\treturn nil\n\t}\n\tattributes := make(map[string]any)\n\tif patch.Comment != nil {\n\t\tattributes[\"comment\"] = *patch.Comment\n\t}\n\tif patch.Labels != nil {\n\t\ttext, _ := jsonutil.Marshal(patch.Labels)\n\t\tattributes[\"labels\"] = string(text)\n\t}\n\terr := d.gormDB.WithContext(ctx).Model(&SQLUser{UserId: userId}).Updates(attributes).Error\n\treturn errors.Trace(err)\n}\n\n// GetUsers returns users from MySQL.\nfunc (d *SQLDatabase) GetUsers(ctx context.Context, cursor string, n int) (string, []User, error) {\n\tbuf, err := base64.StdEncoding.DecodeString(cursor)\n\tif err != nil {\n\t\treturn \"\", nil, errors.Trace(err)\n\t}\n\tcursorUser := string(buf)\n\ttx := d.gormDB.WithContext(ctx).\n\t\tTable(d.UsersTable()).\n\t\tSelect(\"user_id, labels, comment\")\n\tif cursorUser != \"\" {\n\t\ttx.Where(\"user_id >= ?\", cursorUser)\n\t}\n\tresult, err := tx.Order(\"user_id\").Limit(n + 1).Rows()\n\tif err != nil {\n\t\treturn \"\", nil, errors.Trace(err)\n\t}\n\tusers := make([]User, 0)\n\tdefer result.Close()\n\tfor result.Next() {\n\t\tvar user User\n\t\tif err = d.gormDB.ScanRows(result, &user); err != nil {\n\t\t\treturn \"\", nil, errors.Trace(err)\n\t\t}\n\t\tusers = append(users, user)\n\t}\n\tif len(users) == n+1 {\n\t\treturn base64.StdEncoding.EncodeToString([]byte(users[len(users)-1].UserId)), users[:len(users)-1], nil\n\t}\n\treturn \"\", users, nil\n}\n\n// GetUserStream read users by stream.\nfunc (d *SQLDatabase) GetUserStream(ctx context.Context, batchSize int) (chan []User, chan error) {\n\tuserChan := make(chan []User, bufSize)\n\terrChan := make(chan error, 1)\n\tgo func() {\n\t\tdefer close(userChan)\n\t\tdefer close(errChan)\n\t\t// send query\n\t\tresult, err := d.gormDB.WithContext(ctx).Table(d.UsersTable()).Select(\"user_id, labels, comment\").Rows()\n\t\tif err != nil {\n\t\t\terrChan <- errors.Trace(err)\n\t\t\treturn\n\t\t}\n\t\t// fetch result\n\t\tusers := make([]User, 0, batchSize)\n\t\tdefer result.Close()\n\t\tfor result.Next() {\n\t\t\tvar user User\n\t\t\tif err = d.gormDB.ScanRows(result, &user); err != nil {\n\t\t\t\terrChan <- errors.Trace(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tusers = append(users, user)\n\t\t\tif len(users) == batchSize {\n\t\t\t\tuserChan <- users\n\t\t\t\tusers = make([]User, 0, batchSize)\n\t\t\t}\n\t\t}\n\t\tif len(users) > 0 {\n\t\t\tuserChan <- users\n\t\t}\n\t\terrChan <- nil\n\t}()\n\treturn userChan, errChan\n}\n\n// GetUserFeedback returns feedback of a user from MySQL.\nfunc (d *SQLDatabase) GetUserFeedback(ctx context.Context, userId string, endTime *time.Time, feedbackTypes ...expression.FeedbackTypeExpression) ([]Feedback, error) {\n\ttx := d.gormDB.WithContext(ctx)\n\tif d.driver == ClickHouse {\n\t\ttx = tx.Table(d.UserFeedbackTable())\n\t} else {\n\t\ttx = tx.Table(d.FeedbackTable())\n\t}\n\tif d.driver == ClickHouse {\n\t\ttx.Select(\"feedback_type, user_id, item_id, sum(value) AS value, min(time_stamp) AS time_stamp, max(updated) AS updated, anyLast(comment) AS comment\").\n\t\t\tGroup(\"feedback_type, user_id, item_id\")\n\t\tif endTime != nil {\n\t\t\ttx.Having(\"time_stamp <= ?\", d.convertTimeZone(endTime))\n\t\t}\n\t} else {\n\t\ttx.Select(\"feedback_type, user_id, item_id, value, time_stamp, updated, comment\")\n\t\tif endTime != nil {\n\t\t\ttx.Where(\"time_stamp <= ?\", d.convertTimeZone(endTime))\n\t\t}\n\t}\n\ttx.Where(\"user_id = ?\", userId)\n\tif len(feedbackTypes) > 0 {\n\t\tdb := d.gormDB\n\t\tfor _, feedbackType := range feedbackTypes {\n\t\t\tdb = FeedbackTypeExpressionToSQL(db, feedbackType)\n\t\t}\n\t\tif d.driver == ClickHouse {\n\t\t\ttx.Having(db)\n\t\t} else {\n\t\t\ttx.Where(db)\n\t\t}\n\t}\n\tresult, err := tx.Rows()\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\tfeedbacks := make([]Feedback, 0)\n\tdefer result.Close()\n\tfor result.Next() {\n\t\tvar feedback Feedback\n\t\tif err = d.gormDB.ScanRows(result, &feedback); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tfeedbacks = append(feedbacks, feedback)\n\t}\n\treturn feedbacks, nil\n}\n\n// BatchInsertFeedback insert a batch feedback into MySQL.\n// If insertUser set, new users will be inserted to user table.\n// If insertItem set, new items will be inserted to item table.\nfunc (d *SQLDatabase) BatchInsertFeedback(ctx context.Context, feedback []Feedback, insertUser, insertItem, overwrite bool) error {\n\ttx := d.gormDB.WithContext(ctx)\n\t// skip empty list\n\tif len(feedback) == 0 {\n\t\treturn nil\n\t}\n\t// collect users and items\n\tusers := mapset.NewSet[string]()\n\titems := mapset.NewSet[string]()\n\tfor _, v := range feedback {\n\t\tusers.Add(v.UserId)\n\t\titems.Add(v.ItemId)\n\t}\n\t// insert users\n\tif insertUser {\n\t\tuserList := users.ToSlice()\n\t\tif d.driver == ClickHouse {\n\t\t\terr := tx.Create(lo.Map(userList, func(userId string, _ int) ClickhouseUser {\n\t\t\t\treturn ClickhouseUser{\n\t\t\t\t\tSQLUser: SQLUser{\n\t\t\t\t\t\tUserId: userId,\n\t\t\t\t\t\tLabels: \"[]\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t})).Error\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t} else {\n\t\t\terr := tx.Clauses(clause.OnConflict{\n\t\t\t\tColumns:   []clause.Column{{Name: \"user_id\"}},\n\t\t\t\tDoNothing: true,\n\t\t\t}).Create(lo.Map(userList, func(userId string, _ int) SQLUser {\n\t\t\t\treturn SQLUser{\n\t\t\t\t\tUserId: userId,\n\t\t\t\t\tLabels: \"null\",\n\t\t\t\t}\n\t\t\t})).Error\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor _, user := range users.ToSlice() {\n\t\t\trs, err := tx.Table(d.UsersTable()).Select(\"user_id\").Where(\"user_id = ?\", user).Rows()\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t} else if !rs.Next() {\n\t\t\t\tusers.Remove(user)\n\t\t\t}\n\t\t\tif err = rs.Close(); err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t}\n\t}\n\t// insert items\n\tif insertItem {\n\t\titemList := items.ToSlice()\n\t\tif d.driver == ClickHouse {\n\t\t\terr := tx.Create(lo.Map(itemList, func(itemId string, _ int) ClickHouseItem {\n\t\t\t\treturn ClickHouseItem{\n\t\t\t\t\tSQLItem: SQLItem{\n\t\t\t\t\t\tItemId:     itemId,\n\t\t\t\t\t\tLabels:     \"[]\",\n\t\t\t\t\t\tCategories: \"[]\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t})).Error\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t} else {\n\t\t\terr := tx.Clauses(clause.OnConflict{\n\t\t\t\tColumns:   []clause.Column{{Name: \"item_id\"}},\n\t\t\t\tDoNothing: true,\n\t\t\t}).Create(lo.Map(itemList, func(itemId string, _ int) SQLItem {\n\t\t\t\treturn SQLItem{\n\t\t\t\t\tItemId:     itemId,\n\t\t\t\t\tLabels:     \"null\",\n\t\t\t\t\tCategories: \"null\",\n\t\t\t\t}\n\t\t\t})).Error\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor _, item := range items.ToSlice() {\n\t\t\trs, err := tx.Table(d.ItemsTable()).Select(\"item_id\").Where(\"item_id = ?\", item).Rows()\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t} else if !rs.Next() {\n\t\t\t\titems.Remove(item)\n\t\t\t}\n\t\t\tif err = rs.Close(); err != nil {\n\t\t\t\treturn errors.Trace(err)\n\t\t\t}\n\t\t}\n\t}\n\t// insert feedback\n\tif d.driver == ClickHouse {\n\t\trows := make([]Feedback, 0, len(feedback))\n\t\tmemo := make(map[lo.Tuple3[string, string, string]]struct{})\n\t\tfor _, f := range feedback {\n\t\t\tif users.Contains(f.UserId) && items.Contains(f.ItemId) {\n\t\t\t\tif _, exist := memo[lo.Tuple3[string, string, string]{f.FeedbackType, f.UserId, f.ItemId}]; !exist {\n\t\t\t\t\tmemo[lo.Tuple3[string, string, string]{f.FeedbackType, f.UserId, f.ItemId}] = struct{}{}\n\t\t\t\t\tf.Timestamp = f.Timestamp.In(time.UTC)\n\t\t\t\t\tf.Updated = f.Timestamp\n\t\t\t\t\trows = append(rows, f)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif len(rows) == 0 {\n\t\t\treturn nil\n\t\t}\n\t\terr := tx.Create(rows).Error\n\t\treturn errors.Trace(err)\n\t} else {\n\t\trows := make([]Feedback, 0, len(feedback))\n\t\tmemo := make(map[lo.Tuple3[string, string, string]]struct{})\n\t\tfor _, f := range feedback {\n\t\t\tif users.Contains(f.UserId) && items.Contains(f.ItemId) {\n\t\t\t\tif _, exist := memo[lo.Tuple3[string, string, string]{f.FeedbackType, f.UserId, f.ItemId}]; !exist {\n\t\t\t\t\tmemo[lo.Tuple3[string, string, string]{f.FeedbackType, f.UserId, f.ItemId}] = struct{}{}\n\t\t\t\t\tif d.driver == SQLite {\n\t\t\t\t\t\tf.Timestamp = f.Timestamp.In(time.UTC)\n\t\t\t\t\t}\n\t\t\t\t\tf.Updated = f.Timestamp\n\t\t\t\t\tif d.driver == SQLite {\n\t\t\t\t\t\tf.Updated = f.Updated.In(time.UTC)\n\t\t\t\t\t}\n\t\t\t\t\trows = append(rows, f)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif len(rows) == 0 {\n\t\t\treturn nil\n\t\t}\n\t\tvar updates clause.Set\n\t\tif overwrite {\n\t\t\tupdates = clause.AssignmentColumns([]string{\"time_stamp\", \"updated\", \"comment\", \"value\"})\n\t\t} else {\n\t\t\tvalues := make(map[string]any)\n\t\t\tswitch d.driver {\n\t\t\tcase MySQL:\n\t\t\t\tvalues[\"value\"] = clause.Column{Raw: true, Name: \"value + VALUES(value)\"}\n\t\t\t\tvalues[\"time_stamp\"] = clause.Column{Raw: true, Name: \"LEAST(time_stamp, VALUES(time_stamp))\"}\n\t\t\t\tvalues[\"updated\"] = clause.Column{Raw: true, Name: \"GREATEST(updated, VALUES(updated))\"}\n\t\t\t\tvalues[\"comment\"] = clause.Column{Raw: true, Name: \"VALUES(comment)\"}\n\t\t\tcase Postgres:\n\t\t\t\tvalues[\"value\"] = clause.Column{Raw: true, Name: fmt.Sprintf(\"%s.value + EXCLUDED.value\", d.FeedbackTable())}\n\t\t\t\tvalues[\"time_stamp\"] = clause.Column{Raw: true, Name: fmt.Sprintf(\"LEAST(%s.time_stamp, EXCLUDED.time_stamp)\", d.FeedbackTable())}\n\t\t\t\tvalues[\"updated\"] = clause.Column{Raw: true, Name: fmt.Sprintf(\"GREATEST(%s.updated, EXCLUDED.updated)\", d.FeedbackTable())}\n\t\t\t\tvalues[\"comment\"] = clause.Column{Raw: true, Name: \"EXCLUDED.comment\"}\n\t\t\tcase SQLite:\n\t\t\t\tvalues[\"value\"] = clause.Column{Raw: true, Name: \"value + excluded.value\"}\n\t\t\t\tvalues[\"time_stamp\"] = clause.Column{Raw: true, Name: \"MIN(time_stamp, excluded.time_stamp)\"}\n\t\t\t\tvalues[\"updated\"] = clause.Column{Raw: true, Name: \"MAX(updated, excluded.updated)\"}\n\t\t\t\tvalues[\"comment\"] = clause.Column{Raw: true, Name: \"excluded.comment\"}\n\t\t\t}\n\t\t\tupdates = clause.Assignments(values)\n\t\t}\n\t\terr := tx.Clauses(clause.OnConflict{\n\t\t\tColumns:   []clause.Column{{Name: \"feedback_type\"}, {Name: \"user_id\"}, {Name: \"item_id\"}},\n\t\t\tDoUpdates: updates,\n\t\t}).Create(rows).Error\n\t\treturn errors.Trace(err)\n\t}\n}\n\n// GetFeedback returns feedback from MySQL.\nfunc (d *SQLDatabase) GetFeedback(ctx context.Context, cursor string, n int, beginTime, endTime *time.Time, feedbackTypes ...string) (string, []Feedback, error) {\n\tbuf, err := base64.StdEncoding.DecodeString(cursor)\n\tif err != nil {\n\t\treturn \"\", nil, errors.Trace(err)\n\t}\n\ttx := d.gormDB.WithContext(ctx).Table(d.FeedbackTable()).Select(\"feedback_type, user_id, item_id, value, time_stamp, updated, comment\")\n\tif len(buf) > 0 {\n\t\tvar cursorKey FeedbackKey\n\t\tif err := jsonutil.Unmarshal(buf, &cursorKey); err != nil {\n\t\t\treturn \"\", nil, err\n\t\t}\n\t\ttx.Where(\"(feedback_type, user_id, item_id) >= (?,?,?)\", cursorKey.FeedbackType, cursorKey.UserId, cursorKey.ItemId)\n\t}\n\tif len(feedbackTypes) > 0 {\n\t\tdb := d.gormDB\n\t\tfor _, feedbackType := range feedbackTypes {\n\t\t\tdb = db.Or(\"feedback_type = ?\", feedbackType)\n\t\t}\n\t\ttx.Where(db)\n\t}\n\tif beginTime != nil {\n\t\ttx.Where(\"time_stamp >= ?\", d.convertTimeZone(beginTime))\n\t}\n\tif endTime != nil {\n\t\ttx.Where(\"time_stamp <= ?\", d.convertTimeZone(endTime))\n\t}\n\ttx.Order(\"feedback_type, user_id, item_id\").Limit(n + 1)\n\tresult, err := tx.Rows()\n\tif err != nil {\n\t\treturn \"\", nil, errors.Trace(err)\n\t}\n\tfeedbacks := make([]Feedback, 0)\n\tdefer result.Close()\n\tfor result.Next() {\n\t\tvar feedback Feedback\n\t\tif err = d.gormDB.ScanRows(result, &feedback); err != nil {\n\t\t\treturn \"\", nil, errors.Trace(err)\n\t\t}\n\t\tfeedbacks = append(feedbacks, feedback)\n\t}\n\tif len(feedbacks) == n+1 {\n\t\tnextCursorKey := feedbacks[len(feedbacks)-1].FeedbackKey\n\t\tnextCursor, err := jsonutil.Marshal(nextCursorKey)\n\t\tif err != nil {\n\t\t\treturn \"\", nil, errors.Trace(err)\n\t\t}\n\t\treturn base64.StdEncoding.EncodeToString(nextCursor), feedbacks[:len(feedbacks)-1], nil\n\t}\n\treturn \"\", feedbacks, nil\n}\n\n// GetFeedbackStream reads feedback by stream.\nfunc (d *SQLDatabase) GetFeedbackStream(ctx context.Context, batchSize int, scanOptions ...ScanOption) (chan []Feedback, chan error) {\n\tscan := NewScanOptions(scanOptions...)\n\tfeedbackChan := make(chan []Feedback, bufSize)\n\terrChan := make(chan error, 1)\n\tgo func() {\n\t\tdefer close(feedbackChan)\n\t\tdefer close(errChan)\n\t\t// send query\n\t\ttx := d.gormDB.WithContext(ctx).\n\t\t\tTable(d.FeedbackTable()).\n\t\t\tSelect(\"feedback_type, user_id, item_id, value, time_stamp, updated, comment\")\n\t\tif len(scan.FeedbackTypes) > 0 {\n\t\t\tdb := d.gormDB\n\t\t\tfor _, feedbackType := range scan.FeedbackTypes {\n\t\t\t\tdb = FeedbackTypeExpressionToSQL(db, feedbackType)\n\t\t\t}\n\t\t\ttx.Where(db)\n\t\t}\n\t\tif scan.BeginTime != nil {\n\t\t\ttx.Where(\"time_stamp >= ?\", d.convertTimeZone(scan.BeginTime))\n\t\t}\n\t\tif scan.EndTime != nil {\n\t\t\ttx.Where(\"time_stamp <= ?\", d.convertTimeZone(scan.EndTime))\n\t\t}\n\t\tif scan.BeginUserId != nil {\n\t\t\ttx.Where(\"user_id >= ?\", scan.BeginUserId)\n\t\t}\n\t\tif scan.EndUserId != nil {\n\t\t\ttx.Where(\"user_id <= ?\", scan.EndUserId)\n\t\t}\n\t\tif scan.BeginItemId != nil {\n\t\t\ttx.Where(\"item_id >= ?\", scan.BeginItemId)\n\t\t}\n\t\tif scan.EndItemId != nil {\n\t\t\ttx.Where(\"item_id <= ?\", scan.EndItemId)\n\t\t}\n\t\tif scan.OrderByItemId {\n\t\t\ttx.Order(\"item_id\")\n\t\t} else {\n\t\t\ttx.Order(\"feedback_type, user_id, item_id\")\n\t\t}\n\t\tresult, err := tx.Rows()\n\t\tif err != nil {\n\t\t\terrChan <- errors.Trace(err)\n\t\t\treturn\n\t\t}\n\t\t// fetch result\n\t\tfeedbacks := make([]Feedback, 0, batchSize)\n\t\tdefer result.Close()\n\t\tfor result.Next() {\n\t\t\tvar feedback Feedback\n\t\t\tif err = d.gormDB.ScanRows(result, &feedback); err != nil {\n\t\t\t\terrChan <- errors.Trace(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfeedbacks = append(feedbacks, feedback)\n\t\t\tif len(feedbacks) == batchSize {\n\t\t\t\tfeedbackChan <- feedbacks\n\t\t\t\tfeedbacks = make([]Feedback, 0, batchSize)\n\t\t\t}\n\t\t}\n\t\tif len(feedbacks) > 0 {\n\t\t\tfeedbackChan <- feedbacks\n\t\t}\n\t\terrChan <- nil\n\t}()\n\treturn feedbackChan, errChan\n}\n\n// GetUserItemFeedback gets a feedback by user id and item id from MySQL.\nfunc (d *SQLDatabase) GetUserItemFeedback(ctx context.Context, userId, itemId string, feedbackTypes ...string) ([]Feedback, error) {\n\ttx := d.gormDB.WithContext(ctx)\n\tif d.driver == ClickHouse {\n\t\ttx = tx.Table(d.UserFeedbackTable()).\n\t\t\tSelect(\"feedback_type, user_id, item_id, sum(value) AS value, any(time_stamp) AS time_stamp, max(updated) AS updated, any(comment) AS comment\").\n\t\t\tGroup(\"feedback_type, user_id, item_id\")\n\t} else {\n\t\ttx = tx.Table(d.FeedbackTable()).\n\t\t\tSelect(\"feedback_type, user_id, item_id, value, time_stamp, updated, comment\")\n\t}\n\ttx.Where(\"user_id = ? AND item_id = ?\", userId, itemId)\n\tif len(feedbackTypes) > 0 {\n\t\tdb := d.gormDB\n\t\tfor _, feedbackType := range feedbackTypes {\n\t\t\tdb = db.Or(\"feedback_type = ?\", feedbackType)\n\t\t}\n\t\ttx.Where(db)\n\t}\n\tresult, err := tx.Rows()\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\tfeedbacks := make([]Feedback, 0)\n\tdefer result.Close()\n\tfor result.Next() {\n\t\tvar feedback Feedback\n\t\tif err = d.gormDB.ScanRows(result, &feedback); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tfeedbacks = append(feedbacks, feedback)\n\t}\n\treturn feedbacks, nil\n}\n\n// DeleteUserItemFeedback deletes a feedback by user id and item id from MySQL.\nfunc (d *SQLDatabase) DeleteUserItemFeedback(ctx context.Context, userId, itemId string, feedbackTypes ...string) (int, error) {\n\tdeleteUserItemFeedback := func(value any) (int, error) {\n\t\ttx := d.gormDB.WithContext(ctx).Where(\"user_id = ? AND item_id = ?\", userId, itemId)\n\t\tif len(feedbackTypes) > 0 {\n\t\t\ttx.Where(\"feedback_type IN ?\", feedbackTypes)\n\t\t}\n\t\ttx.Delete(value)\n\t\tif tx.Error != nil {\n\t\t\treturn 0, errors.Trace(tx.Error)\n\t\t}\n\t\treturn int(tx.RowsAffected), nil\n\t}\n\trowAffected, err := deleteUserItemFeedback(&Feedback{})\n\tif err != nil {\n\t\treturn 0, errors.Trace(err)\n\t}\n\tif d.driver == ClickHouse {\n\t\t_, err = deleteUserItemFeedback(&UserFeedback{})\n\t\tif err != nil {\n\t\t\treturn 0, errors.Trace(err)\n\t\t}\n\t\t_, err = deleteUserItemFeedback(&ItemFeedback{})\n\t\tif err != nil {\n\t\t\treturn 0, errors.Trace(err)\n\t\t}\n\t}\n\treturn rowAffected, nil\n}\n\nfunc (d *SQLDatabase) convertTimeZone(timestamp *time.Time) time.Time {\n\tswitch d.driver {\n\tcase ClickHouse, SQLite:\n\t\treturn timestamp.In(time.UTC)\n\tdefault:\n\t\treturn *timestamp\n\t}\n}\n\nfunc (d *SQLDatabase) CountUsers(ctx context.Context) (int, error) {\n\tvar (\n\t\tcount int64\n\t\terr   error\n\t)\n\tswitch d.driver {\n\tcase MySQL:\n\t\tvar tableStatus struct {\n\t\t\tRows int64\n\t\t}\n\t\terr = d.gormDB.WithContext(ctx).\n\t\t\tRaw(fmt.Sprintf(\"show table status like '%s'\", d.UsersTable())).\n\t\t\tScan(&tableStatus).Error\n\t\tcount = tableStatus.Rows\n\tcase Postgres:\n\t\tvar pgCount float64\n\t\terr = d.gormDB.WithContext(ctx).\n\t\t\tRaw(fmt.Sprintf(\"SELECT reltuples AS estimate FROM pg_class where relname = '%s'\", d.UsersTable())).\n\t\t\tScan(&pgCount).Error\n\t\tcount = max(int64(pgCount), 0)\n\tdefault:\n\t\terr = d.gormDB.WithContext(ctx).Table(d.UsersTable()).Count(&count).Error\n\t}\n\treturn int(count), errors.Trace(err)\n}\n\nfunc (d *SQLDatabase) CountItems(ctx context.Context) (int, error) {\n\tvar (\n\t\tcount int64\n\t\terr   error\n\t)\n\tswitch d.driver {\n\tcase MySQL:\n\t\tvar tableStatus struct {\n\t\t\tRows int64\n\t\t}\n\t\terr = d.gormDB.WithContext(ctx).\n\t\t\tRaw(fmt.Sprintf(\"show table status like '%s'\", d.ItemsTable())).\n\t\t\tScan(&tableStatus).Error\n\t\tcount = tableStatus.Rows\n\tcase Postgres:\n\t\tvar pgCount float64\n\t\terr = d.gormDB.WithContext(ctx).\n\t\t\tRaw(fmt.Sprintf(\"SELECT reltuples AS estimate FROM pg_class where relname = '%s'\", d.ItemsTable())).\n\t\t\tScan(&pgCount).Error\n\t\tcount = max(int64(pgCount), 0)\n\tdefault:\n\t\terr = d.gormDB.WithContext(ctx).Table(d.ItemsTable()).Count(&count).Error\n\t}\n\treturn int(count), errors.Trace(err)\n}\n\nfunc (d *SQLDatabase) CountFeedback(ctx context.Context) (int, error) {\n\tvar (\n\t\tcount int64\n\t\terr   error\n\t)\n\tswitch d.driver {\n\tcase MySQL:\n\t\tvar tableStatus struct {\n\t\t\tRows int64\n\t\t}\n\t\terr = d.gormDB.WithContext(ctx).\n\t\t\tRaw(fmt.Sprintf(\"show table status like '%s'\", d.FeedbackTable())).\n\t\t\tScan(&tableStatus).Error\n\t\tcount = tableStatus.Rows\n\tcase Postgres:\n\t\tvar pgCount float64\n\t\terr = d.gormDB.WithContext(ctx).\n\t\t\tRaw(fmt.Sprintf(\"SELECT reltuples AS estimate FROM pg_class where relname = '%s'\", d.FeedbackTable())).\n\t\t\tScan(&pgCount).Error\n\t\tcount = max(int64(pgCount), 0)\n\tdefault:\n\t\terr = d.gormDB.WithContext(ctx).Table(d.FeedbackTable()).Count(&count).Error\n\t}\n\treturn int(count), errors.Trace(err)\n}\n"
  },
  {
    "path": "storage/data/sql_test.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage data\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gorse-io/gorse/storage\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\nvar (\n\tmySqlDSN      string\n\tpostgresDSN   string\n\tclickhouseDSN string\n)\n\nfunc init() {\n\t// get environment variables\n\tenv := func(key, defaultValue string) string {\n\t\tif value := os.Getenv(key); value != \"\" {\n\t\t\treturn value\n\t\t}\n\t\treturn defaultValue\n\t}\n\tmySqlDSN = env(\"MYSQL_URI\", \"mysql://root:password@tcp(127.0.0.1:3306)/\")\n\tpostgresDSN = env(\"POSTGRES_URI\", \"postgres://gorse:gorse_pass@127.0.0.1/\")\n\tclickhouseDSN = env(\"CLICKHOUSE_URI\", \"clickhouse://127.0.0.1:8123/\")\n}\n\ntype MySQLTestSuite struct {\n\tbaseTestSuite\n}\n\nfunc (suite *MySQLTestSuite) SetupSuite() {\n\t// create database\n\tdatabaseComm, err := sql.Open(\"mysql\", mySqlDSN[len(storage.MySQLPrefix):])\n\tsuite.NoError(err)\n\tconst dbName = \"gorse_data_test\"\n\t_, err = databaseComm.Exec(\"DROP DATABASE IF EXISTS \" + dbName)\n\tsuite.NoError(err)\n\t_, err = databaseComm.Exec(\"CREATE DATABASE \" + dbName)\n\tsuite.NoError(err)\n\terr = databaseComm.Close()\n\tsuite.NoError(err)\n\t// connect database\n\tsuite.Database, err = Open(mySqlDSN+dbName, \"gorse_\")\n\tsuite.NoError(err)\n\t// create schema\n\terr = suite.Database.Init()\n\tsuite.NoError(err)\n}\n\nfunc (suite *MySQLTestSuite) TestInit() {\n\tname, err := storage.ProbeMySQLIsolationVariableName(mySqlDSN[len(storage.MySQLPrefix):])\n\tsuite.NoError(err)\n\tconnection := suite.Database.(*SQLDatabase).client\n\tassertQuery(suite.T(), connection, fmt.Sprintf(\"SELECT @@%s\", name), \"READ-UNCOMMITTED\")\n\tassertQuery(suite.T(), connection, \"SELECT @@sql_mode\", \"ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION\")\n}\n\nfunc TestMySQL(t *testing.T) {\n\tsuite.Run(t, new(MySQLTestSuite))\n}\n\ntype PostgresTestSuite struct {\n\tbaseTestSuite\n}\n\nfunc (suite *PostgresTestSuite) SetupSuite() {\n\tvar err error\n\t// create database\n\tdatabaseComm, err := sql.Open(\"postgres\", postgresDSN+\"?sslmode=disable\")\n\tsuite.NoError(err)\n\tconst dbName = \"gorse_data_test\"\n\t_, err = databaseComm.Exec(\"DROP DATABASE IF EXISTS \" + dbName)\n\tsuite.NoError(err)\n\t_, err = databaseComm.Exec(\"CREATE DATABASE \" + dbName)\n\tsuite.NoError(err)\n\terr = databaseComm.Close()\n\tsuite.NoError(err)\n\t// connect database\n\tsuite.Database, err = Open(postgresDSN+strings.ToLower(dbName)+\"?sslmode=disable\", \"gorse_\")\n\tsuite.NoError(err)\n\t// create schema\n\terr = suite.Database.Init()\n\tsuite.NoError(err)\n}\n\nfunc TestPostgres(t *testing.T) {\n\tsuite.Run(t, new(PostgresTestSuite))\n}\n\ntype ClickHouseTestSuite struct {\n\tbaseTestSuite\n}\n\nfunc (suite *ClickHouseTestSuite) SetupSuite() {\n\tvar err error\n\t// create database\n\tdatabaseComm, err := sql.Open(\"chhttp\", \"http://\"+clickhouseDSN[len(storage.ClickhousePrefix):])\n\tsuite.NoError(err)\n\tconst dbName = \"gorse_data_test\"\n\t_, err = databaseComm.Exec(\"DROP DATABASE IF EXISTS \" + dbName)\n\tsuite.NoError(err)\n\t_, err = databaseComm.Exec(\"CREATE DATABASE \" + dbName)\n\tsuite.NoError(err)\n\terr = databaseComm.Close()\n\tsuite.NoError(err)\n\t// connect database\n\tsuite.Database, err = Open(clickhouseDSN+dbName+\"?mutations_sync=2\", \"gorse_\")\n\tsuite.NoError(err)\n\t// create schema\n\terr = suite.Database.Init()\n\tsuite.NoError(err)\n}\n\nfunc TestClickHouse(t *testing.T) {\n\tsuite.Run(t, new(ClickHouseTestSuite))\n}\n\ntype SQLiteTestSuite struct {\n\tbaseTestSuite\n}\n\nfunc (suite *SQLiteTestSuite) SetupSuite() {\n\tvar err error\n\t// create database\n\tpath := fmt.Sprintf(\"sqlite://%s/sqlite.db\", suite.T().TempDir())\n\tsuite.Database, err = Open(path, \"gorse_\")\n\tsuite.NoError(err)\n\t// create schema\n\terr = suite.Database.Init()\n\tsuite.NoError(err)\n}\n\nfunc (suite *SQLiteTestSuite) TearDownSuite() {\n\tsuite.NoError(suite.Database.Close())\n}\n\nfunc TestSQLite(t *testing.T) {\n\tsuite.Run(t, new(SQLiteTestSuite))\n}\n\nfunc assertQuery(t *testing.T, connection *sql.DB, sql string, expected string) {\n\trows, err := connection.Query(sql)\n\tassert.NoError(t, err)\n\tassert.True(t, rows.Next())\n\tvar result string\n\terr = rows.Scan(&result)\n\tassert.NoError(t, err)\n\tassert.Equal(t, expected, result)\n}\n\nfunc BenchmarkMySQL_CountItems(b *testing.B) {\n\t// create database\n\tdatabase, err := Open(mySqlDSN, \"gorse_\")\n\trequire.NoError(b, err)\n\tdbName := \"gorse_data_test\"\n\tdatabaseComm := database.(*SQLDatabase)\n\t_, err = databaseComm.client.Exec(\"DROP DATABASE IF EXISTS \" + dbName)\n\trequire.NoError(b, err)\n\t_, err = databaseComm.client.Exec(\"CREATE DATABASE \" + dbName)\n\trequire.NoError(b, err)\n\tdatabase, err = Open(mySqlDSN+dbName, \"gorse_\")\n\trequire.NoError(b, err)\n\terr = database.Init()\n\trequire.NoError(b, err)\n\t// benchmark\n\tbenchmarkCountItems(b, database)\n\t// close database\n\terr = database.Close()\n\trequire.NoError(b, err)\n}\n\nfunc BenchmarkPostgres_CountItems(b *testing.B) {\n\t// create database\n\tdatabase, err := Open(postgresDSN+\"gorse_data_test?sslmode=disable\", \"gorse_\")\n\trequire.NoError(b, err)\n\terr = database.Init()\n\trequire.NoError(b, err)\n\t// benchmark\n\tbenchmarkCountItems(b, database)\n\t// close database\n\terr = database.Close()\n\trequire.NoError(b, err)\n}\n\nfunc BenchmarkClickHouse_CountItems(b *testing.B) {\n\t// create database\n\tdatabaseComm, err := sql.Open(\"chhttp\", \"http://\"+clickhouseDSN[len(storage.ClickhousePrefix):])\n\trequire.NoError(b, err)\n\tconst dbName = \"gorse_data_test\"\n\t_, err = databaseComm.Exec(\"DROP DATABASE IF EXISTS \" + dbName)\n\trequire.NoError(b, err)\n\t_, err = databaseComm.Exec(\"CREATE DATABASE \" + dbName)\n\trequire.NoError(b, err)\n\terr = databaseComm.Close()\n\trequire.NoError(b, err)\n\tdatabase, err := Open(clickhouseDSN+\"gorse_data_test?mutations_sync=2\", \"gorse_\")\n\trequire.NoError(b, err)\n\terr = database.Init()\n\trequire.NoError(b, err)\n\t// benchmark\n\tbenchmarkCountItems(b, database)\n\t// close database\n\terr = database.Close()\n\trequire.NoError(b, err)\n}\n\nfunc BenchmarkSQLite_CountItems(b *testing.B) {\n\t// create database\n\tdatabase, err := Open(\"sqlite://\"+os.TempDir()+\"/sqlite.db\", \"gorse_\")\n\trequire.NoError(b, err)\n\terr = database.Init()\n\trequire.NoError(b, err)\n\t// benchmark\n\tbenchmarkCountItems(b, database)\n\t// close database\n\terr = database.Close()\n\trequire.NoError(b, err)\n}\n"
  },
  {
    "path": "storage/docker-compose.yml",
    "content": "version: \"3\"\nservices:\n  redis:\n    image: redis/redis-stack:6.2.6-v9\n    ports:\n      - 6379:6379\n\n  mysql:\n    image: mysql:8.0\n    ports:\n      - 3306:3306\n    environment:\n      MYSQL_ROOT_PASSWORD: password\n      MYSQL_DATABASE: gorse\n      MYSQL_USER: gorse\n      MYSQL_PASSWORD: gorse_pass\n\n  postgres:\n    image: postgres:10.0\n    ports:\n      - 5432:5432\n    environment:\n      POSTGRES_USER: gorse\n      POSTGRES_PASSWORD: gorse_pass\n\n  mongo:\n    image: mongo:4.0\n    ports:\n      - 27017:27017\n    environment:\n      MONGO_INITDB_ROOT_USERNAME: root\n      MONGO_INITDB_ROOT_PASSWORD: password\n\n  clickhouse:\n    image: clickhouse/clickhouse-server:23\n    ports:\n      - 8123:8123\n  \n  rustfs:\n    image: rustfs/rustfs:alpha\n    ports:\n      - 9000:9000\n    environment:\n      RUSTFS_ACCESS_KEY: rustfsadmin\n      RUSTFS_SECRET_KEY: rustfsadmin\n\n  azurite:\n    image: mcr.microsoft.com/azure-storage/azurite:3.34.0\n    ports:\n      - 10000:10000\n    command: azurite-blob --blobHost 0.0.0.0 --blobPort 10000\n\n  qdrant:\n    image: qdrant/qdrant:latest\n    ports:\n      - 6333:6333\n      - 6334:6334\n\n  weaviate:\n    image: cr.weaviate.io/semitechnologies/weaviate:1.35.7\n    ports:\n      - 8080:8080\n\n  # Milvus\n  etcd:\n    image: quay.io/coreos/etcd:v3.5.25\n    environment:\n      - ETCD_AUTO_COMPACTION_MODE=revision\n      - ETCD_AUTO_COMPACTION_RETENTION=1000\n      - ETCD_QUOTA_BACKEND_BYTES=4294967296\n      - ETCD_SNAPSHOT_COUNT=50000\n    command: etcd -advertise-client-urls=http://etcd:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd\n    healthcheck:\n      test: [\"CMD\", \"etcdctl\", \"endpoint\", \"health\"]\n      interval: 30s\n      timeout: 20s\n      retries: 3\n\n  minio:\n    image: minio/minio:RELEASE.2024-12-18T13-15-44Z\n    environment:\n      MINIO_ACCESS_KEY: minioadmin\n      MINIO_SECRET_KEY: minioadmin\n    ports:\n      - 9001:9001\n      - 9000:9000\n    command: minio server /minio_data --console-address \":9001\"\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:9000/minio/health/live\"]\n      interval: 30s\n      timeout: 20s\n      retries: 3\n\n  milvus:\n    image: milvusdb/milvus:v2.6.9\n    command: [\"milvus\", \"run\", \"standalone\"]\n    security_opt:\n    - seccomp:unconfined\n    environment:\n      ETCD_ENDPOINTS: etcd:2379\n      MINIO_ADDRESS: minio:9000\n      MQ_TYPE: woodpecker\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:9091/healthz\"]\n      interval: 30s\n      start_period: 90s\n      timeout: 20s\n      retries: 3\n    ports:\n      - 19530:19530\n      - 9091:9091\n    depends_on:\n      - etcd\n      - minio\n"
  },
  {
    "path": "storage/meta/database.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage meta\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/XSAM/otelsql\"\n\t\"github.com/gorse-io/gorse/model\"\n\t\"github.com/gorse-io/gorse/storage\"\n\t\"github.com/juju/errors\"\n\t\"github.com/samber/lo\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.12.0\"\n\t\"golang.org/x/exp/maps\"\n)\n\nconst (\n\tCOLLABORATIVE_FILTERING_MODEL = \"COLLABORATIVE_FILTERING_MODEL\"\n\tCLICK_THROUGH_RATE_MODEL      = \"CLICK_THROUGH_RATE_MODEL\"\n\tRECOMMEND_CONFIG              = \"RECOMMEND_CONFIG\"\n)\n\ntype Model[T any] struct {\n\tID     int64\n\tType   string\n\tParams model.Params\n\tScore  T\n}\n\nfunc (m *Model[T]) ToJSON() string {\n\treturn string(lo.Must1(json.Marshal(m)))\n}\n\nfunc (m *Model[T]) FromJSON(data string) error {\n\treturn json.Unmarshal([]byte(data), m)\n}\n\n// Equal checks if two models have the same type and parameters.\nfunc (m *Model[T]) Equal(other Model[T]) bool {\n\treturn m.Type == other.Type && maps.Equal(m.Params, other.Params)\n}\n\ntype Node struct {\n\tUUID       string\n\tHostname   string\n\tType       string\n\tVersion    string\n\tUpdateTime time.Time\n}\n\ntype Database interface {\n\tClose() error\n\tInit() error\n\tUpdateNode(node *Node) error\n\tListNodes() ([]*Node, error)\n\tPut(key, value string) error\n\tGet(key string) (*string, error)\n\tDelete(key string) error\n}\n\n// Open a connection to a database.\nfunc Open(path string, ttl time.Duration) (Database, error) {\n\tvar err error\n\tif strings.HasPrefix(path, storage.SQLitePrefix) {\n\t\tdataSourceName := path[len(storage.SQLitePrefix):]\n\t\t// append parameters\n\t\tif dataSourceName, err = storage.AppendURLParams(dataSourceName, []lo.Tuple2[string, string]{\n\t\t\t{\"_pragma\", \"busy_timeout(10000)\"},\n\t\t\t{\"_pragma\", \"journal_mode(wal)\"},\n\t\t}); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\t// connect to database\n\t\tdatabase := new(SQLite)\n\t\tdatabase.ttl = ttl\n\t\tif database.db, err = otelsql.Open(\"sqlite\", dataSourceName,\n\t\t\totelsql.WithAttributes(semconv.DBSystemSqlite),\n\t\t\totelsql.WithSpanOptions(otelsql.SpanOptions{DisableErrSkip: true}),\n\t\t); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\treturn database, nil\n\t}\n\treturn nil, errors.Errorf(\"Unknown database: %s\", path)\n}\n"
  },
  {
    "path": "storage/meta/database_test.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage meta\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/model\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\ntype baseTestSuite struct {\n\tsuite.Suite\n\tDatabase\n}\n\nfunc (suite *baseTestSuite) TestNodes() {\n\t// Add node\n\terr := suite.Database.UpdateNode(&Node{\n\t\tUUID:       \"node-1\",\n\t\tHostname:   \"localhost\",\n\t\tType:       \"master\",\n\t\tVersion:    \"v0.1.0\",\n\t\tUpdateTime: time.Now(),\n\t})\n\tsuite.NoError(err)\n\t// Add duplicate node\n\terr = suite.Database.UpdateNode(&Node{\n\t\tUUID:       \"node-1\",\n\t\tHostname:   \"localhost\",\n\t\tType:       \"master\",\n\t\tVersion:    \"v0.1.1\",\n\t\tUpdateTime: time.Now(),\n\t})\n\tsuite.NoError(err)\n\t// Add outdated node\n\terr = suite.Database.UpdateNode(&Node{\n\t\tUUID:       \"node-2\",\n\t\tHostname:   \"localhost\",\n\t\tType:       \"master\",\n\t\tVersion:    \"v0.1.0\",\n\t\tUpdateTime: time.Now().Add(-time.Hour),\n\t})\n\tsuite.NoError(err)\n\t// List nodes\n\tnodes, err := suite.Database.ListNodes()\n\tsuite.NoError(err)\n\tif suite.Equal(1, len(nodes)) {\n\t\tsuite.Equal(\"node-1\", nodes[0].UUID)\n\t\tsuite.Equal(\"localhost\", nodes[0].Hostname)\n\t\tsuite.Equal(\"master\", nodes[0].Type)\n\t\tsuite.Equal(\"v0.1.1\", nodes[0].Version)\n\t}\n}\n\nfunc (suite *baseTestSuite) TestKeyValues() {\n\terr := suite.Database.Put(\"key1\", \"value1\")\n\tsuite.NoError(err)\n\terr = suite.Database.Put(\"key2\", \"value2\")\n\tsuite.NoError(err)\n\terr = suite.Database.Put(\"key3\", \"value3\")\n\tsuite.NoError(err)\n\n\tvalue, err := suite.Database.Get(\"key1\")\n\tsuite.NoError(err)\n\tsuite.Equal(\"value1\", *value)\n\n\tvalue, err = suite.Database.Get(\"key2\")\n\tsuite.NoError(err)\n\tsuite.Equal(\"value2\", *value)\n\n\tvalue, err = suite.Database.Get(\"key3\")\n\tsuite.NoError(err)\n\tsuite.Equal(\"value3\", *value)\n\n\t// Test overwrite\n\terr = suite.Database.Put(\"key1\", \"new_value1\")\n\tsuite.NoError(err)\n\tvalue, err = suite.Database.Get(\"key1\")\n\tsuite.NoError(err)\n\tsuite.Equal(\"new_value1\", *value)\n\n\t// Test non-existing key\n\tvalue, err = suite.Database.Get(\"non-existing-key\")\n\tsuite.NoError(err)\n\tsuite.Nil(value)\n\n\t// Test delete existing key\n\terr = suite.Database.Delete(\"key2\")\n\tsuite.NoError(err)\n\tvalue, err = suite.Database.Get(\"key2\")\n\tsuite.NoError(err)\n\tsuite.Nil(value)\n\n\t// Test delete non-existing key\n\terr = suite.Database.Delete(\"non-existing-key\")\n\tsuite.NoError(err)\n}\n\nfunc TestModel_Equal(t *testing.T) {\n\ta := Model[int]{\n\t\tID:   1,\n\t\tType: \"test\",\n\t\tParams: map[model.ParamName]any{\n\t\t\t\"param1\": 1,\n\t\t\t\"param2\": \"value2\",\n\t\t},\n\t\tScore: 0,\n\t}\n\tb := Model[int]{\n\t\tID:   2,\n\t\tType: \"test\",\n\t\tParams: map[model.ParamName]any{\n\t\t\t\"param1\": 1,\n\t\t\t\"param2\": \"value2\",\n\t\t},\n\t\tScore: 1,\n\t}\n\tassert.True(t, a.Equal(b))\n\n\ta.Type = \"different\"\n\tassert.False(t, a.Equal(b))\n\ta.Type = \"test\"\n\n\ta.Params[\"param2\"] = \"different\"\n\tassert.False(t, a.Equal(b))\n\ta.Params[\"param2\"] = \"value2\"\n}\n"
  },
  {
    "path": "storage/meta/sqlite.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage meta\n\nimport (\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t_ \"modernc.org/sqlite\"\n)\n\ntype SQLite struct {\n\tdb  *sql.DB\n\tttl time.Duration\n}\n\nfunc (s *SQLite) Close() error {\n\treturn s.db.Close()\n}\n\nfunc (s *SQLite) Init() error {\n\t// Create tables\n\tif _, err := s.db.Exec(`\nCREATE TABLE IF NOT EXISTS nodes (\n\tuuid TEXT PRIMARY KEY,\n\thostname TEXT,\n\ttype TEXT,\n\tversion TEXT,\n\tupdate_time DATETIME\n);`); err != nil {\n\t\treturn err\n\t}\n\tif _, err := s.db.Exec(`\nCREATE TABLE IF NOT EXISTS cron_jobs (\n\tname TEXT PRIMARY KEY,\n\tdescription TEXT,\n\tcurrent INTEGER,\n\ttotal INTEGER,\n\tstart_time TIMESTAMP,\n\tend_time TIMESTAMP,\n\tupdate_time TIMESTAMP\n);`); err != nil {\n\t\treturn err\n\t}\n\tif _, err := s.db.Exec(`\nCREATE TABLE IF NOT EXISTS key_values (\n\tkey TEXT PRIMARY KEY,\n\tvalue TEXT\n);`); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (s *SQLite) UpdateNode(node *Node) error {\n\t_, err := s.db.Exec(`\nINSERT INTO nodes (uuid, hostname, type, version, update_time)\nVALUES (?, ?, ?, ?, ?)\nON CONFLICT(uuid) DO UPDATE SET\n\thostname = excluded.hostname,\n\ttype = excluded.type,\n\tversion = excluded.version,\n\tupdate_time = excluded.update_time\n`, node.UUID, node.Hostname, node.Type, node.Version, node.UpdateTime.UTC())\n\treturn err\n}\n\nfunc (s *SQLite) ListNodes() ([]*Node, error) {\n\t// List nodes within TTL\n\trs, err := s.db.Query(`\nSELECT uuid, hostname, type, version, update_time FROM nodes\nWHERE update_time > datetime('now', ?)\n`, fmt.Sprintf(\"-%.0f seconds\", s.ttl.Seconds()))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rs.Close()\n\tvar nodes []*Node\n\tfor rs.Next() {\n\t\tvar node Node\n\t\tif err = rs.Scan(&node.UUID, &node.Hostname, &node.Type, &node.Version, &node.UpdateTime); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tnodes = append(nodes, &node)\n\t}\n\t// Delete outdated nodes\n\tif _, err = s.db.Exec(`\nDELETE FROM nodes WHERE update_time < datetime('now', ?)\n`, fmt.Sprintf(\"-%.0f seconds\", s.ttl.Seconds())); err != nil {\n\t\treturn nil, err\n\t}\n\treturn nodes, nil\n}\n\nfunc (s *SQLite) Put(key, value string) error {\n\t_, err := s.db.Exec(`\nINSERT INTO key_values (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value\n`, key, value)\n\treturn err\n}\n\nfunc (s *SQLite) Get(key string) (*string, error) {\n\tvar value string\n\terr := s.db.QueryRow(`\nSELECT value FROM key_values WHERE key = ?\n`, key).Scan(&value)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, nil // key not found\n\t\t}\n\t\treturn nil, err // other error\n\t}\n\treturn &value, nil // key found\n}\n\nfunc (s *SQLite) Delete(key string) error {\n\t_, err := s.db.Exec(`\nDELETE FROM key_values WHERE key = ?\n`, key)\n\treturn err\n}\n"
  },
  {
    "path": "storage/meta/sqlite_test.go",
    "content": "// Copyright 2024 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage meta\n\nimport (\n\t\"fmt\"\n\t\"github.com/stretchr/testify/suite\"\n\t\"testing\"\n\t\"time\"\n)\n\ntype SQLiteTestSuite struct {\n\tbaseTestSuite\n}\n\nfunc (suite *SQLiteTestSuite) SetupTest() {\n\tvar err error\n\t// create database\n\tpath := fmt.Sprintf(\"sqlite://%s/sqlite.db\", suite.T().TempDir())\n\tsuite.Database, err = Open(path, time.Second)\n\tsuite.NoError(err)\n\t// create schema\n\terr = suite.Database.Init()\n\tsuite.NoError(err)\n}\n\nfunc (suite *SQLiteTestSuite) TearDownTest() {\n\tsuite.NoError(suite.Database.Close())\n}\n\nfunc TestSQLite(t *testing.T) {\n\tsuite.Run(t, new(SQLiteTestSuite))\n}\n"
  },
  {
    "path": "storage/options.go",
    "content": "package storage\n\nimport (\n\t\"database/sql\"\n\t\"time\"\n)\n\ntype Options struct {\n\tIsolationLevel   string\n\tMaxOpenConns     int\n\tMaxIdleConns     int\n\tConnMaxLifetime  time.Duration\n\tMaxSearchResults int\n}\n\ntype Option func(*Options)\n\nfunc WithIsolationLevel(isolationLevel string) Option {\n\treturn func(o *Options) {\n\t\to.IsolationLevel = isolationLevel\n\t}\n}\n\nfunc WithMaxOpenConns(maxOpenConns int) Option {\n\treturn func(o *Options) {\n\t\to.MaxOpenConns = maxOpenConns\n\t}\n}\n\nfunc WithMaxIdleConns(maxIdleConns int) Option {\n\treturn func(o *Options) {\n\t\to.MaxIdleConns = maxIdleConns\n\t}\n}\n\nfunc WithConnMaxLifetime(connMaxLifetime time.Duration) Option {\n\treturn func(o *Options) {\n\t\to.ConnMaxLifetime = connMaxLifetime\n\t}\n}\n\nfunc WithMaxSearchResults(limit int) Option {\n\treturn func(o *Options) {\n\t\to.MaxSearchResults = limit\n\t}\n}\n\nfunc ApplySQLPool(db *sql.DB, opt Options) {\n\tif opt.MaxOpenConns > 0 {\n\t\tdb.SetMaxOpenConns(opt.MaxOpenConns)\n\t}\n\tif opt.MaxIdleConns > 0 {\n\t\tdb.SetMaxIdleConns(opt.MaxIdleConns)\n\t}\n\tif opt.ConnMaxLifetime > 0 {\n\t\tdb.SetConnMaxLifetime(opt.ConnMaxLifetime)\n\t}\n}\n\nfunc NewOptions(opts ...Option) Options {\n\topt := Options{\n\t\tIsolationLevel:   \"READ-UNCOMMITTED\",\n\t\tMaxSearchResults: 10000,\n\t}\n\tfor _, o := range opts {\n\t\to(&opt)\n\t}\n\treturn opt\n}\n"
  },
  {
    "path": "storage/schema_test.go",
    "content": "package storage\n\nimport (\n\t\"github.com/samber/lo\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"testing\"\n)\n\nfunc TestAppendURLParams(t *testing.T) {\n\t// test windows path\n\turl, err := AppendURLParams(`c:\\\\sqlite.db`, []lo.Tuple2[string, string]{{\"a\", \"b\"}})\n\tassert.NoError(t, err)\n\tassert.Equal(t, `c:\\\\sqlite.db?a=b`, url)\n\t// test no scheme\n\turl, err = AppendURLParams(`sqlite.db`, []lo.Tuple2[string, string]{{\"a\", \"b\"}})\n\tassert.NoError(t, err)\n\tassert.Equal(t, `sqlite.db?a=b`, url)\n}\n"
  },
  {
    "path": "storage/scheme.go",
    "content": "// Copyright 2022 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage storage\n\nimport (\n\t\"database/sql\"\n\t\"database/sql/driver\"\n\t\"encoding/json\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/go-sql-driver/mysql\"\n\t\"github.com/juju/errors\"\n\t\"github.com/samber/lo\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/logger\"\n\t\"gorm.io/gorm/schema\"\n\t\"modernc.org/sqlite\"\n)\n\nfunc init() {\n\tsqlite.MustRegisterDeterministicScalarFunction(\"json_contains\", 2, func(ctx *sqlite.FunctionContext, args []driver.Value) (driver.Value, error) {\n\t\tparse := func(arg driver.Value) (j []any, err error) {\n\t\t\tvar data []byte\n\t\t\tswitch argTyped := arg.(type) {\n\t\t\tcase string:\n\t\t\t\tdata = []byte(argTyped)\n\t\t\tcase []byte:\n\t\t\t\tdata = argTyped\n\t\t\tdefault:\n\t\t\t\treturn nil, errors.Errorf(\"unsupported type %T\", arg)\n\t\t\t}\n\t\t\terr = json.Unmarshal(data, &j)\n\t\t\treturn\n\t\t}\n\t\tif args[0] == nil || args[1] == nil {\n\t\t\treturn nil, nil\n\t\t}\n\t\tj1, err := parse(args[0])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tj2, err := parse(args[1])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\telements := make(map[any]struct{}, len(j1))\n\t\tfor _, e := range j1 {\n\t\t\telements[e] = struct{}{}\n\t\t}\n\t\tfor _, e := range j2 {\n\t\t\tif _, ok := elements[e]; !ok {\n\t\t\t\treturn false, nil\n\t\t\t}\n\t\t}\n\t\treturn true, nil\n\t})\n}\n\nconst (\n\tMySQLPrefix         = \"mysql://\"\n\tMongoPrefix         = \"mongodb://\"\n\tMongoSrvPrefix      = \"mongodb+srv://\"\n\tPostgresPrefix      = \"postgres://\"\n\tPostgreSQLPrefix    = \"postgresql://\"\n\tClickhousePrefix    = \"clickhouse://\"\n\tCHHTTPPrefix        = \"chhttp://\"\n\tCHHTTPSPrefix       = \"chhttps://\"\n\tSQLitePrefix        = \"sqlite://\"\n\tRedisPrefix         = \"redis://\"\n\tRedissPrefix        = \"rediss://\"\n\tRedisClusterPrefix  = \"redis+cluster://\"\n\tRedissClusterPrefix = \"rediss+cluster://\"\n\tQdrantPrefix        = \"qdrant://\"\n\tWeaviatePrefix      = \"weaviate://\"\n\tWeaviatesPrefix     = \"weaviates://\"\n\tMilvusPrefix        = \"milvus://\"\n)\n\nfunc AppendURLParams(rawURL string, params []lo.Tuple2[string, string]) (string, error) {\n\tparsed, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn \"\", errors.Trace(err)\n\t}\n\tq := parsed.Query()\n\tfor _, tuple := range params {\n\t\tq.Add(tuple.A, tuple.B)\n\t}\n\tparsed.RawQuery = q.Encode()\n\treturn parsed.String(), nil\n}\n\nfunc AppendMySQLParams(dsn string, params map[string]string) (string, error) {\n\tcfg, err := mysql.ParseDSN(dsn)\n\tif err != nil {\n\t\treturn \"\", errors.Trace(err)\n\t}\n\tif cfg.Params == nil {\n\t\tcfg.Params = make(map[string]string)\n\t}\n\tfor key, value := range params {\n\t\tif _, exist := cfg.Params[key]; !exist {\n\t\t\tcfg.Params[key] = value\n\t\t}\n\t}\n\treturn cfg.FormatDSN(), nil\n}\n\nfunc ProbeMySQLIsolationVariableName(dsn string) (string, error) {\n\tconnection, err := sql.Open(\"mysql\", dsn)\n\tif err != nil {\n\t\treturn \"\", errors.Trace(err)\n\t}\n\tdefer connection.Close()\n\trows, err := connection.Query(\"SHOW VARIABLES WHERE variable_name = 'transaction_isolation' OR variable_name = 'tx_isolation'\")\n\tif err != nil {\n\t\treturn \"\", errors.Trace(err)\n\t}\n\tdefer rows.Close()\n\tvar name, value string\n\tif rows.Next() {\n\t\tif err = rows.Scan(&name, &value); err != nil {\n\t\t\treturn \"\", errors.Trace(err)\n\t\t}\n\t}\n\treturn name, nil\n}\n\ntype TablePrefix string\n\nfunc (tp TablePrefix) ValuesTable() string {\n\treturn string(tp) + \"values\"\n}\n\nfunc (tp TablePrefix) SetsTable() string {\n\treturn string(tp) + \"sets\"\n}\n\nfunc (tp TablePrefix) MessageTable() string {\n\treturn string(tp) + \"message\"\n}\n\nfunc (tp TablePrefix) DocumentTable() string {\n\treturn string(tp) + \"documents\"\n}\n\nfunc (tp TablePrefix) PointsTable() string {\n\treturn string(tp) + \"time_series_points\"\n}\n\nfunc (tp TablePrefix) UsersTable() string {\n\treturn string(tp) + \"users\"\n}\n\nfunc (tp TablePrefix) ItemsTable() string {\n\treturn string(tp) + \"items\"\n}\n\n// LatestItemsTable returns the materialized view for latest items.\nfunc (tp TablePrefix) LatestItemsTable() string {\n\treturn string(tp) + \"latest_items\"\n}\n\nfunc (tp TablePrefix) FeedbackTable() string {\n\treturn string(tp) + \"feedback\"\n}\n\n// AggregatingFeedbackTable returns the aggregating feedback table.\nfunc (tp TablePrefix) AggregatingFeedbackTable() string {\n\treturn string(tp) + \"aggregating_feedback\"\n}\n\n// UserFeedbackTable returns the materialized view of user feedback.\nfunc (tp TablePrefix) UserFeedbackTable() string {\n\treturn string(tp) + \"user_feedback\"\n}\n\n// ItemFeedbackTable returns the materialized view of item feedback.\nfunc (tp TablePrefix) ItemFeedbackTable() string {\n\treturn string(tp) + \"item_feedback\"\n}\n\nfunc (tp TablePrefix) Key(key string) string {\n\treturn string(tp) + key\n}\n\nfunc NewGORMConfig(tablePrefix string) *gorm.Config {\n\treturn &gorm.Config{\n\t\tLogger:                 logger.Default.LogMode(logger.Silent),\n\t\tCreateBatchSize:        1000,\n\t\tSkipDefaultTransaction: true,\n\t\tNamingStrategy: schema.NamingStrategy{\n\t\t\tTablePrefix:   tablePrefix,\n\t\t\tSingularTable: true,\n\t\t\tNameReplacer: strings.NewReplacer(\n\t\t\t\t\"SQLValue\", \"Values\",\n\t\t\t\t\"SQLSet\", \"Sets\",\n\t\t\t\t\"SQLUser\", \"Users\",\n\t\t\t\t\"SQLItem\", \"Items\",\n\t\t\t\t\"SQLFeedback\", \"Feedback\",\n\t\t\t\t\"SQLDocument\", \"Documents\",\n\t\t\t\t\"PostgresDocument\", \"Documents\",\n\t\t\t\t\"TimeSeriesPoint\", \"time_series_points\",\n\t\t\t\t\"ClickhouseUser\", \"Users\",\n\t\t\t\t\"ClickHouseItem\", \"Items\",\n\t\t\t\t\"ClickHouseFeedback\", \"Feedback\",\n\t\t\t),\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "storage/vectors/database.go",
    "content": "// Copyright 2026 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage vectors\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/storage\"\n\t\"github.com/juju/errors\"\n)\n\ntype Distance int\n\nconst (\n\tCosine Distance = iota\n\tEuclidean\n\tDot\n)\n\ntype Vector struct {\n\tId         string\n\tVector     []float32\n\tIsHidden   bool      `json:\"-\"`\n\tCategories []string  `json:\"-\" gorm:\"type:text;serializer:json\"`\n\tTimestamp  time.Time `json:\"-\"`\n}\n\ntype Database interface {\n\tInit() error\n\tOptimize() error\n\tClose() error\n\tListCollections(ctx context.Context) ([]string, error)\n\tAddCollection(ctx context.Context, name string, dimensions int, distance Distance) error\n\tDeleteCollection(ctx context.Context, name string) error\n\tAddVectors(ctx context.Context, collection string, vectors []Vector) error\n\tDeleteVectors(ctx context.Context, collection string, timestamp time.Time) error\n\tQueryVectors(ctx context.Context, collection string, q []float32, categories []string, topK int) ([]Vector, error)\n}\n\n// Creator creates a database instance.\ntype Creator func(path, tablePrefix string, opts ...storage.Option) (Database, error)\n\nvar creators = make(map[string]Creator)\n\n// Register a database creator.\nfunc Register(prefixes []string, creator Creator) {\n\tfor _, p := range prefixes {\n\t\tcreators[p] = creator\n\t}\n}\n\n// Open a connection to a database.\nfunc Open(path, tablePrefix string, opts ...storage.Option) (Database, error) {\n\tfor prefix, creator := range creators {\n\t\tif strings.HasPrefix(path, prefix) {\n\t\t\treturn creator(path, tablePrefix, opts...)\n\t\t}\n\t}\n\treturn nil, errors.Errorf(\"Unknown database: %s\", path)\n}\n"
  },
  {
    "path": "storage/vectors/database_test.go",
    "content": "// Copyright 2026 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage vectors\n\nimport (\n\t\"time\"\n\n\t\"github.com/stretchr/testify/suite\"\n)\n\nconst defaultVectorSize = 4\n\ntype vectorsTestSuite struct {\n\tsuite.Suite\n\tDatabase\n}\n\nfunc (suite *vectorsTestSuite) SetupTest() {\n\t// purge\n\tctx := suite.T().Context()\n\tcollections, err := suite.Database.ListCollections(ctx)\n\tsuite.NoError(err)\n\tfor _, collection := range collections {\n\t\terr = suite.Database.DeleteCollection(ctx, collection)\n\t\tsuite.NoError(err)\n\t}\n}\n\nfunc (suite *vectorsTestSuite) TestCollections() {\n\tctx := suite.T().Context()\n\t// list collections\n\tcollections, err := suite.Database.ListCollections(ctx)\n\tsuite.NoError(err)\n\tsuite.Empty(collections)\n\t// create collection\n\terr = suite.Database.AddCollection(ctx, \"test\", defaultVectorSize, Cosine)\n\tsuite.NoError(err)\n\t// list collections\n\tcollections, err = suite.Database.ListCollections(ctx)\n\tsuite.NoError(err)\n\tsuite.Equal([]string{\"test\"}, collections)\n\t// delete collection\n\terr = suite.Database.DeleteCollection(ctx, \"test\")\n\tsuite.NoError(err)\n\t// list collections\n\tcollections, err = suite.Database.ListCollections(ctx)\n\tsuite.NoError(err)\n\tsuite.Empty(collections)\n\t// delete non-existent collection\n\terr = suite.Database.DeleteCollection(ctx, \"non-existent\")\n\tsuite.Error(err)\n}\n\nfunc (suite *vectorsTestSuite) TestVectors() {\n\tctx := suite.T().Context()\n\terr := suite.Database.AddCollection(ctx, \"test\", defaultVectorSize, Cosine)\n\tsuite.NoError(err)\n\n\tvectorA := make([]float32, defaultVectorSize)\n\tvectorA[0] = 1\n\tvectorB := make([]float32, defaultVectorSize)\n\tvectorB[0] = 0.9\n\tvectorB[1] = 0.1\n\n\terr = suite.Database.AddVectors(ctx, \"test\", []Vector{\n\t\t{\n\t\t\tId:         \"a\",\n\t\t\tVector:     vectorA,\n\t\t\tCategories: []string{\"cat-a\", \"common\"},\n\t\t},\n\t\t{\n\t\t\tId:         \"b\",\n\t\t\tVector:     vectorB,\n\t\t\tCategories: []string{\"cat-b\", \"common\"},\n\t\t},\n\t})\n\tsuite.NoError(err)\n\n\tresults, err := suite.Database.QueryVectors(ctx, \"test\", vectorA, []string{\"cat-a\"}, 10)\n\tsuite.NoError(err)\n\tsuite.Len(results, 1)\n\tsuite.Equal(\"a\", results[0].Id)\n\tsuite.NotEmpty(results[0].Categories)\n\n\tresults, err = suite.Database.QueryVectors(ctx, \"test\", vectorA, []string{\"common\"}, 10)\n\tsuite.NoError(err)\n\tsuite.Len(results, 2)\n\tids := map[string]bool{}\n\tfor _, result := range results {\n\t\tids[result.Id] = true\n\t\tsuite.NotEmpty(result.Categories)\n\t}\n\tsuite.True(ids[\"a\"])\n\tsuite.True(ids[\"b\"])\n\n\tresults, err = suite.Database.QueryVectors(ctx, \"test\", vectorA, nil, 1)\n\tsuite.NoError(err)\n\tsuite.NotEmpty(results)\n\tfor _, result := range results {\n\t\tsuite.NotEmpty(result.Categories)\n\t}\n}\n\nfunc (suite *vectorsTestSuite) TestDeleteVectors() {\n\tctx := suite.T().Context()\n\terr := suite.Database.AddCollection(ctx, \"test\", defaultVectorSize, Cosine)\n\tsuite.NoError(err)\n\n\tvectorA := make([]float32, defaultVectorSize)\n\tvectorA[0] = 1\n\tvectorB := make([]float32, defaultVectorSize)\n\tvectorB[0] = 0.9\n\tvectorB[1] = 0.1\n\n\tcutoff := time.Now().UTC().Truncate(time.Millisecond)\n\terr = suite.Database.AddVectors(ctx, \"test\", []Vector{\n\t\t{\n\t\t\tId:         \"old\",\n\t\t\tVector:     vectorA,\n\t\t\tCategories: []string{\"common\"},\n\t\t\tTimestamp:  cutoff.Add(-time.Hour),\n\t\t},\n\t\t{\n\t\t\tId:         \"new\",\n\t\t\tVector:     vectorB,\n\t\t\tCategories: []string{\"common\"},\n\t\t\tTimestamp:  cutoff,\n\t\t},\n\t})\n\tsuite.NoError(err)\n\n\terr = suite.Database.DeleteVectors(ctx, \"test\", cutoff)\n\tsuite.NoError(err)\n\n\tresults, err := suite.Database.QueryVectors(ctx, \"test\", vectorA, []string{\"common\"}, 10)\n\tsuite.NoError(err)\n\tsuite.Len(results, 1)\n\tsuite.Equal(\"new\", results[0].Id)\n}\n"
  },
  {
    "path": "storage/vectors/milvus.go",
    "content": "// Copyright 2026 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage vectors\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/storage\"\n\t\"github.com/juju/errors\"\n\t\"github.com/milvus-io/milvus-sdk-go/v2/client\"\n\t\"github.com/milvus-io/milvus-sdk-go/v2/entity\"\n)\n\nconst (\n\tmilvusIdField         = \"id\"\n\tmilvusVectorField     = \"vector\"\n\tmilvusCategoriesField = \"categories\"\n\tmilvusTimestampField  = \"timestamp\"\n)\n\nfunc init() {\n\tRegister([]string{storage.MilvusPrefix}, func(path, tablePrefix string, opts ...storage.Option) (Database, error) {\n\t\tdatabase := new(Milvus)\n\t\tu, err := url.Parse(path)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tdatabase.client, err = client.NewClient(context.Background(), client.Config{\n\t\t\tAddress: u.Host,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\treturn database, nil\n\t})\n}\n\ntype Milvus struct {\n\tclient client.Client\n}\n\nfunc (db *Milvus) Init() error {\n\treturn nil\n}\n\nfunc (db *Milvus) Optimize() error {\n\treturn nil\n}\n\nfunc (db *Milvus) Close() error {\n\treturn db.client.Close()\n}\n\nfunc (db *Milvus) ListCollections(ctx context.Context) ([]string, error) {\n\tcollections, err := db.client.ListCollections(ctx)\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\tvar names []string\n\tfor _, collection := range collections {\n\t\tnames = append(names, collection.Name)\n\t}\n\treturn names, nil\n}\n\nfunc (db *Milvus) AddCollection(ctx context.Context, name string, dimensions int, distance Distance) error {\n\tschema := entity.NewSchema().WithName(name).WithDescription(\"gorse collection\").\n\t\tWithField(entity.NewField().WithName(milvusIdField).WithDataType(entity.FieldTypeVarChar).WithMaxLength(65535).WithIsPrimaryKey(true)).\n\t\tWithField(entity.NewField().WithName(milvusCategoriesField).WithDataType(entity.FieldTypeArray).WithElementType(entity.FieldTypeVarChar).WithMaxCapacity(100).WithMaxLength(65535)).\n\t\tWithField(entity.NewField().WithName(milvusTimestampField).WithDataType(entity.FieldTypeInt64)).\n\t\tWithField(entity.NewField().WithName(milvusVectorField).WithDataType(entity.FieldTypeFloatVector).WithDim(int64(dimensions)))\n\n\terr := db.client.CreateCollection(ctx, schema, entity.DefaultShardNumber)\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\n\t// Create index\n\tvar metricType entity.MetricType\n\tswitch distance {\n\tcase Cosine:\n\t\tmetricType = entity.COSINE\n\tcase Euclidean:\n\t\tmetricType = entity.L2\n\tcase Dot:\n\t\tmetricType = entity.IP\n\tdefault:\n\t\treturn errors.NotSupportedf(\"distance method\")\n\t}\n\tidx, err := entity.NewIndexHNSW(metricType, 8, 200)\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\terr = db.client.CreateIndex(ctx, name, milvusVectorField, idx, false)\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tscalarIdx := entity.NewScalarIndex()\n\terr = db.client.CreateIndex(ctx, name, milvusTimestampField, scalarIdx, false)\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\n\t// Load collection\n\terr = db.client.LoadCollection(ctx, name, false)\n\treturn errors.Trace(err)\n}\n\nfunc (db *Milvus) DeleteCollection(ctx context.Context, name string) error {\n\texists, err := db.client.HasCollection(ctx, name)\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif !exists {\n\t\treturn errors.NotFoundf(\"collection %s\", name)\n\t}\n\terr = db.client.DropCollection(ctx, name)\n\treturn errors.Trace(err)\n}\n\nfunc (db *Milvus) AddVectors(ctx context.Context, collection string, vectors []Vector) error {\n\tif len(vectors) == 0 {\n\t\treturn nil\n\t}\n\tids := make([]string, 0, len(vectors))\n\tcategories := make([][]string, 0, len(vectors))\n\ttimestamps := make([]int64, 0, len(vectors))\n\tdata := make([][]float32, 0, len(vectors))\n\tfor _, v := range vectors {\n\t\tids = append(ids, v.Id)\n\t\tcategories = append(categories, v.Categories)\n\t\ttimestamps = append(timestamps, v.Timestamp.UnixMilli())\n\t\tdata = append(data, v.Vector)\n\t}\n\n\tidCol := entity.NewColumnVarChar(milvusIdField, ids)\n\tcategoriesCol := entity.NewColumnVarCharArray(milvusCategoriesField, milvusStringsToBytes(categories))\n\ttimestampCol := entity.NewColumnInt64(milvusTimestampField, timestamps)\n\tvectorCol := entity.NewColumnFloatVector(milvusVectorField, len(data[0]), data)\n\n\t_, err := db.client.Upsert(ctx, collection, \"\", idCol, categoriesCol, timestampCol, vectorCol)\n\treturn errors.Trace(err)\n}\n\nfunc (db *Milvus) DeleteVectors(ctx context.Context, collection string, timestamp time.Time) error {\n\terr := db.client.Delete(ctx, collection, \"\", fmt.Sprintf(\"%s < %d\", milvusTimestampField, timestamp.UnixMilli()))\n\treturn errors.Trace(err)\n}\n\nfunc (db *Milvus) QueryVectors(ctx context.Context, collection string, q []float32, categories []string, topK int) ([]Vector, error) {\n\tif topK <= 0 {\n\t\treturn []Vector{}, nil\n\t}\n\n\tvar expr string\n\tif len(categories) > 0 {\n\t\tvar conditions []string\n\t\tfor _, category := range categories {\n\t\t\tconditions = append(conditions, fmt.Sprintf(\"array_contains(%s, '%s')\", milvusCategoriesField, category))\n\t\t}\n\t\texpr = strings.Join(conditions, \" or \")\n\t}\n\n\tsearchParam, _ := entity.NewIndexHNSWSearchParam(64)\n\tresults, err := db.client.Search(ctx, collection, []string{}, expr, []string{milvusIdField, milvusCategoriesField}, []entity.Vector{entity.FloatVector(q)}, milvusVectorField, entity.COSINE, topK, searchParam, client.WithSearchQueryConsistencyLevel(entity.ClStrong))\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\n\tvar vectors []Vector\n\tfor _, result := range results {\n\t\tvar idCol *entity.ColumnVarChar\n\t\tif col := result.Fields.GetColumn(milvusIdField); col != nil {\n\t\t\tidCol = col.(*entity.ColumnVarChar)\n\t\t} else if result.IDs != nil {\n\t\t\tidCol = result.IDs.(*entity.ColumnVarChar)\n\t\t}\n\n\t\tvar categoriesCol *entity.ColumnVarCharArray\n\t\tif col := result.Fields.GetColumn(milvusCategoriesField); col != nil {\n\t\t\tcategoriesCol = col.(*entity.ColumnVarCharArray)\n\t\t}\n\n\t\tfor i := 0; i < result.ResultCount; i++ {\n\t\t\tvar id string\n\t\t\tif idCol != nil {\n\t\t\t\tid, err = idCol.ValueByIdx(i)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, errors.Trace(err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar cats []string\n\t\t\tif categoriesCol != nil {\n\t\t\t\tcatsValue, err := categoriesCol.ValueByIdx(i)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, errors.Trace(err)\n\t\t\t\t}\n\t\t\t\tcats = milvusBytesToStrings(catsValue)\n\t\t\t}\n\n\t\t\tvectors = append(vectors, Vector{\n\t\t\t\tId:         id,\n\t\t\t\tCategories: cats,\n\t\t\t})\n\t\t}\n\t}\n\treturn vectors, nil\n}\n\nfunc milvusStringsToBytes(ss [][]string) [][][]byte {\n\tres := make([][][]byte, len(ss))\n\tfor i, s1 := range ss {\n\t\tres[i] = make([][]byte, len(s1))\n\t\tfor j, s2 := range s1 {\n\t\t\tres[i][j] = []byte(s2)\n\t\t}\n\t}\n\treturn res\n}\n\nfunc milvusBytesToStrings(bs [][]byte) []string {\n\tres := make([]string, len(bs))\n\tfor i, b := range bs {\n\t\tres[i] = string(b)\n\t}\n\treturn res\n}\n"
  },
  {
    "path": "storage/vectors/milvus_test.go",
    "content": "// Copyright 2026 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage vectors\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/suite\"\n)\n\nvar (\n\tmilvusUri string\n)\n\nfunc init() {\n\t// get environment variables\n\tenv := func(key, defaultValue string) string {\n\t\tif value := os.Getenv(key); value != \"\" {\n\t\t\treturn value\n\t\t}\n\t\treturn defaultValue\n\t}\n\tmilvusUri = env(\"MILVUS_URI\", \"milvus://127.0.0.1:19530\")\n}\n\ntype MilvusTestSuite struct {\n\tvectorsTestSuite\n}\n\nfunc (suite *MilvusTestSuite) SetupSuite() {\n\tvar err error\n\tsuite.Database, err = Open(milvusUri, \"gorse_\")\n\tsuite.NoError(err)\n}\n\nfunc TestMilvus(t *testing.T) {\n\tsuite.Run(t, new(MilvusTestSuite))\n}\n"
  },
  {
    "path": "storage/vectors/proxy.go",
    "content": "// Copyright 2026 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage vectors\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/protocol\"\n\t\"github.com/juju/errors\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n)\n\ntype ProxyServer struct {\n\tprotocol.UnimplementedVectorStoreServer\n\tdatabase Database\n\tserver   *grpc.Server\n}\n\nfunc NewProxyServer(database Database) *ProxyServer {\n\treturn &ProxyServer{database: database}\n}\n\nfunc (p *ProxyServer) Serve(lis net.Listener) error {\n\tp.server = grpc.NewServer()\n\tprotocol.RegisterVectorStoreServer(p.server, p)\n\treturn p.server.Serve(lis)\n}\n\nfunc (p *ProxyServer) Stop() {\n\tp.server.Stop()\n}\n\nfunc (p *ProxyServer) ListCollections(ctx context.Context, _ *protocol.ListCollectionsRequest) (*protocol.ListCollectionsResponse, error) {\n\tcollections, err := p.database.ListCollections(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &protocol.ListCollectionsResponse{Collections: collections}, nil\n}\n\nfunc (p *ProxyServer) AddCollection(ctx context.Context, request *protocol.AddCollectionRequest) (*protocol.AddCollectionResponse, error) {\n\tdistance, err := protoDistanceToDistance(request.GetDistance())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\terr = p.database.AddCollection(ctx, request.GetName(), int(request.GetDimensions()), distance)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &protocol.AddCollectionResponse{}, nil\n}\n\nfunc (p *ProxyServer) DeleteCollection(ctx context.Context, request *protocol.DeleteCollectionRequest) (*protocol.DeleteCollectionResponse, error) {\n\terr := p.database.DeleteCollection(ctx, request.GetName())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &protocol.DeleteCollectionResponse{}, nil\n}\n\nfunc (p *ProxyServer) AddVectors(ctx context.Context, request *protocol.AddVectorsRequest) (*protocol.AddVectorsResponse, error) {\n\tvectors := make([]Vector, len(request.Vectors))\n\tfor i, vector := range request.Vectors {\n\t\ttimestamp := time.Time{}\n\t\tif vector.GetTimestamp() != nil {\n\t\t\ttimestamp = vector.GetTimestamp().AsTime()\n\t\t}\n\t\tvectors[i] = Vector{\n\t\t\tId:         vector.GetId(),\n\t\t\tVector:     vector.GetValues(),\n\t\t\tCategories: vector.GetCategories(),\n\t\t\tTimestamp:  timestamp,\n\t\t}\n\t}\n\terr := p.database.AddVectors(ctx, request.GetCollection(), vectors)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &protocol.AddVectorsResponse{}, nil\n}\n\nfunc (p *ProxyServer) DeleteVectors(ctx context.Context, request *protocol.DeleteVectorsRequest) (*protocol.DeleteVectorsResponse, error) {\n\ttimestamp := time.Time{}\n\tif request.GetTimestamp() != nil {\n\t\ttimestamp = request.GetTimestamp().AsTime()\n\t}\n\terr := p.database.DeleteVectors(ctx, request.GetCollection(), timestamp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &protocol.DeleteVectorsResponse{}, nil\n}\n\nfunc (p *ProxyServer) QueryVectors(ctx context.Context, request *protocol.QueryVectorsRequest) (*protocol.QueryVectorsResponse, error) {\n\tresults, err := p.database.QueryVectors(ctx, request.GetCollection(), request.GetQuery(), request.GetCategories(), int(request.GetTopK()))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpbVectors := make([]*protocol.Vector, len(results))\n\tfor i, result := range results {\n\t\tpbVectors[i] = &protocol.Vector{\n\t\t\tId:         result.Id,\n\t\t\tValues:     result.Vector,\n\t\t\tCategories: result.Categories,\n\t\t}\n\t}\n\treturn &protocol.QueryVectorsResponse{Vectors: pbVectors}, nil\n}\n\ntype ProxyClient struct {\n\tprotocol.VectorStoreClient\n}\n\nfunc NewProxyClient(conn *grpc.ClientConn) *ProxyClient {\n\treturn &ProxyClient{\n\t\tVectorStoreClient: protocol.NewVectorStoreClient(conn),\n\t}\n}\n\nfunc (p ProxyClient) Init() error {\n\treturn nil\n}\n\nfunc (p ProxyClient) Optimize() error {\n\treturn nil\n}\n\nfunc (p ProxyClient) Close() error {\n\treturn nil\n}\n\nfunc (p ProxyClient) ListCollections(ctx context.Context) ([]string, error) {\n\tresp, err := p.VectorStoreClient.ListCollections(ctx, &protocol.ListCollectionsRequest{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Collections, nil\n}\n\nfunc (p ProxyClient) AddCollection(ctx context.Context, name string, dimensions int, distance Distance) error {\n\tpbDistance, err := distanceToProtoDistance(distance)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = p.VectorStoreClient.AddCollection(ctx, &protocol.AddCollectionRequest{\n\t\tName:       name,\n\t\tDimensions: int32(dimensions),\n\t\tDistance:   pbDistance,\n\t})\n\treturn err\n}\n\nfunc (p ProxyClient) DeleteCollection(ctx context.Context, name string) error {\n\t_, err := p.VectorStoreClient.DeleteCollection(ctx, &protocol.DeleteCollectionRequest{Name: name})\n\treturn err\n}\n\nfunc (p ProxyClient) AddVectors(ctx context.Context, collection string, vectors []Vector) error {\n\tpbVectors := make([]*protocol.Vector, len(vectors))\n\tfor i, vector := range vectors {\n\t\tpbVectors[i] = &protocol.Vector{\n\t\t\tId:         vector.Id,\n\t\t\tValues:     vector.Vector,\n\t\t\tCategories: vector.Categories,\n\t\t\tTimestamp:  timestamppb.New(vector.Timestamp),\n\t\t}\n\t}\n\t_, err := p.VectorStoreClient.AddVectors(ctx, &protocol.AddVectorsRequest{\n\t\tCollection: collection,\n\t\tVectors:    pbVectors,\n\t})\n\treturn err\n}\n\nfunc (p ProxyClient) DeleteVectors(ctx context.Context, collection string, timestamp time.Time) error {\n\t_, err := p.VectorStoreClient.DeleteVectors(ctx, &protocol.DeleteVectorsRequest{\n\t\tCollection: collection,\n\t\tTimestamp:  timestamppb.New(timestamp),\n\t})\n\treturn err\n}\n\nfunc (p ProxyClient) QueryVectors(ctx context.Context, collection string, q []float32, categories []string, topK int) ([]Vector, error) {\n\tresp, err := p.VectorStoreClient.QueryVectors(ctx, &protocol.QueryVectorsRequest{\n\t\tCollection: collection,\n\t\tQuery:      q,\n\t\tCategories: categories,\n\t\tTopK:       int32(topK),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresults := make([]Vector, len(resp.Vectors))\n\tfor i, vector := range resp.Vectors {\n\t\tresults[i] = Vector{\n\t\t\tId:         vector.GetId(),\n\t\t\tVector:     vector.GetValues(),\n\t\t\tCategories: vector.GetCategories(),\n\t\t}\n\t}\n\treturn results, nil\n}\n\nfunc distanceToProtoDistance(distance Distance) (protocol.Distance, error) {\n\tswitch distance {\n\tcase Cosine:\n\t\treturn protocol.Distance_Cosine, nil\n\tcase Euclidean:\n\t\treturn protocol.Distance_Euclidean, nil\n\tcase Dot:\n\t\treturn protocol.Distance_Dot, nil\n\tdefault:\n\t\treturn protocol.Distance_Unknown, errors.NotSupportedf(\"distance method\")\n\t}\n}\n\nfunc protoDistanceToDistance(distance protocol.Distance) (Distance, error) {\n\tswitch distance {\n\tcase protocol.Distance_Cosine:\n\t\treturn Cosine, nil\n\tcase protocol.Distance_Euclidean:\n\t\treturn Euclidean, nil\n\tcase protocol.Distance_Dot:\n\t\treturn Dot, nil\n\tdefault:\n\t\treturn Cosine, errors.NotSupportedf(\"distance method\")\n\t}\n}\n"
  },
  {
    "path": "storage/vectors/proxy_test.go",
    "content": "// Copyright 2026 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage vectors\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/suite\"\n\t\"google.golang.org/grpc\"\n)\n\ntype ProxyTestSuite struct {\n\tvectorsTestSuite\n\tsqlite     Database\n\tserver     *ProxyServer\n\tclientConn *grpc.ClientConn\n}\n\nfunc (suite *ProxyTestSuite) SetupSuite() {\n\tvar err error\n\tpath := fmt.Sprintf(\"sqlite://%s/sqlite.db\", suite.T().TempDir())\n\tsuite.sqlite, err = Open(path, \"gorse_\")\n\tsuite.NoError(err)\n\tlis, err := net.Listen(\"tcp\", \"localhost:0\")\n\tsuite.NoError(err)\n\tsuite.server = NewProxyServer(suite.sqlite)\n\tgo func() {\n\t\terr = suite.server.Serve(lis)\n\t\tsuite.NoError(err)\n\t}()\n\tsuite.clientConn, err = grpc.Dial(lis.Addr().String(), grpc.WithInsecure())\n\tsuite.NoError(err)\n\tsuite.Database = NewProxyClient(suite.clientConn)\n}\n\nfunc (suite *ProxyTestSuite) TearDownSuite() {\n\tsuite.server.Stop()\n\tsuite.NoError(suite.clientConn.Close())\n\tsuite.NoError(suite.sqlite.Close())\n}\n\nfunc TestProxy(t *testing.T) {\n\tsuite.Run(t, new(ProxyTestSuite))\n}\n"
  },
  {
    "path": "storage/vectors/qdrant.go",
    "content": "// Copyright 2026 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage vectors\n\nimport (\n\t\"context\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/gorse-io/gorse/storage\"\n\t\"github.com/juju/errors\"\n\t\"github.com/qdrant/go-client/qdrant\"\n)\n\nconst (\n\tqdrantPayloadCategoriesKey = \"categories\"\n\tqdrantPayloadIdKey         = \"id\"\n\tqdrantPayloadTimestampKey  = \"timestamp\"\n)\n\nfunc init() {\n\tRegister([]string{storage.QdrantPrefix}, func(path, tablePrefix string, opts ...storage.Option) (Database, error) {\n\t\tdatabase := new(Qdrant)\n\t\tu, err := url.Parse(path)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\thost := u.Hostname()\n\t\tport := u.Port()\n\t\tportInt, err := strconv.Atoi(port)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tdatabase.client, err = qdrant.NewClient(&qdrant.Config{\n\t\t\tHost: host,\n\t\t\tPort: portInt,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\treturn database, nil\n\t})\n}\n\ntype Qdrant struct {\n\tclient *qdrant.Client\n}\n\nfunc (db *Qdrant) Init() error {\n\treturn nil\n}\n\nfunc (db *Qdrant) Optimize() error {\n\treturn nil\n}\n\nfunc (db *Qdrant) Close() error {\n\treturn db.client.Close()\n}\n\nfunc (db *Qdrant) ListCollections(ctx context.Context) ([]string, error) {\n\treturn db.client.ListCollections(ctx)\n}\n\nfunc (db *Qdrant) AddCollection(ctx context.Context, name string, dimensions int, distance Distance) error {\n\tvar qdrantDistance qdrant.Distance\n\tswitch distance {\n\tcase Cosine:\n\t\tqdrantDistance = qdrant.Distance_Cosine\n\tcase Euclidean:\n\t\tqdrantDistance = qdrant.Distance_Euclid\n\tcase Dot:\n\t\tqdrantDistance = qdrant.Distance_Dot\n\tdefault:\n\t\treturn errors.NotSupportedf(\"distance method\")\n\t}\n\terr := db.client.CreateCollection(ctx, &qdrant.CreateCollection{\n\t\tCollectionName: name,\n\t\tVectorsConfig: qdrant.NewVectorsConfig(&qdrant.VectorParams{\n\t\t\tSize:     uint64(dimensions),\n\t\t\tDistance: qdrantDistance,\n\t\t}),\n\t})\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t_, err = db.client.CreateFieldIndex(ctx, &qdrant.CreateFieldIndexCollection{\n\t\tCollectionName: name,\n\t\tWait:           new(true),\n\t\tFieldName:      qdrantPayloadTimestampKey,\n\t\tFieldType:      qdrant.FieldType_FieldTypeInteger.Enum(),\n\t})\n\treturn errors.Trace(err)\n}\n\nfunc (db *Qdrant) DeleteCollection(ctx context.Context, name string) error {\n\treturn db.client.DeleteCollection(ctx, name)\n}\n\nfunc (db *Qdrant) AddVectors(ctx context.Context, collection string, vectors []Vector) error {\n\tif len(vectors) == 0 {\n\t\treturn nil\n\t}\n\tpoints := make([]*qdrant.PointStruct, 0, len(vectors))\n\tfor _, vector := range vectors {\n\t\tpoints = append(points, &qdrant.PointStruct{\n\t\t\tId: qdrant.NewID(uuid.NewMD5(uuid.NameSpaceURL, []byte(vector.Id)).String()),\n\t\t\tPayload: map[string]*qdrant.Value{\n\t\t\t\tqdrantPayloadCategoriesKey: qdrantListValue(vector.Categories),\n\t\t\t\tqdrantPayloadIdKey:         qdrant.NewValueString(vector.Id),\n\t\t\t\tqdrantPayloadTimestampKey:  qdrant.NewValueInt(vector.Timestamp.UnixMilli()),\n\t\t\t},\n\t\t\tVectors: qdrant.NewVectorsDense(vector.Vector),\n\t\t})\n\t}\n\t_, err := db.client.Upsert(ctx, &qdrant.UpsertPoints{\n\t\tCollectionName: collection,\n\t\tPoints:         points,\n\t})\n\treturn errors.Trace(err)\n}\n\nfunc (db *Qdrant) DeleteVectors(ctx context.Context, collection string, timestamp time.Time) error {\n\tlt := float64(timestamp.UnixMilli())\n\t_, err := db.client.Delete(ctx, &qdrant.DeletePoints{\n\t\tCollectionName: collection,\n\t\tPoints: qdrant.NewPointsSelectorFilter(&qdrant.Filter{\n\t\t\tMust: []*qdrant.Condition{\n\t\t\t\tqdrant.NewRange(qdrantPayloadTimestampKey, &qdrant.Range{Lt: &lt}),\n\t\t\t},\n\t\t}),\n\t})\n\treturn errors.Trace(err)\n}\n\nfunc (db *Qdrant) QueryVectors(ctx context.Context, collection string, q []float32, categories []string, topK int) ([]Vector, error) {\n\tif topK <= 0 {\n\t\treturn []Vector{}, nil\n\t}\n\trequest := &qdrant.QueryPoints{\n\t\tCollectionName: collection,\n\t\tQuery:          qdrant.NewQueryDense(q),\n\t\tLimit:          new(uint64(topK)),\n\t\tWithPayload:    qdrant.NewWithPayloadEnable(true),\n\t\tWithVectors:    qdrant.NewWithVectorsEnable(true),\n\t}\n\tif len(categories) > 0 {\n\t\trequest.Filter = &qdrant.Filter{\n\t\t\tMust: []*qdrant.Condition{\n\t\t\t\tqdrant.NewMatchKeywords(qdrantPayloadCategoriesKey, categories...),\n\t\t\t},\n\t\t}\n\t}\n\tresponse, err := db.client.Query(ctx, request)\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\tresults := make([]Vector, 0, len(response))\n\tfor _, scored := range response {\n\t\tresults = append(results, Vector{\n\t\t\tId:         qdrantId(scored.GetPayload()),\n\t\t\tVector:     qdrantVectorOutput(scored.GetVectors()),\n\t\t\tCategories: qdrantCategories(scored.GetPayload()),\n\t\t})\n\t}\n\treturn results, nil\n}\n\nfunc qdrantId(payload map[string]*qdrant.Value) string {\n\tif payload == nil {\n\t\treturn \"\"\n\t}\n\tif value, ok := payload[qdrantPayloadIdKey]; ok {\n\t\treturn value.GetStringValue()\n\t}\n\treturn \"\"\n}\n\nfunc qdrantListValue(items []string) *qdrant.Value {\n\tvalues := make([]*qdrant.Value, 0, len(items))\n\tfor _, item := range items {\n\t\tvalues = append(values, qdrant.NewValueString(item))\n\t}\n\treturn qdrant.NewValueFromList(values...)\n}\n\nfunc qdrantCategories(payload map[string]*qdrant.Value) []string {\n\tif payload == nil {\n\t\treturn []string{}\n\t}\n\tvalue, ok := payload[qdrantPayloadCategoriesKey]\n\tif !ok || value == nil {\n\t\treturn []string{}\n\t}\n\tlist := value.GetListValue()\n\tif list == nil {\n\t\treturn []string{}\n\t}\n\tcategories := make([]string, 0, len(list.GetValues()))\n\tfor _, item := range list.GetValues() {\n\t\tif item == nil {\n\t\t\tcontinue\n\t\t}\n\t\tcategories = append(categories, item.GetStringValue())\n\t}\n\treturn categories\n}\n\nfunc qdrantVectorOutput(output *qdrant.VectorsOutput) []float32 {\n\tif output == nil {\n\t\treturn nil\n\t}\n\n\tvector := output.GetVector()\n\tif vector == nil {\n\t\treturn nil\n\t}\n\treturn vector.GetDenseVector().GetData()\n}\n"
  },
  {
    "path": "storage/vectors/qdrant_test.go",
    "content": "// Copyright 2026 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage vectors\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/suite\"\n)\n\nvar (\n\tqdrantUri string\n)\n\nfunc init() {\n\t// get environment variables\n\tenv := func(key, defaultValue string) string {\n\t\tif value := os.Getenv(key); value != \"\" {\n\t\t\treturn value\n\t\t}\n\t\treturn defaultValue\n\t}\n\tqdrantUri = env(\"QDRANT_URI\", \"qdrant://127.0.0.1:6334\")\n}\n\ntype QdrantTestSuite struct {\n\tvectorsTestSuite\n}\n\nfunc (suite *QdrantTestSuite) SetupSuite() {\n\tvar err error\n\tsuite.Database, err = Open(qdrantUri, \"gorse_\")\n\tsuite.NoError(err)\n}\n\nfunc TestQdrant(t *testing.T) {\n\tsuite.Run(t, new(QdrantTestSuite))\n}\n"
  },
  {
    "path": "storage/vectors/sqlite.go",
    "content": "// Copyright 2026 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage vectors\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/storage\"\n\t\"github.com/juju/errors\"\n\t_ \"modernc.org/sqlite/vec\"\n)\n\nfunc init() {\n\tRegister([]string{storage.SQLitePrefix}, func(path, tablePrefix string, opts ...storage.Option) (Database, error) {\n\t\tdatabase := new(SQLite)\n\t\t// Strip sqlite:// prefix\n\t\tdbPath := strings.TrimPrefix(path, storage.SQLitePrefix)\n\t\tvar err error\n\t\tdatabase.db, err = sql.Open(\"sqlite\", dbPath)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\treturn database, nil\n\t})\n}\n\ntype SQLite struct {\n\tdb *sql.DB\n}\n\nfunc (db *SQLite) Init() error {\n\treturn nil\n}\n\nfunc (db *SQLite) Optimize() error {\n\treturn nil\n}\n\nfunc (db *SQLite) Close() error {\n\treturn db.db.Close()\n}\n\nfunc (db *SQLite) ListCollections(ctx context.Context) ([]string, error) {\n\trows, err := db.db.QueryContext(ctx, \"SELECT name FROM sqlite_master WHERE type='table' AND sql LIKE '%USING vec0%'\")\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\tdefer rows.Close()\n\tvar names []string\n\tfor rows.Next() {\n\t\tvar name string\n\t\tif err := rows.Scan(&name); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tnames = append(names, name)\n\t}\n\treturn names, nil\n}\n\nfunc (db *SQLite) AddCollection(ctx context.Context, name string, dimensions int, distance Distance) error {\n\tvar metric string\n\tswitch distance {\n\tcase Cosine:\n\t\tmetric = \"cosine\"\n\tcase Euclidean:\n\t\tmetric = \"l2\"\n\tcase Dot:\n\t\tmetric = \"ip\"\n\tdefault:\n\t\treturn errors.NotSupportedf(\"distance method\")\n\t}\n\t_, err := db.db.ExecContext(ctx, fmt.Sprintf(\"CREATE VIRTUAL TABLE %s USING vec0(id TEXT, categories TEXT, timestamp INTEGER, vector FLOAT[%d] distance_metric=%s)\", name, dimensions, metric))\n\treturn errors.Trace(err)\n}\n\nfunc (db *SQLite) DeleteCollection(ctx context.Context, name string) error {\n\tvar count int\n\terr := db.db.QueryRowContext(ctx, \"SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?\", name).Scan(&count)\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif count == 0 {\n\t\treturn errors.NotFoundf(\"collection %s\", name)\n\t}\n\t_, err = db.db.ExecContext(ctx, fmt.Sprintf(\"DROP TABLE %s\", name))\n\treturn errors.Trace(err)\n}\n\nfunc (db *SQLite) AddVectors(ctx context.Context, collection string, vectors []Vector) error {\n\tif len(vectors) == 0 {\n\t\treturn nil\n\t}\n\ttx, err := db.db.BeginTx(ctx, nil)\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tstmt, err := tx.PrepareContext(ctx, fmt.Sprintf(\"INSERT INTO %s(id, categories, timestamp, vector) VALUES(?, ?, ?, ?)\", collection))\n\tif err != nil {\n\t\t_ = tx.Rollback()\n\t\treturn errors.Trace(err)\n\t}\n\tdefer stmt.Close()\n\n\tfor _, v := range vectors {\n\t\tcategories, err := json.Marshal(v.Categories)\n\t\tif err != nil {\n\t\t\t_ = tx.Rollback()\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\tvectorJson, err := json.Marshal(v.Vector)\n\t\tif err != nil {\n\t\t\t_ = tx.Rollback()\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t\ttimestamp := v.Timestamp.UnixMilli()\n\t\t_, err = stmt.ExecContext(ctx, v.Id, string(categories), timestamp, string(vectorJson))\n\t\tif err != nil {\n\t\t\t_ = tx.Rollback()\n\t\t\treturn errors.Trace(err)\n\t\t}\n\t}\n\treturn errors.Trace(tx.Commit())\n}\n\nfunc (db *SQLite) DeleteVectors(ctx context.Context, collection string, timestamp time.Time) error {\n\t_, err := db.db.ExecContext(ctx, fmt.Sprintf(\"DELETE FROM %s WHERE timestamp < ?\", collection), timestamp.UnixMilli())\n\treturn errors.Trace(err)\n}\n\nfunc (db *SQLite) QueryVectors(ctx context.Context, collection string, q []float32, categories []string, topK int) ([]Vector, error) {\n\tif topK <= 0 {\n\t\treturn []Vector{}, nil\n\t}\n\n\tqJson, err := json.Marshal(q)\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\n\tquery := fmt.Sprintf(\"SELECT id, categories FROM %s WHERE vector MATCH ? AND k = ? \", collection)\n\tvar args []any\n\targs = append(args, string(qJson), topK)\n\n\tif len(categories) > 0 {\n\t\tvar categoryConditions []string\n\t\tfor _, category := range categories {\n\t\t\tcategoryConditions = append(categoryConditions, \"json_contains(categories, ?)\")\n\t\t\targs = append(args, fmt.Sprintf(\"[%q]\", category))\n\t\t}\n\t\tquery += \" AND (\" + strings.Join(categoryConditions, \" OR \") + \")\"\n\t}\n\tquery += \" ORDER BY distance\"\n\n\trows, err := db.db.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\tdefer rows.Close()\n\n\tvar results []Vector\n\tfor rows.Next() {\n\t\tvar v Vector\n\t\tvar categoriesStr string\n\t\tif err := rows.Scan(&v.Id, &categoriesStr); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tif err := json.Unmarshal([]byte(categoriesStr), &v.Categories); err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tresults = append(results, v)\n\t}\n\treturn results, nil\n}\n"
  },
  {
    "path": "storage/vectors/sqlite_test.go",
    "content": "// Copyright 2026 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage vectors\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/suite\"\n)\n\ntype SQLiteTestSuite struct {\n\tvectorsTestSuite\n}\n\nfunc (suite *SQLiteTestSuite) SetupSuite() {\n\tvar err error\n\tsuite.Database, err = Open(\"sqlite://:memory:\", \"gorse_\")\n\tsuite.NoError(err)\n}\n\nfunc TestSQLite(t *testing.T) {\n\tsuite.Run(t, new(SQLiteTestSuite))\n}\n"
  },
  {
    "path": "storage/vectors/weaviate.go",
    "content": "// Copyright 2026 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage vectors\n\nimport (\n\t\"context\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/google/uuid\"\n\t\"github.com/gorse-io/gorse/storage\"\n\t\"github.com/juju/errors\"\n\t\"github.com/weaviate/weaviate-go-client/v4/weaviate\"\n\t\"github.com/weaviate/weaviate-go-client/v4/weaviate/filters\"\n\t\"github.com/weaviate/weaviate-go-client/v4/weaviate/graphql\"\n\t\"github.com/weaviate/weaviate/entities/models\"\n)\n\nconst (\n\tweaviatePayloadCategoriesKey = \"categories\"\n\tweaviatePayloadTimestampKey  = \"timestamp\"\n)\n\nfunc init() {\n\tRegister([]string{storage.WeaviatePrefix, storage.WeaviatesPrefix}, func(path, tablePrefix string, opts ...storage.Option) (Database, error) {\n\t\tdatabase := new(Weaviate)\n\t\tu, err := url.Parse(path)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\tscheme := \"http\"\n\t\tif strings.HasPrefix(path, storage.WeaviatesPrefix) {\n\t\t\tscheme = \"https\"\n\t\t}\n\t\tcfg := weaviate.Config{\n\t\t\tHost:   u.Host,\n\t\t\tScheme: scheme,\n\t\t}\n\t\tdatabase.client, err = weaviate.NewClient(cfg)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Trace(err)\n\t\t}\n\t\treturn database, nil\n\t})\n}\n\ntype Weaviate struct {\n\tclient *weaviate.Client\n}\n\nfunc (db *Weaviate) Init() error {\n\treturn nil\n}\n\nfunc (db *Weaviate) Optimize() error {\n\treturn nil\n}\n\nfunc (db *Weaviate) Close() error {\n\treturn nil\n}\n\nfunc (db *Weaviate) ListCollections(ctx context.Context) ([]string, error) {\n\ts, err := db.client.Schema().Getter().Do(ctx)\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\tvar names []string\n\tfor _, class := range s.Classes {\n\t\tnames = append(names, uncapitalize(class.Class))\n\t}\n\treturn names, nil\n}\n\nfunc (db *Weaviate) AddCollection(ctx context.Context, name string, dimensions int, distance Distance) error {\n\tvar weaviateDistance string\n\tswitch distance {\n\tcase Cosine:\n\t\tweaviateDistance = \"cosine\"\n\tcase Euclidean:\n\t\tweaviateDistance = \"l2-squared\"\n\tcase Dot:\n\t\tweaviateDistance = \"dot\"\n\tdefault:\n\t\treturn errors.NotSupportedf(\"distance method\")\n\t}\n\tclass := &models.Class{\n\t\tClass:      capitalize(name),\n\t\tVectorizer: \"none\",\n\t\tProperties: []*models.Property{\n\t\t\t{\n\t\t\t\tName:     \"originalId\",\n\t\t\t\tDataType: []string{\"string\"},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:     weaviatePayloadCategoriesKey,\n\t\t\t\tDataType: []string{\"string[]\"},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:              weaviatePayloadTimestampKey,\n\t\t\t\tDataType:          []string{\"date\"},\n\t\t\t\tIndexFilterable:   new(true),\n\t\t\t\tIndexRangeFilters: new(true),\n\t\t\t},\n\t\t},\n\t\tVectorIndexConfig: map[string]interface{}{\n\t\t\t\"distance\": weaviateDistance,\n\t\t},\n\t}\n\terr := db.client.Schema().ClassCreator().WithClass(class).Do(ctx)\n\treturn errors.Trace(err)\n}\n\nfunc (db *Weaviate) DeleteCollection(ctx context.Context, name string) error {\n\texists, err := db.client.Schema().ClassExistenceChecker().WithClassName(capitalize(name)).Do(ctx)\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\tif !exists {\n\t\treturn errors.NotFoundf(\"collection %s\", name)\n\t}\n\terr = db.client.Schema().ClassDeleter().WithClassName(capitalize(name)).Do(ctx)\n\treturn errors.Trace(err)\n}\n\nfunc (db *Weaviate) AddVectors(ctx context.Context, collection string, vectors []Vector) error {\n\tif len(vectors) == 0 {\n\t\treturn nil\n\t}\n\tobjects := make([]*models.Object, 0, len(vectors))\n\tfor _, vector := range vectors {\n\t\tobjects = append(objects, &models.Object{\n\t\t\tClass: capitalize(collection),\n\t\t\tID:    strfmt.UUID(uuid.NewMD5(uuid.NameSpaceURL, []byte(vector.Id)).String()),\n\t\t\tProperties: map[string]interface{}{\n\t\t\t\t\"originalId\":                 vector.Id,\n\t\t\t\tweaviatePayloadCategoriesKey: vector.Categories,\n\t\t\t\tweaviatePayloadTimestampKey:  vector.Timestamp,\n\t\t\t},\n\t\t\tVector: models.C11yVector(vector.Vector),\n\t\t})\n\t}\n\t_, err := db.client.Batch().ObjectsBatcher().WithObjects(objects...).Do(ctx)\n\treturn errors.Trace(err)\n}\n\nfunc (db *Weaviate) DeleteVectors(ctx context.Context, collection string, timestamp time.Time) error {\n\t_, err := db.client.Batch().ObjectsBatchDeleter().\n\t\tWithClassName(capitalize(collection)).\n\t\tWithWhere(filters.Where().\n\t\t\tWithPath([]string{weaviatePayloadTimestampKey}).\n\t\t\tWithOperator(filters.LessThan).\n\t\t\tWithValueDate(timestamp)).\n\t\tDo(ctx)\n\treturn errors.Trace(err)\n}\n\nfunc (db *Weaviate) QueryVectors(ctx context.Context, collection string, q []float32, categories []string, topK int) ([]Vector, error) {\n\tif topK <= 0 {\n\t\treturn []Vector{}, nil\n\t}\n\n\tfields := []graphql.Field{\n\t\t{Name: \"originalId\"},\n\t\t{Name: weaviatePayloadCategoriesKey},\n\t}\n\n\texplore := db.client.GraphQL().NearVectorArgBuilder().WithVector(q)\n\tbuilder := db.client.GraphQL().Get().\n\t\tWithClassName(capitalize(collection)).\n\t\tWithFields(fields...).\n\t\tWithNearVector(explore).\n\t\tWithLimit(topK)\n\n\tif len(categories) > 0 {\n\t\toperands := make([]*filters.WhereBuilder, 0, len(categories))\n\t\tfor _, category := range categories {\n\t\t\toperands = append(operands, filters.Where().\n\t\t\t\tWithPath([]string{weaviatePayloadCategoriesKey}).\n\t\t\t\tWithOperator(filters.ContainsAny).\n\t\t\t\tWithValueString(category))\n\t\t}\n\t\tvar where *filters.WhereBuilder\n\t\tif len(operands) == 1 {\n\t\t\twhere = operands[0]\n\t\t} else {\n\t\t\twhere = filters.Where().\n\t\t\t\tWithOperator(filters.Or).\n\t\t\t\tWithOperands(operands)\n\t\t}\n\t\tbuilder = builder.WithWhere(where)\n\t}\n\n\tresult, err := builder.Do(ctx)\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\n\tif len(result.Errors) > 0 {\n\t\treturn nil, errors.New(result.Errors[0].Message)\n\t}\n\n\tdata := result.Data[\"Get\"].(map[string]interface{})\n\titems := data[capitalize(collection)].([]interface{})\n\tresults := make([]Vector, 0, len(items))\n\tfor _, item := range items {\n\t\tm := item.(map[string]interface{})\n\t\tid := m[\"originalId\"].(string)\n\t\tvar cats []string\n\t\tif m[weaviatePayloadCategoriesKey] != nil {\n\t\t\tfor _, c := range m[weaviatePayloadCategoriesKey].([]interface{}) {\n\t\t\t\tcats = append(cats, c.(string))\n\t\t\t}\n\t\t}\n\t\tresults = append(results, Vector{\n\t\t\tId:         id,\n\t\t\tCategories: cats,\n\t\t})\n\t}\n\treturn results, nil\n}\n\nfunc capitalize(s string) string {\n\tif len(s) == 0 {\n\t\treturn s\n\t}\n\treturn strings.ToUpper(s[:1]) + s[1:]\n}\n\nfunc uncapitalize(s string) string {\n\tif len(s) == 0 {\n\t\treturn s\n\t}\n\treturn strings.ToLower(s[:1]) + s[1:]\n}\n"
  },
  {
    "path": "storage/vectors/weaviate_test.go",
    "content": "// Copyright 2026 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage vectors\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/suite\"\n)\n\nvar (\n\tweaviateUri string\n)\n\nfunc init() {\n\t// get environment variables\n\tenv := func(key, defaultValue string) string {\n\t\tif value := os.Getenv(key); value != \"\" {\n\t\t\treturn value\n\t\t}\n\t\treturn defaultValue\n\t}\n\tweaviateUri = env(\"WEAVIATE_URI\", \"weaviate://127.0.0.1:8080\")\n}\n\ntype WeaviateTestSuite struct {\n\tvectorsTestSuite\n}\n\nfunc (suite *WeaviateTestSuite) SetupSuite() {\n\tvar err error\n\tsuite.Database, err = Open(weaviateUri, \"gorse_\")\n\tsuite.NoError(err)\n}\n\nfunc TestWeaviate(t *testing.T) {\n\tsuite.Run(t, new(WeaviateTestSuite))\n}\n"
  },
  {
    "path": "worker/metrics.go",
    "content": "// Copyright 2021 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage worker\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n)\n\nconst (\n\tLabelStep = \"step\"\n\tLabelData = \"data\"\n)\n\nvar (\n\tUpdateUserRecommendTotal = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"worker\",\n\t\tName:      \"update_user_recommend_total\",\n\t})\n\tOfflineRecommendStepSecondsVec = promauto.NewGaugeVec(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"worker\",\n\t\tName:      \"offline_recommend_step_seconds\",\n\t}, []string{LabelStep})\n\tOfflineRecommendTotalSeconds = promauto.NewGauge(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"worker\",\n\t\tName:      \"offline_recommend_total_seconds\",\n\t})\n\tMemoryInuseBytesVec = promauto.NewGaugeVec(prometheus.GaugeOpts{\n\t\tNamespace: \"gorse\",\n\t\tSubsystem: \"worker\",\n\t\tName:      \"memory_inuse_bytes\",\n\t}, []string{LabelData})\n)\n"
  },
  {
    "path": "worker/pipeline.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage worker\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\tmapset \"github.com/deckarep/golang-set/v2\"\n\t\"github.com/gorse-io/gorse/common/expression\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/common/monitor\"\n\t\"github.com/gorse-io/gorse/common/parallel\"\n\t\"github.com/gorse-io/gorse/common/util\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/logics\"\n\t\"github.com/gorse-io/gorse/model/ctr\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/juju/errors\"\n\t\"github.com/samber/lo\"\n\t\"go.uber.org/atomic\"\n\t\"go.uber.org/zap\"\n)\n\ntype Pipeline struct {\n\tConfig                   *config.Config\n\tCacheClient              cache.Database\n\tDataClient               data.Database\n\tTracer                   *monitor.Monitor\n\tJobs                     int\n\tMatrixFactorizationItems *logics.MatrixFactorizationItems\n\tMatrixFactorizationUsers *logics.MatrixFactorizationUsers\n\tClickThroughRateModel    ctr.FactorizationMachines\n\tdontskipColdStartUsers   bool\n}\n\nfunc (p *Pipeline) Recommend(ctx context.Context, users []data.User, progress func(completed, throughput int)) {\n\tstartRecommendTime := time.Now()\n\titemCache := NewItemCache(p.DataClient)\n\tlog.Logger().Info(\"ranking recommendation\",\n\t\tzap.Int(\"n_working_users\", len(users)),\n\t\tzap.Int(\"n_jobs\", p.Jobs),\n\t\tzap.Int(\"cache_size\", p.Config.Recommend.CacheSize))\n\n\t// progress tracker\n\tcompleted := make(chan struct{}, 1000)\n\t_, span := p.Tracer.Start(ctx, \"Generate recommendation\", len(users))\n\tdefer span.End()\n\n\tgo func() {\n\t\tdefer util.CheckPanic()\n\t\tcompletedCount, previousCount := 0, 0\n\t\tticker := time.NewTicker(10 * time.Second)\n\t\tdefer ticker.Stop()\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase _, ok := <-completed:\n\t\t\t\tif !ok {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tcompletedCount++\n\t\t\tcase <-ticker.C:\n\t\t\t\tthroughput := completedCount - previousCount\n\t\t\t\tspan.Add(throughput)\n\t\t\t\tif progress != nil {\n\t\t\t\t\tprogress(completedCount, completedCount-previousCount)\n\t\t\t\t}\n\t\t\t\tpreviousCount = completedCount\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\t// recommendation\n\tstartTime := time.Now()\n\tvar (\n\t\tupdateUserCount               atomic.Float64\n\t\tcollaborativeRecommendSeconds atomic.Float64\n\t\tuserBasedRecommendSeconds     atomic.Float64\n\t\titemBasedRecommendSeconds     atomic.Float64\n\t\tlatestRecommendSeconds        atomic.Float64\n\t\tpopularRecommendSeconds       atomic.Float64\n\t)\n\n\tdefer MemoryInuseBytesVec.WithLabelValues(\"user_feedback_cache\").Set(0)\n\tif err := parallel.Detachable(ctx, len(users), p.Jobs, p.Config.OpenAI.ChatCompletionRPM, func(pCtx *parallel.Context, jobId int) {\n\t\tdefer func() {\n\t\t\tcompleted <- struct{}{}\n\t\t}()\n\t\tuser := users[jobId]\n\t\tuserId := user.UserId\n\t\t// skip inactive users before max recommend period\n\t\tif !p.checkUserActiveTime(ctx, userId) || !p.checkRecommendCacheOutOfDate(ctx, userId) {\n\t\t\treturn\n\t\t}\n\t\tupdateUserCount.Add(1)\n\n\t\trecommendTime := time.Now()\n\t\trecommender, err := logics.NewRecommender(p.Config.Recommend, p.CacheClient, p.DataClient, false, userId, nil)\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to create recommender\", zap.String(\"user_id\", userId), zap.Error(err))\n\t\t\treturn\n\t\t}\n\t\tif !p.dontskipColdStartUsers && recommender.IsColdStart() {\n\t\t\t// skip cold-start users without any positive feedback\n\t\t\treturn\n\t\t}\n\n\t\t// Update collaborative filtering recommendation.\n\t\tif !strings.EqualFold(p.Config.Recommend.Collaborative.Type, \"none\") && p.MatrixFactorizationUsers != nil && p.MatrixFactorizationItems != nil {\n\t\t\tif userEmbedding, ok := p.MatrixFactorizationUsers.Get(userId); ok {\n\t\t\t\terr = p.updateCollaborativeRecommend(ctx, p.MatrixFactorizationItems, userId, userEmbedding, recommender.ExcludeSet(), itemCache)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Logger().Error(\"failed to recommend by collaborative filtering\",\n\t\t\t\t\t\tzap.String(\"user_id\", userId), zap.Error(err))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} else if !p.dontskipColdStartUsers {\n\t\t\t\t// skip users without collaborative filtering embeddings\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\t// Generate recommendation from recommenders.\n\t\tvar (\n\t\t\tscores           []cache.Score\n\t\t\tdigest           string\n\t\t\trecommenderNames []string\n\t\t)\n\t\tif len(p.Config.Recommend.Ranker.Recommenders) > 0 {\n\t\t\trecommenderNames = p.Config.Recommend.Ranker.Recommenders\n\t\t} else {\n\t\t\trecommenderNames = p.Config.Recommend.ListRecommenders()\n\t\t}\n\t\tscores, digest, err = recommender.RecommendSequential(ctx, scores, 0, recommenderNames...)\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to recommend items\", zap.String(\"user_id\", userId), zap.Error(err))\n\t\t\treturn\n\t\t}\n\n\t\tcandidates := make([]cache.Score, 0, len(scores))\n\t\tcandidateSet := mapset.NewSet[string]()\n\t\titems, err := itemCache.GetMap(ctx, lo.Map(scores, func(score cache.Score, _ int) string {\n\t\t\treturn score.Id\n\t\t}))\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to download items\", zap.String(\"user_id\", userId), zap.Error(err))\n\t\t\treturn\n\t\t}\n\t\tfor _, score := range scores {\n\t\t\tif _, exist := items[score.Id]; exist {\n\t\t\t\tscore.Timestamp = recommendTime\n\t\t\t\tcandidates = append(candidates, score)\n\t\t\t\tcandidateSet.Add(score.Id)\n\t\t\t}\n\t\t}\n\n\t\t// Insert replacement items into the candidate set before ranking so all rankers (including LLM) can order them.\n\t\tvar replacementPositiveItems, replacementNegativeItems mapset.Set[string]\n\t\tif p.Config.Recommend.Replacement.EnableReplacement && p.Config.Recommend.Ranker.Type != \"none\" {\n\t\t\tcandidates, replacementPositiveItems, replacementNegativeItems, err = p.addReplacementCandidates(\n\t\t\t\tctx, candidates, candidateSet, recommender.UserFeedback(), itemCache, recommendTime,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\tlog.Logger().Error(\"failed to prepare replacement candidates\", zap.Error(err))\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\t// rank by click-through-rate\n\t\tvar results []cache.Score\n\t\tif p.Config.Recommend.Ranker.Type == \"fm\" && p.ClickThroughRateModel != nil && !p.ClickThroughRateModel.Invalid() {\n\t\t\tresults, err = p.rankByClickTroughRate(ctx, p.ClickThroughRateModel, &user, candidates, itemCache, recommendTime)\n\t\t\tif err != nil {\n\t\t\t\tlog.Logger().Error(\"failed to rank items\", zap.Error(err))\n\t\t\t\treturn\n\t\t\t}\n\t\t} else if p.Config.Recommend.Ranker.Type == \"llm\" && p.Config.OpenAI.ChatCompletionModel != \"\" {\n\t\t\tranker, err := logics.NewChatReranker(\n\t\t\t\tp.Config.Recommend.Ranker.RerankerAPI,\n\t\t\t\tp.Config.Recommend.Ranker.QueryTemplate,\n\t\t\t\tp.Config.Recommend.Ranker.DocumentTemplate)\n\t\t\tif err != nil {\n\t\t\t\tlog.Logger().Error(\"failed to create LLM ranker\", zap.Error(err))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tresults, err = p.rankByLLM(ctx, pCtx, ranker, &user, recommender.UserFeedback(), candidates, itemCache, recommendTime)\n\t\t\tif err != nil {\n\t\t\t\tlog.Logger().Error(\"failed to rank items by LLM\", zap.Error(err))\n\t\t\t\treturn\n\t\t\t}\n\t\t} else {\n\t\t\tresults = candidates\n\t\t}\n\n\t\t// Apply replacement decay after ranking so weights don't bypass the ranker ordering.\n\t\tif p.Config.Recommend.Replacement.EnableReplacement && p.Config.Recommend.Ranker.Type != \"none\" {\n\t\t\tresults = p.applyReplacementDecay(results, replacementPositiveItems, replacementNegativeItems)\n\t\t}\n\n\t\t// cache recommendation\n\t\tif err = p.CacheClient.AddScores(ctx, cache.Recommend, userId, results); err != nil {\n\t\t\tlog.Logger().Error(\"failed to cache recommendation\", zap.Error(err))\n\t\t\treturn\n\t\t}\n\t\tif err = p.CacheClient.DeleteScores(ctx, []string{cache.Recommend}, cache.ScoreCondition{\n\t\t\tBefore: &recommendTime,\n\t\t\tSubset: new(userId),\n\t\t}); err != nil {\n\t\t\tlog.Logger().Error(\"failed to delete stale recommendation\", zap.Error(err))\n\t\t\treturn\n\t\t}\n\t\tif err = p.CacheClient.Set(ctx,\n\t\t\tcache.Time(cache.Key(cache.RecommendUpdateTime, userId), recommendTime),\n\t\t\tcache.String(cache.Key(cache.RecommendDigest, userId), digest),\n\t\t); err != nil {\n\t\t\tlog.Logger().Error(\"failed to cache recommendation time\", zap.Error(err))\n\t\t}\n\t}); err != nil {\n\t\tlog.Logger().Error(\"recommendation was cancelled\", zap.Error(err))\n\t}\n\tclose(completed)\n\tlog.Logger().Info(\"complete ranking recommendation\",\n\t\tzap.String(\"used_time\", time.Since(startTime).String()))\n\tUpdateUserRecommendTotal.Set(updateUserCount.Load())\n\tOfflineRecommendTotalSeconds.Set(time.Since(startRecommendTime).Seconds())\n\tOfflineRecommendStepSecondsVec.WithLabelValues(\"collaborative_recommend\").Set(collaborativeRecommendSeconds.Load())\n\tOfflineRecommendStepSecondsVec.WithLabelValues(\"item_based_recommend\").Set(itemBasedRecommendSeconds.Load())\n\tOfflineRecommendStepSecondsVec.WithLabelValues(\"user_based_recommend\").Set(userBasedRecommendSeconds.Load())\n\tOfflineRecommendStepSecondsVec.WithLabelValues(\"latest_recommend\").Set(latestRecommendSeconds.Load())\n\tOfflineRecommendStepSecondsVec.WithLabelValues(\"popular_recommend\").Set(popularRecommendSeconds.Load())\n}\n\n// checkUserActiveTime checks if a user is active based on their last modification time.\nfunc (p *Pipeline) checkUserActiveTime(ctx context.Context, userId string) bool {\n\tif p.Config.Recommend.ActiveUserTTL == 0 {\n\t\treturn true\n\t}\n\t// read active time\n\tactiveTime, err := p.CacheClient.Get(ctx, cache.Key(cache.LastModifyUserTime, userId)).Time()\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to read last modify user time\", zap.String(\"user_id\", userId), zap.Error(err))\n\t\treturn true\n\t}\n\tif activeTime.IsZero() {\n\t\treturn true\n\t}\n\t// check active time\n\tif time.Since(activeTime) < time.Duration(p.Config.Recommend.ActiveUserTTL*24)*time.Hour {\n\t\treturn true\n\t}\n\t// remove recommend cache for inactive users\n\tif err := p.CacheClient.DeleteScores(ctx, []string{cache.Recommend},\n\t\tcache.ScoreCondition{Subset: new(userId)}); err != nil {\n\t\tlog.Logger().Error(\"failed to delete recommend cache\", zap.String(\"user_id\", userId), zap.Error(err))\n\t}\n\treturn false\n}\n\n// checkRecommendCacheOutOfDate checks if recommend cache stale.\nfunc (p *Pipeline) checkRecommendCacheOutOfDate(ctx context.Context, userId string) bool {\n\tvar (\n\t\tactiveTime    time.Time\n\t\trecommendTime time.Time\n\t\terr           error\n\t)\n\n\t// 1. If cache is empty, stale.\n\titems, err := p.CacheClient.SearchScores(ctx, cache.Recommend, userId, nil, 0, -1)\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to load offline recommendation\", zap.String(\"user_id\", userId), zap.Error(err))\n\t\treturn true\n\t} else if len(items) == 0 {\n\t\treturn true\n\t}\n\n\t// 2. If digest is empty or not match, stale.\n\tdigest, err := p.CacheClient.Get(ctx, cache.Key(cache.RecommendDigest, userId)).String()\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to read offline recommendation digest\", zap.String(\"user_id\", userId), zap.Error(err))\n\t\treturn true\n\t}\n\tif digest == \"\" {\n\t\treturn true\n\t}\n\tif digest != p.Config.Recommend.Hash() {\n\t\treturn true\n\t}\n\n\t// read active time\n\tactiveTime, err = p.CacheClient.Get(ctx, cache.Key(cache.LastModifyUserTime, userId)).Time()\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to read last modify user time\", zap.String(\"user_id\", userId), zap.Error(err))\n\t}\n\n\t// 3. If update time is empty, stale.\n\trecommendTime, err = p.CacheClient.Get(ctx, cache.Key(cache.RecommendUpdateTime, userId)).Time()\n\tif err != nil {\n\t\tlog.Logger().Error(\"failed to read last update user recommend time\", zap.Error(err))\n\t\treturn true\n\t}\n\n\t// 4. If update time + cache expire > current time, not stale.\n\tif recommendTime.Before(time.Now().Add(-p.Config.Recommend.CacheExpire)) {\n\t\treturn true\n\t}\n\n\t// 5. If active time > recommend time, not stale.\n\tif activeTime.Before(recommendTime) {\n\t\ttimeoutTime := recommendTime.Add(p.Config.Recommend.Ranker.CacheExpire)\n\t\treturn timeoutTime.Before(time.Now())\n\t}\n\treturn true\n}\n\nfunc (p *Pipeline) updateCollaborativeRecommend(\n\tctx context.Context,\n\titems *logics.MatrixFactorizationItems,\n\tuserId string,\n\tuserEmbedding []float32,\n\texcludeSet mapset.Set[string],\n\titemCache *ItemCache,\n) error {\n\tlocalStartTime := time.Now()\n\tscores := items.Search(userEmbedding, p.Config.Recommend.CacheSize+excludeSet.Cardinality())\n\t// update categories\n\titemsMap, err := itemCache.GetMap(ctx, lo.Map(scores, func(score cache.Score, _ int) string {\n\t\treturn score.Id\n\t}))\n\tif err != nil {\n\t\treturn errors.Trace(err)\n\t}\n\t// remove excluded items and non-existing items\n\trecommend := make([]cache.Score, 0, len(scores))\n\tfor i := range scores {\n\t\tif item, exist := itemsMap[scores[i].Id]; exist && !excludeSet.Contains(item.ItemId) {\n\t\t\trecommend = append(recommend, cache.Score{\n\t\t\t\tId:         scores[i].Id,\n\t\t\t\tScore:      scores[i].Score,\n\t\t\t\tCategories: item.Categories,\n\t\t\t\t// the scores use the timestamp of the ranking index, which is only refreshed every so often.\n\t\t\t\t// if we don't overwrite the timestamp here, the code below will delete all scores that were\n\t\t\t\t// just written.\n\t\t\t\tTimestamp: localStartTime,\n\t\t\t})\n\t\t}\n\t}\n\tif err := p.CacheClient.AddScores(ctx, cache.CollaborativeFiltering, userId, recommend); err != nil {\n\t\tlog.Logger().Error(\"failed to cache collaborative filtering recommendation result\", zap.String(\"user_id\", userId), zap.Error(err))\n\t\treturn errors.Trace(err)\n\t}\n\tif err := p.CacheClient.Set(ctx,\n\t\tcache.Time(cache.Key(cache.CollaborativeFilteringUpdateTime, userId), localStartTime),\n\t\tcache.String(cache.Key(cache.CollaborativeFilteringDigest, userId), p.Config.Recommend.Collaborative.Hash(&p.Config.Recommend)),\n\t); err != nil {\n\t\tlog.Logger().Error(\"failed to cache collaborative filtering recommendation time\", zap.String(\"user_id\", userId), zap.Error(err))\n\t\treturn errors.Trace(err)\n\t}\n\tif err := p.CacheClient.DeleteScores(ctx, []string{cache.CollaborativeFiltering}, cache.ScoreCondition{Before: &localStartTime, Subset: new(userId)}); err != nil {\n\t\tlog.Logger().Error(\"failed to delete stale collaborative filtering recommendation result\", zap.String(\"user_id\", userId), zap.Error(err))\n\t\treturn errors.Trace(err)\n\t}\n\treturn nil\n}\n\n// rankByClickTroughRate ranks items by predicted click-through-rate.\nfunc (p *Pipeline) rankByClickTroughRate(\n\tctx context.Context,\n\tpredictor ctr.FactorizationMachines,\n\tuser *data.User,\n\tcandidates []cache.Score,\n\titemCache *ItemCache,\n\trecommendTime time.Time,\n) ([]cache.Score, error) {\n\t// download items\n\titems, err := itemCache.GetSlice(ctx, lo.Map(candidates, func(score cache.Score, _ int) string {\n\t\treturn score.Id\n\t}))\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\t// rank by CTR\n\ttopItems := make([]cache.Score, 0, len(items))\n\tif batchPredictor, ok := predictor.(ctr.BatchInference); ok {\n\t\tinputs := make([]lo.Tuple4[string, string, []ctr.Label, []ctr.Label], len(items))\n\t\tembeddings := make([][]ctr.Embedding, len(items))\n\t\tfor i, item := range items {\n\t\t\tinputs[i].A = user.UserId\n\t\t\tinputs[i].B = item.ItemId\n\t\t\tinputs[i].C = ctr.ConvertLabels(user.Labels)\n\t\t\tinputs[i].D = ctr.ConvertLabels(item.Labels)\n\t\t\tembeddings[i] = ctr.ConvertEmbeddings(item.Labels)\n\t\t}\n\t\toutput := batchPredictor.BatchPredict(inputs, embeddings, p.Jobs)\n\t\tfor i, score := range output {\n\t\t\ttopItems = append(topItems, cache.Score{\n\t\t\t\tId:         items[i].ItemId,\n\t\t\t\tScore:      float64(score),\n\t\t\t\tCategories: items[i].Categories,\n\t\t\t\tTimestamp:  recommendTime,\n\t\t\t})\n\t\t}\n\t} else {\n\t\tfor _, item := range items {\n\t\t\ttopItems = append(topItems, cache.Score{\n\t\t\t\tId:         item.ItemId,\n\t\t\t\tScore:      float64(predictor.Predict(user.UserId, item.ItemId, ctr.ConvertLabels(user.Labels), ctr.ConvertLabels(item.Labels))),\n\t\t\t\tCategories: item.Categories,\n\t\t\t\tTimestamp:  recommendTime,\n\t\t\t})\n\t\t}\n\t}\n\tcache.SortDocuments(topItems)\n\treturn topItems, nil\n}\n\nfunc (p *Pipeline) rankByLLM(\n\tctx context.Context,\n\tpCtx *parallel.Context,\n\tranker *logics.ChatReranker,\n\tuser *data.User,\n\tfeedback []data.Feedback,\n\tcandidates []cache.Score,\n\titemCache *ItemCache,\n\trecommendTime time.Time,\n) ([]cache.Score, error) {\n\t// download items\n\titems, err := itemCache.GetSlice(ctx, lo.Map(candidates, func(score cache.Score, _ int) string {\n\t\treturn score.Id\n\t}))\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\t// convert feedback\n\tdata.SortFeedbacks(feedback)\n\tcontextUserFeedback := make([]data.Feedback, 0, p.Config.Recommend.ContextSize)\n\tfor _, f := range feedback {\n\t\tif p.Config.Recommend.ContextSize <= len(contextUserFeedback) {\n\t\t\tbreak\n\t\t}\n\t\tif expression.MatchFeedbackTypeExpressions(p.Config.Recommend.DataSource.PositiveFeedbackTypes, f.FeedbackType, f.Value) {\n\t\t\tcontextUserFeedback = append(contextUserFeedback, f)\n\t\t}\n\t}\n\titemMap, err := itemCache.GetMap(ctx, lo.Map(contextUserFeedback, func(fb data.Feedback, _ int) string {\n\t\treturn fb.ItemId\n\t}))\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\tfeedbackItems := make([]*logics.FeedbackItem, 0, len(contextUserFeedback))\n\tfor _, fb := range contextUserFeedback {\n\t\tif item, exist := itemMap[fb.ItemId]; exist {\n\t\t\tfeedbackItems = append(feedbackItems, &logics.FeedbackItem{\n\t\t\t\tFeedbackType: fb.FeedbackType,\n\t\t\t\tItem:         *item,\n\t\t\t})\n\t\t}\n\t}\n\t// rank by LLM\n\tpCtx.Detach()\n\tparsed, err := ranker.Rank(ctx, user, feedbackItems, items)\n\tpCtx.Attach()\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\t// construct scores\n\tvar topItems []cache.Score\n\titemCategories := make(map[string][]string, len(items))\n\tfor _, item := range items {\n\t\tif item == nil {\n\t\t\tcontinue\n\t\t}\n\t\titemCategories[item.ItemId] = item.Categories\n\t}\n\tfor _, item := range parsed {\n\t\ttopItems = append(topItems, cache.Score{\n\t\t\tId:         item.Id,\n\t\t\tScore:      item.Score,\n\t\t\tTimestamp:  recommendTime,\n\t\t\tCategories: itemCategories[item.Id],\n\t\t})\n\t}\n\treturn topItems, nil\n}\n\n// replacement inserts historical items back to recommendation.\n// It now adds the replacement items before ranking, then applies decay after ranking.\nfunc (p *Pipeline) addReplacementCandidates(\n\tctx context.Context,\n\tcandidates []cache.Score,\n\tcandidateSet mapset.Set[string],\n\tfeedbacks []data.Feedback,\n\titemCache *ItemCache,\n\trecommendTime time.Time,\n) ([]cache.Score, mapset.Set[string], mapset.Set[string], error) {\n\tpositiveItems := mapset.NewSet[string]()\n\tdistinctItems := mapset.NewSet[string]()\n\tfor _, feedback := range feedbacks {\n\t\tif expression.MatchFeedbackTypeExpressions(p.Config.Recommend.DataSource.PositiveFeedbackTypes, feedback.FeedbackType, feedback.Value) {\n\t\t\tpositiveItems.Add(feedback.ItemId)\n\t\t\tdistinctItems.Add(feedback.ItemId)\n\t\t} else if expression.MatchFeedbackTypeExpressions(p.Config.Recommend.DataSource.ReadFeedbackTypes, feedback.FeedbackType, feedback.Value) {\n\t\t\tdistinctItems.Add(feedback.ItemId)\n\t\t}\n\t}\n\n\tif distinctItems.Cardinality() == 0 {\n\t\treturn candidates, positiveItems, mapset.NewSet[string](), nil\n\t}\n\n\titems, err := itemCache.GetSlice(ctx, distinctItems.ToSlice())\n\tif err != nil {\n\t\treturn nil, nil, nil, errors.Trace(err)\n\t}\n\t// Only keep items that exist and aren't hidden in the cache.\n\tfor _, item := range items {\n\t\tif candidateSet.Contains(item.ItemId) {\n\t\t\tcontinue\n\t\t}\n\t\tcandidates = append(candidates, cache.Score{Id: item.ItemId, Categories: item.Categories, Timestamp: recommendTime})\n\t\tcandidateSet.Add(item.ItemId)\n\t}\n\n\t// Build negative items set from distinct minus positive, filtered by existence.\n\texistingSet := mapset.NewSet(lo.Map(items, func(it *data.Item, _ int) string { return it.ItemId })...)\n\tpositiveExisting := positiveItems.Intersect(existingSet)\n\tnegativeItems := distinctItems.Difference(positiveItems).Intersect(existingSet)\n\n\treturn candidates, positiveExisting, negativeItems, nil\n}\n\nfunc (p *Pipeline) applyReplacementDecay(\n\tresults []cache.Score,\n\tpositiveItems mapset.Set[string],\n\tnegativeItems mapset.Set[string],\n) []cache.Score {\n\tif (positiveItems == nil || positiveItems.Cardinality() == 0) && (negativeItems == nil || negativeItems.Cardinality() == 0) {\n\t\treturn results\n\t}\n\n\tupdated := make([]cache.Score, len(results))\n\tcopy(updated, results)\n\tchanged := false\n\tfor i := range updated {\n\t\tswitch {\n\t\tcase positiveItems != nil && positiveItems.Contains(updated[i].Id):\n\t\t\tupdated[i].Score *= p.Config.Recommend.Replacement.PositiveReplacementDecay\n\t\t\tchanged = true\n\t\tcase negativeItems != nil && negativeItems.Contains(updated[i].Id):\n\t\t\tupdated[i].Score *= p.Config.Recommend.Replacement.ReadReplacementDecay\n\t\t\tchanged = true\n\t\t}\n\t}\n\tif changed {\n\t\tcache.SortDocuments(updated)\n\t}\n\treturn updated\n}\n\n// ItemCache is alias of map[string]data.Item.\ntype ItemCache struct {\n\tClient data.Database\n\tData   sync.Map\n}\n\n// NewItemCache creates a new ItemCache.\nfunc NewItemCache(client data.Database) *ItemCache {\n\treturn &ItemCache{\n\t\tClient: client,\n\t\tData:   sync.Map{},\n\t}\n}\n\nfunc (c *ItemCache) GetSlice(ctx context.Context, itemIds []string) ([]*data.Item, error) {\n\trequests := make([]string, 0, len(itemIds))\n\tfor _, itemId := range itemIds {\n\t\tif _, exist := c.Data.Load(itemId); !exist {\n\t\t\trequests = append(requests, itemId)\n\t\t}\n\t}\n\tresponse, err := c.Client.BatchGetItems(ctx, requests)\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\tfor _, item := range response {\n\t\tc.Data.Store(item.ItemId, &item)\n\t}\n\titems := make([]*data.Item, 0, len(itemIds))\n\tfor _, itemId := range itemIds {\n\t\tif val, exist := c.Data.Load(itemId); exist {\n\t\t\titem := val.(*data.Item)\n\t\t\tif !item.IsHidden {\n\t\t\t\titems = append(items, item)\n\t\t\t}\n\t\t}\n\t}\n\treturn items, nil\n}\n\nfunc (c *ItemCache) GetMap(ctx context.Context, itemIds []string) (map[string]*data.Item, error) {\n\titems, err := c.GetSlice(ctx, itemIds)\n\tif err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\treturn lo.SliceToMap(items, func(item *data.Item) (string, *data.Item) {\n\t\treturn item.ItemId, item\n\t}), nil\n}\n"
  },
  {
    "path": "worker/pipeline_test.go",
    "content": "// Copyright 2025 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage worker\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\ntype PipelineTestSuite struct {\n\tsuite.Suite\n\tdataClient data.Database\n}\n\nfunc (suite *PipelineTestSuite) SetupSuite() {\n\tvar err error\n\tsuite.dataClient, err = data.Open(fmt.Sprintf(\"sqlite://%s/data.db\", suite.T().TempDir()), \"\")\n\tsuite.NoError(err)\n\terr = suite.dataClient.Init()\n\tsuite.NoError(err)\n\n\t// insert items\n\terr = suite.dataClient.BatchInsertItems(suite.T().Context(), []data.Item{\n\t\t{ItemId: \"1\"},\n\t\t{ItemId: \"2\"},\n\t\t{ItemId: \"3\"},\n\t\t{ItemId: \"4\"},\n\t\t{ItemId: \"5\"},\n\t})\n\tsuite.NoError(err)\n}\n\nfunc (suite *PipelineTestSuite) TearDownSuite() {\n\terr := suite.dataClient.Close()\n\tsuite.NoError(err)\n}\n\nfunc (suite *PipelineTestSuite) TestGetSlice() {\n\tc := NewItemCache(suite.dataClient)\n\titems, err := c.GetSlice(suite.T().Context(), []string{\"1\", \"2\", \"3\", \"4\", \"5\", \"6\"})\n\tsuite.NoError(err)\n\tsuite.Equal(5, len(items))\n}\n\nfunc (suite *PipelineTestSuite) TestGetMap() {\n\tc := NewItemCache(suite.dataClient)\n\titems, err := c.GetMap(suite.T().Context(), []string{\"1\", \"2\", \"3\", \"4\", \"5\", \"6\"})\n\tsuite.NoError(err)\n\tsuite.Equal(5, len(items))\n}\n\nfunc TestPipeline(t *testing.T) {\n\tsuite.Run(t, new(PipelineTestSuite))\n}\n"
  },
  {
    "path": "worker/worker.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage worker\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gorse-io/gorse/cmd/version\"\n\t\"github.com/gorse-io/gorse/common/log\"\n\t\"github.com/gorse-io/gorse/common/monitor\"\n\t\"github.com/gorse-io/gorse/common/util\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/logics\"\n\t\"github.com/gorse-io/gorse/model/ctr\"\n\t\"github.com/gorse-io/gorse/protocol\"\n\t\"github.com/gorse-io/gorse/storage\"\n\t\"github.com/gorse-io/gorse/storage/blob\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/juju/errors\"\n\t\"github.com/lafikl/consistent\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n\t\"github.com/samber/lo\"\n\t\"go.uber.org/zap\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n)\n\nconst batchSize = 10000\n\n// Worker manages states of a worker node.\ntype Worker struct {\n\tPipeline\n\ttestMode bool\n\n\tcollaborativeFilteringModelId int64\n\tclickThroughRateModelId       int64\n\n\t// worker config\n\tworkerName string\n\thttpHost   string\n\thttpPort   int\n\tmasterHost string\n\tmasterPort int\n\ttlsConfig  *util.TLSConfig\n\tcacheFile  string\n\n\t// database connection path\n\tcachePath   string\n\tcachePrefix string\n\tdataPath    string\n\tdataPrefix  string\n\n\tblobConfig string\n\tblobStore  blob.Store\n\n\t// master connection\n\tconn         *grpc.ClientConn\n\tmasterClient protocol.MasterClient\n\n\tlatestCollaborativeFilteringModelId int64\n\tlatestClickThroughRateModelId       int64\n\trandGenerator                       *rand.Rand\n\n\t// peers\n\tpeers []string\n\tme    string\n\n\t// events\n\ttickDuration time.Duration\n\tticker       *time.Ticker\n\tsyncedChan   chan struct{} // meta synced events\n\tpulledChan   chan struct{} // model pulled events\n}\n\n// NewWorker creates a new worker node.\nfunc NewWorker(\n\tmasterHost string,\n\tmasterPort int,\n\thttpHost string,\n\thttpPort int,\n\tjobs int,\n\tcacheFile string,\n\ttlsConfig *util.TLSConfig,\n\tinterval time.Duration,\n) *Worker {\n\treturn &Worker{\n\t\tPipeline: Pipeline{\n\t\t\tConfig:      config.GetDefaultConfig(),\n\t\t\tCacheClient: new(cache.NoDatabase),\n\t\t\tDataClient:  new(data.NoDatabase),\n\t\t\tJobs:        jobs,\n\t\t},\n\t\trandGenerator: util.NewRand(time.Now().UTC().UnixNano()),\n\t\t// config\n\t\tcacheFile:  cacheFile,\n\t\tmasterHost: masterHost,\n\t\tmasterPort: masterPort,\n\t\ttlsConfig:  tlsConfig,\n\t\thttpHost:   httpHost,\n\t\thttpPort:   httpPort,\n\t\t// events\n\t\ttickDuration: interval,\n\t\tticker:       time.NewTicker(interval),\n\t\tsyncedChan:   make(chan struct{}, 1),\n\t\tpulledChan:   make(chan struct{}, 1),\n\t}\n}\n\n// Sync this worker to the master.\nfunc (w *Worker) Sync() {\n\tvar nextBlobConfig config.BlobConfig\n\tlog.Logger().Info(\"start meta sync\", zap.Duration(\"meta_timeout\", w.Config.Master.MetaTimeout))\n\tfor {\n\t\tvar meta *protocol.Meta\n\t\tvar err error\n\t\tif meta, err = w.masterClient.GetMeta(context.Background(),\n\t\t\t&protocol.NodeInfo{\n\t\t\t\tNodeType:      protocol.NodeType_Worker,\n\t\t\t\tUuid:          w.workerName,\n\t\t\t\tBinaryVersion: version.Version,\n\t\t\t\tHostname:      lo.Must(os.Hostname()),\n\t\t\t}); err != nil {\n\t\t\tlog.Logger().Error(\"failed to get meta\", zap.Error(err))\n\t\t\tgoto sleep\n\t\t}\n\n\t\t// load master config\n\t\terr = json.Unmarshal([]byte(meta.Config), &w.Config)\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to parse master config\", zap.Error(err))\n\t\t\tgoto sleep\n\t\t}\n\n\t\t// connect to data store\n\t\tif w.dataPath != w.Config.Database.DataStore || w.dataPrefix != w.Config.Database.DataTablePrefix {\n\t\t\tif strings.HasPrefix(w.Config.Database.DataStore, storage.SQLitePrefix) {\n\t\t\t\tlog.Logger().Info(\"connect data store via master\")\n\t\t\t\tw.DataClient = data.NewProxyClient(w.conn)\n\t\t\t} else {\n\t\t\t\tlog.Logger().Info(\"connect data store\",\n\t\t\t\t\tzap.String(\"database\", log.RedactDBURL(w.Config.Database.DataStore)))\n\t\t\t\tdataOpts := w.Config.Database.StorageOptions(w.Config.Database.DataStore)\n\t\t\t\tif w.DataClient, err = data.Open(w.Config.Database.DataStore, w.Config.Database.DataTablePrefix, dataOpts...); err != nil {\n\t\t\t\t\tlog.Logger().Error(\"failed to connect data store\", zap.Error(err))\n\t\t\t\t\tgoto sleep\n\t\t\t\t}\n\t\t\t}\n\t\t\tw.dataPath = w.Config.Database.DataStore\n\t\t\tw.dataPrefix = w.Config.Database.DataTablePrefix\n\t\t}\n\n\t\t// connect to cache store\n\t\tif w.cachePath != w.Config.Database.CacheStore || w.cachePrefix != w.Config.Database.CacheTablePrefix {\n\t\t\tif strings.HasPrefix(w.Config.Database.CacheStore, storage.SQLitePrefix) {\n\t\t\t\tlog.Logger().Info(\"connect cache store via master\")\n\t\t\t\tw.CacheClient = cache.NewProxyClient(w.conn)\n\t\t\t} else {\n\t\t\t\tlog.Logger().Info(\"connect cache store\",\n\t\t\t\t\tzap.String(\"database\", log.RedactDBURL(w.Config.Database.CacheStore)))\n\t\t\t\tcacheOpts := w.Config.Database.StorageOptions(w.Config.Database.CacheStore)\n\t\t\t\tif w.CacheClient, err = cache.Open(w.Config.Database.CacheStore, w.Config.Database.CacheTablePrefix, cacheOpts...); err != nil {\n\t\t\t\t\tlog.Logger().Error(\"failed to connect cache store\", zap.Error(err))\n\t\t\t\t\tgoto sleep\n\t\t\t\t}\n\t\t\t}\n\t\t\tw.cachePath = w.Config.Database.CacheStore\n\t\t\tw.cachePrefix = w.Config.Database.CacheTablePrefix\n\t\t}\n\n\t\t// connect to blob store\n\t\tnextBlobConfig = w.Config.Blob\n\t\tif w.blobConfig != nextBlobConfig.URI {\n\t\t\tw.blobStore, err = blob.NewStore(w.Config.Blob, w.conn)\n\t\t\tif err != nil {\n\t\t\t\tlog.Logger().Error(\"failed to connect blob store\", zap.Error(err))\n\t\t\t\tgoto sleep\n\t\t\t}\n\t\t\tw.blobConfig = nextBlobConfig.URI\n\t\t}\n\n\t\t// synchronize collaborative filtering model\n\t\tw.latestCollaborativeFilteringModelId = meta.CollaborativeFilteringModelId\n\t\tif w.latestCollaborativeFilteringModelId > w.collaborativeFilteringModelId {\n\t\t\tlog.Logger().Info(\"new ranking model found\",\n\t\t\t\tzap.Int64(\"old_version\", w.collaborativeFilteringModelId),\n\t\t\t\tzap.Int64(\"new_version\", w.latestCollaborativeFilteringModelId))\n\n\t\t\tselect {\n\t\t\tcase w.syncedChan <- struct{}{}:\n\t\t\tdefault:\n\t\t\t}\n\t\t}\n\n\t\t// synchronize click-through rate model\n\t\tw.latestClickThroughRateModelId = meta.ClickThroughRateModelId\n\t\tif w.latestClickThroughRateModelId > w.clickThroughRateModelId {\n\t\t\tlog.Logger().Info(\"new click model found\",\n\t\t\t\tzap.Int64(\"old_version\", w.clickThroughRateModelId),\n\t\t\t\tzap.Int64(\"new_version\", w.latestClickThroughRateModelId))\n\n\t\t\tselect {\n\t\t\tcase w.syncedChan <- struct{}{}:\n\t\t\tdefault:\n\t\t\t}\n\t\t}\n\n\t\tw.peers = meta.Workers\n\t\tw.me = meta.Me\n\tsleep:\n\t\tif w.testMode {\n\t\t\treturn\n\t\t}\n\t\ttime.Sleep(w.Config.Master.MetaTimeout)\n\t}\n}\n\n// Pull user index and ranking model from master.\nfunc (w *Worker) Pull() {\n\tfor range w.syncedChan {\n\t\tpulled := false\n\n\t\t// pull ranking model\n\t\tif w.latestCollaborativeFilteringModelId > w.collaborativeFilteringModelId {\n\t\t\tlog.Logger().Info(\"start pull collaborative filtering model\")\n\t\t\tr, err := w.blobStore.Open(strconv.FormatInt(w.latestCollaborativeFilteringModelId, 10))\n\t\t\tif err != nil {\n\t\t\t\tlog.Logger().Error(\"failed to open collaborative filtering model\", zap.Error(err))\n\t\t\t} else {\n\t\t\t\titems := logics.NewMatrixFactorizationItems(time.Time{})\n\t\t\t\tusers := logics.NewMatrixFactorizationUsers()\n\t\t\t\tif err = items.Unmarshal(r); err != nil {\n\t\t\t\t\tlog.Logger().Error(\"failed to unmarshal matrix factorization items\", zap.Error(err))\n\t\t\t\t} else if err = users.Unmarshal(r); err != nil {\n\t\t\t\t\tlog.Logger().Error(\"failed to unmarshal matrix factorization users\", zap.Error(err))\n\t\t\t\t} else {\n\t\t\t\t\tw.MatrixFactorizationItems = items\n\t\t\t\t\tw.MatrixFactorizationUsers = users\n\t\t\t\t\tw.collaborativeFilteringModelId = w.latestCollaborativeFilteringModelId\n\t\t\t\t\tlog.Logger().Info(\"synced collaborative filtering model\",\n\t\t\t\t\t\tzap.Int64(\"id\", w.collaborativeFilteringModelId))\n\t\t\t\t\tpulled = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// pull click model\n\t\tif w.latestClickThroughRateModelId > w.clickThroughRateModelId {\n\t\t\tlog.Logger().Info(\"start pull click model\")\n\t\t\tr, err := w.blobStore.Open(strconv.FormatInt(w.latestClickThroughRateModelId, 10))\n\t\t\tif err != nil {\n\t\t\t\tlog.Logger().Error(\"failed to open click-through rate model\", zap.Error(err))\n\t\t\t} else {\n\t\t\t\tmodel, err := ctr.UnmarshalModel(r)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Logger().Error(\"failed to unmarshal click-through rate model\", zap.Error(err))\n\t\t\t\t} else {\n\t\t\t\t\tw.ClickThroughRateModel = model\n\t\t\t\t\tw.clickThroughRateModelId = w.latestClickThroughRateModelId\n\t\t\t\t\tlog.Logger().Info(\"synced click-through rate model\",\n\t\t\t\t\t\tzap.Int64(\"version\", w.clickThroughRateModelId))\n\t\t\t\t\tpulled = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif w.testMode {\n\t\t\treturn\n\t\t}\n\t\tif pulled {\n\t\t\tselect {\n\t\t\tcase w.pulledChan <- struct{}{}:\n\t\t\tdefault:\n\t\t\t}\n\t\t}\n\t}\n}\n\n// ServeHTTP serves Prometheus metrics and API.\nfunc (w *Worker) ServeHTTP() {\n\thttp.Handle(\"/metrics\", promhttp.Handler())\n\thttp.HandleFunc(\"/api/health/live\", w.checkLive)\n\thttp.HandleFunc(\"/api/health/ready\", w.checkReady)\n\terr := http.ListenAndServe(fmt.Sprintf(\"%s:%d\", w.httpHost, w.httpPort), nil)\n\tif err != nil {\n\t\tlog.Logger().Fatal(\"failed to start http server\", zap.Error(err))\n\t}\n}\n\nfunc writeJSON(w http.ResponseWriter, content any) {\n\tw.WriteHeader(http.StatusOK)\n\tbytes, err := json.Marshal(content)\n\tif err != nil {\n\t\twriteError(w, err.Error(), http.StatusInternalServerError)\n\t}\n\tif _, err = w.Write(bytes); err != nil {\n\t\twriteError(w, err.Error(), http.StatusInternalServerError)\n\t}\n}\n\nfunc writeError(w http.ResponseWriter, error string, code int) {\n\tlog.Logger().Error(strings.ToLower(http.StatusText(code)), zap.String(\"error\", error))\n\thttp.Error(w, error, code)\n}\n\n// Serve as a worker node.\nfunc (w *Worker) Serve() {\n\tvar err error\n\tif w.workerName, err = w.WorkerName(); err != nil {\n\t\tlog.Logger().Fatal(\"failed to get worker name\", zap.Error(err))\n\t}\n\n\t// create progress tracer\n\tw.Tracer = monitor.NewTracer(w.workerName)\n\n\t// connect to master\n\tvar opts []grpc.DialOption\n\topts = append(opts, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(512*1024*1024)))\n\tif w.tlsConfig != nil {\n\t\tc, err := util.NewClientCreds(w.tlsConfig)\n\t\tif err != nil {\n\t\t\tlog.Logger().Fatal(\"failed to create credentials\", zap.Error(err))\n\t\t}\n\t\topts = append(opts, grpc.WithTransportCredentials(c))\n\t} else {\n\t\topts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))\n\t}\n\tw.conn, err = grpc.Dial(net.JoinHostPort(w.masterHost, strconv.Itoa(w.masterPort)), opts...)\n\tif err != nil {\n\t\tlog.Logger().Fatal(\"failed to connect master\", zap.Error(err))\n\t}\n\tw.masterClient = protocol.NewMasterClient(w.conn)\n\n\tgo w.Sync()\n\tgo w.Pull()\n\tgo w.ServeHTTP()\n\n\tloop := func() {\n\t\t// pull users\n\t\tworkingUsers, err := w.pullUsers(w.peers, w.me)\n\t\tif err != nil {\n\t\t\tlog.Logger().Error(\"failed to split users\", zap.Error(err),\n\t\t\t\tzap.String(\"me\", w.me),\n\t\t\t\tzap.Strings(\"workers\", w.peers))\n\t\t\treturn\n\t\t}\n\n\t\t// recommendation\n\t\tw.Recommend(context.Background(), workingUsers, func(completed, throughput int) {\n\t\t\tlog.Logger().Info(\"ranking recommendation\",\n\t\t\t\tzap.Int(\"n_complete_users\", completed),\n\t\t\t\tzap.Int(\"throughput\", throughput))\n\t\t\tif w.masterClient != nil {\n\t\t\t\tif _, err := w.masterClient.PushProgress(context.Background(), monitor.EncodeProgress(w.Tracer.List())); err != nil {\n\t\t\t\t\tlog.Logger().Error(\"failed to report update task\", zap.Error(err))\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n\tfor {\n\t\tselect {\n\t\tcase tick := <-w.ticker.C:\n\t\t\tif time.Since(tick) <= w.tickDuration {\n\t\t\t\tloop()\n\t\t\t}\n\t\tcase <-w.pulledChan:\n\t\t\tloop()\n\t\t}\n\t}\n}\n\nfunc (w *Worker) WorkerName() (string, error) {\n\thostname, err := os.Hostname()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\thash := md5.New()\n\thash.Write([]byte(hostname))\n\thash.Write([]byte(w.httpHost))\n\thash.Write([]byte(strconv.Itoa(w.httpPort)))\n\tb := hash.Sum(nil)\n\treturn hex.EncodeToString(b), nil\n}\n\nfunc (w *Worker) pullUsers(peers []string, me string) ([]data.User, error) {\n\tctx := context.Background()\n\t// locate me\n\tif !lo.Contains(peers, me) {\n\t\treturn nil, errors.New(\"current node isn't in worker nodes\")\n\t}\n\t// create consistent hash ring\n\tc := consistent.New()\n\tfor _, peer := range peers {\n\t\tc.Add(peer)\n\t}\n\t// pull users from database\n\tvar users []data.User\n\tuserChan, errChan := w.DataClient.GetUserStream(ctx, batchSize)\n\tfor batchUsers := range userChan {\n\t\tfor _, user := range batchUsers {\n\t\t\tp, err := c.Get(user.UserId)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Trace(err)\n\t\t\t}\n\t\t\tif p == me {\n\t\t\t\tusers = append(users, user)\n\t\t\t}\n\t\t}\n\t}\n\tif err := <-errChan; err != nil {\n\t\treturn nil, errors.Trace(err)\n\t}\n\treturn users, nil\n}\n\ntype HealthStatus struct {\n\tReady               bool\n\tDataStoreError      error\n\tCacheStoreError     error\n\tDataStoreConnected  bool\n\tCacheStoreConnected bool\n}\n\nfunc (w *Worker) checkHealth() HealthStatus {\n\thealthStatus := HealthStatus{}\n\thealthStatus.DataStoreError = w.DataClient.Ping()\n\thealthStatus.CacheStoreError = w.CacheClient.Ping()\n\thealthStatus.DataStoreConnected = healthStatus.DataStoreError == nil\n\thealthStatus.CacheStoreConnected = healthStatus.CacheStoreError == nil\n\thealthStatus.Ready = healthStatus.DataStoreConnected && healthStatus.CacheStoreConnected\n\treturn healthStatus\n}\n\nfunc (w *Worker) checkLive(writer http.ResponseWriter, _ *http.Request) {\n\thealthStatus := w.checkHealth()\n\twriteJSON(writer, healthStatus)\n}\n\nfunc (w *Worker) checkReady(writer http.ResponseWriter, _ *http.Request) {\n\thealthStatus := w.checkHealth()\n\tif healthStatus.Ready {\n\t\twriteJSON(writer, healthStatus)\n\t} else {\n\t\terrReason, err := json.Marshal(healthStatus)\n\t\tif err != nil {\n\t\t\twriteError(writer, err.Error(), http.StatusInternalServerError)\n\t\t} else {\n\t\t\twriteError(writer, string(errReason), http.StatusServiceUnavailable)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "worker/worker_test.go",
    "content": "// Copyright 2020 gorse Project Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage worker\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/c-bata/goptuna\"\n\t\"github.com/gorse-io/gorse/common/expression\"\n\t\"github.com/gorse-io/gorse/common/monitor\"\n\t\"github.com/gorse-io/gorse/common/reranker\"\n\t\"github.com/gorse-io/gorse/common/util\"\n\t\"github.com/gorse-io/gorse/config\"\n\t\"github.com/gorse-io/gorse/dataset\"\n\t\"github.com/gorse-io/gorse/logics\"\n\t\"github.com/gorse-io/gorse/model\"\n\t\"github.com/gorse-io/gorse/model/cf\"\n\t\"github.com/gorse-io/gorse/model/ctr\"\n\t\"github.com/gorse-io/gorse/protocol\"\n\t\"github.com/gorse-io/gorse/storage/cache\"\n\t\"github.com/gorse-io/gorse/storage/data\"\n\t\"github.com/samber/lo\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/suite\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n)\n\ntype WorkerTestSuite struct {\n\tsuite.Suite\n\tWorker\n}\n\nfunc (suite *WorkerTestSuite) SetupSuite() {\n\t// open database\n\tvar err error\n\tsuite.Tracer = monitor.NewTracer(\"test\")\n\tsuite.Config = config.GetDefaultConfig()\n\tsuite.DataClient, err = data.Open(fmt.Sprintf(\"sqlite://%s/data.db\", suite.T().TempDir()), \"\")\n\tsuite.NoError(err)\n\tsuite.CacheClient, err = cache.Open(fmt.Sprintf(\"sqlite://%s/cache.db\", suite.T().TempDir()), \"\")\n\tsuite.NoError(err)\n\t// init database\n\terr = suite.DataClient.Init()\n\tsuite.NoError(err)\n\terr = suite.CacheClient.Init()\n\tsuite.NoError(err)\n}\n\nfunc (suite *WorkerTestSuite) TearDownSuite() {\n\terr := suite.DataClient.Close()\n\tsuite.NoError(err)\n\terr = suite.CacheClient.Close()\n\tsuite.NoError(err)\n}\n\nfunc (suite *WorkerTestSuite) SetupTest() {\n\terr := suite.DataClient.Purge()\n\tsuite.NoError(err)\n\terr = suite.CacheClient.Purge()\n\tsuite.NoError(err)\n\t// configuration\n\tsuite.Config = config.GetDefaultConfig()\n\tsuite.Config.Recommend.Collaborative.Type = \"mf\"\n\tsuite.Config.Recommend.Fallback.Recommenders = []string{\"latest\"}\n\tsuite.Jobs = 1\n\tsuite.dontskipColdStartUsers = true\n\t// reset random generator\n\tsuite.randGenerator = rand.New(rand.NewSource(0))\n\t// reset index\n\tsuite.MatrixFactorizationItems = nil\n\tsuite.ClickThroughRateModel = nil\n}\n\nfunc (suite *WorkerTestSuite) TestPullUsers() {\n\tctx := suite.T().Context()\n\t// create user index\n\terr := suite.DataClient.BatchInsertUsers(ctx, []data.User{\n\t\t{UserId: \"1\"},\n\t\t{UserId: \"2\"},\n\t\t{UserId: \"3\"},\n\t\t{UserId: \"4\"},\n\t\t{UserId: \"5\"},\n\t\t{UserId: \"6\"},\n\t\t{UserId: \"7\"},\n\t\t{UserId: \"8\"},\n\t})\n\tsuite.NoError(err)\n\t// create nodes\n\tnodes := []string{\"a\", \"b\", \"c\"}\n\n\tusers, err := suite.pullUsers(nodes, \"b\")\n\tsuite.NoError(err)\n\tsuite.Equal([]data.User{{UserId: \"1\"}, {UserId: \"3\"}, {UserId: \"6\"}}, users)\n\n\t_, err = suite.pullUsers(nodes, \"d\")\n\tsuite.Error(err)\n}\n\nfunc (suite *WorkerTestSuite) TestCheckRecommendCacheTimeout() {\n\tctx := suite.T().Context()\n\n\t// empty cache\n\tsuite.True(suite.checkRecommendCacheOutOfDate(ctx, \"0\"))\n\terr := suite.CacheClient.AddScores(ctx, cache.Recommend, \"0\", []cache.Score{{Id: \"0\", Score: 0, Categories: []string{\"\"}}})\n\tsuite.NoError(err)\n\n\t// digest mismatch\n\tsuite.True(suite.checkRecommendCacheOutOfDate(ctx, \"0\"))\n\terr = suite.CacheClient.Set(ctx, cache.String(cache.Key(cache.RecommendDigest, \"0\"), suite.Config.Recommend.Hash()))\n\tsuite.NoError(err)\n\n\terr = suite.CacheClient.Set(ctx, cache.Time(cache.Key(cache.LastModifyUserTime, \"0\"), time.Now().Add(-time.Hour)))\n\tsuite.NoError(err)\n\tsuite.True(suite.checkRecommendCacheOutOfDate(ctx, \"0\"))\n\terr = suite.CacheClient.Set(ctx, cache.Time(cache.Key(cache.RecommendUpdateTime, \"0\"), time.Now().Add(-time.Hour*100)))\n\tsuite.NoError(err)\n\tsuite.True(suite.checkRecommendCacheOutOfDate(ctx, \"0\"))\n\terr = suite.CacheClient.Set(ctx, cache.Time(cache.Key(cache.RecommendUpdateTime, \"0\"), time.Now().Add(time.Hour*100)))\n\tsuite.NoError(err)\n\tsuite.False(suite.checkRecommendCacheOutOfDate(ctx, \"0\"))\n\terr = suite.CacheClient.DeleteScores(ctx, []string{cache.Recommend}, cache.ScoreCondition{Subset: new(\"0\")})\n\tsuite.NoError(err)\n\tsuite.True(suite.checkRecommendCacheOutOfDate(ctx, \"0\"))\n}\n\nfunc (suite *WorkerTestSuite) TestRecommendCollaborative() {\n\tctx := suite.T().Context()\n\tsuite.Config.Recommend.Ranker.Recommenders = []string{\"collaborative\"}\n\t// insert feedbacks\n\tnow := time.Now()\n\terr := suite.DataClient.BatchInsertFeedback(ctx, []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"click\", UserId: \"0\", ItemId: \"9\"}, Timestamp: now.Add(-time.Hour)},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"click\", UserId: \"0\", ItemId: \"8\"}, Timestamp: now.Add(-time.Hour)},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"click\", UserId: \"0\", ItemId: \"7\"}, Timestamp: now.Add(-time.Hour)},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"click\", UserId: \"0\", ItemId: \"6\"}, Timestamp: now.Add(-time.Hour)},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"click\", UserId: \"0\", ItemId: \"5\"}, Timestamp: now.Add(-time.Hour)},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"click\", UserId: \"0\", ItemId: \"4\"}, Timestamp: now.Add(-time.Hour)},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"click\", UserId: \"0\", ItemId: \"3\"}, Timestamp: now.Add(time.Hour)},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"click\", UserId: \"0\", ItemId: \"2\"}, Timestamp: now.Add(time.Hour)},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"click\", UserId: \"0\", ItemId: \"1\"}, Timestamp: now.Add(time.Hour)},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"click\", UserId: \"0\", ItemId: \"0\"}, Timestamp: now.Add(time.Hour)},\n\t}, true, true, true)\n\tsuite.NoError(err)\n\n\t// insert hidden items and categorized items\n\terr = suite.DataClient.BatchInsertItems(ctx, []data.Item{\n\t\t{ItemId: \"10\", IsHidden: true},\n\t\t{ItemId: \"11\", IsHidden: true},\n\t\t{ItemId: \"3\", Categories: []string{\"*\"}},\n\t\t{ItemId: \"1\", Categories: []string{\"*\"}},\n\t})\n\tsuite.NoError(err)\n\n\t// create mock model\n\tsuite.MatrixFactorizationItems = logics.NewMatrixFactorizationItems(time.Time{})\n\tfor i := 0; i < 10; i++ {\n\t\tsuite.MatrixFactorizationItems.Add(strconv.Itoa(i), []float32{float32(i)})\n\t}\n\tsuite.MatrixFactorizationUsers = logics.NewMatrixFactorizationUsers()\n\tsuite.MatrixFactorizationUsers.Add(\"0\", []float32{1})\n\tsuite.Recommend(ctx, []data.User{{UserId: \"0\"}}, nil)\n\n\t// read recommend time\n\trecommendTime, err := suite.CacheClient.Get(ctx, cache.Key(cache.RecommendUpdateTime, \"0\")).Time()\n\tsuite.NoError(err)\n\n\trecommends, err := suite.CacheClient.SearchScores(ctx, cache.Recommend, \"0\", nil, 0, -1)\n\tsuite.NoError(err)\n\tsuite.Equal([]cache.Score{\n\t\t{Id: \"3\", Score: 3, Categories: []string{\"*\"}, Timestamp: recommendTime},\n\t\t{Id: \"2\", Score: 2, Timestamp: recommendTime},\n\t\t{Id: \"1\", Score: 1, Categories: []string{\"*\"}, Timestamp: recommendTime},\n\t\t{Id: \"0\", Score: 0, Timestamp: recommendTime},\n\t}, recommends)\n}\n\nfunc (suite *WorkerTestSuite) TestRecommendItemToItem() {\n\tctx := suite.T().Context()\n\tsuite.Config.Recommend.Ranker.Recommenders = []string{\"item-to-item/default\"}\n\tsuite.Config.Recommend.ItemToItem = []config.ItemToItemConfig{{Name: \"default\"}}\n\tsuite.Config.Recommend.DataSource.PositiveFeedbackTypes = []expression.FeedbackTypeExpression{expression.MustParseFeedbackTypeExpression(\"a\")}\n\t// insert feedback\n\terr := suite.DataClient.BatchInsertFeedback(ctx, []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"21\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"22\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"23\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"24\"}},\n\t}, true, true, true)\n\tsuite.NoError(err)\n\n\t// insert similar items\n\terr = suite.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"21\"), []cache.Score{\n\t\t{Id: \"22\", Score: 100000, Categories: []string{\"*\"}},\n\t\t{Id: \"25\", Score: 1000000},\n\t\t{Id: \"29\", Score: 1},\n\t})\n\tsuite.NoError(err)\n\terr = suite.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"22\"), []cache.Score{\n\t\t{Id: \"23\", Score: 100000, Categories: []string{\"*\"}},\n\t\t{Id: \"25\", Score: 1000000},\n\t\t{Id: \"28\", Score: 1, Categories: []string{\"*\"}},\n\t\t{Id: \"29\", Score: 1},\n\t})\n\tsuite.NoError(err)\n\terr = suite.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"23\"), []cache.Score{\n\t\t{Id: \"24\", Score: 100000, Categories: []string{\"*\"}},\n\t\t{Id: \"25\", Score: 1000000},\n\t\t{Id: \"27\", Score: 1},\n\t\t{Id: \"28\", Score: 1, Categories: []string{\"*\"}},\n\t\t{Id: \"29\", Score: 1},\n\t})\n\tsuite.NoError(err)\n\terr = suite.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"24\"), []cache.Score{\n\t\t{Id: \"21\", Score: 100000},\n\t\t{Id: \"25\", Score: 1000000},\n\t\t{Id: \"26\", Score: 1, Categories: []string{\"*\"}},\n\t\t{Id: \"27\", Score: 1},\n\t\t{Id: \"28\", Score: 1, Categories: []string{\"*\"}},\n\t\t{Id: \"29\", Score: 1},\n\t})\n\tsuite.NoError(err)\n\n\t// insert items\n\terr = suite.DataClient.BatchInsertItems(ctx, []data.Item{{ItemId: \"21\"}, {ItemId: \"22\"}, {ItemId: \"23\"}, {ItemId: \"24\"},\n\t\t{ItemId: \"25\"}, {ItemId: \"26\"}, {ItemId: \"27\"}, {ItemId: \"28\"}, {ItemId: \"29\"}})\n\tsuite.NoError(err)\n\t// insert hidden items\n\terr = suite.DataClient.BatchInsertItems(ctx, []data.Item{{ItemId: \"25\", IsHidden: true}})\n\tsuite.NoError(err)\n\t// insert categorized items\n\terr = suite.DataClient.BatchInsertItems(ctx, []data.Item{{ItemId: \"26\", Categories: []string{\"*\"}}, {ItemId: \"28\", Categories: []string{\"*\"}}})\n\tsuite.NoError(err)\n\tsuite.Recommend(ctx, []data.User{{UserId: \"0\"}}, nil)\n\t// read recommend time\n\trecommendTime, err := suite.CacheClient.Get(ctx, cache.Key(cache.RecommendUpdateTime, \"0\")).Time()\n\tsuite.NoError(err)\n\t// read recommend result\n\trecommends, err := suite.CacheClient.SearchScores(ctx, cache.Recommend, \"0\", nil, 0, 3)\n\tsuite.NoError(err)\n\tsuite.Equal([]cache.Score{\n\t\t{Id: \"29\", Score: 4, Timestamp: recommendTime},\n\t\t{Id: \"28\", Score: 3, Categories: []string{\"*\"}, Timestamp: recommendTime},\n\t\t{Id: \"27\", Score: 2, Timestamp: recommendTime},\n\t}, recommends)\n\trecommends, err = suite.CacheClient.SearchScores(ctx, cache.Recommend, \"0\", []string{\"*\"}, 0, 3)\n\tsuite.NoError(err)\n\tsuite.Equal([]cache.Score{\n\t\t{Id: \"28\", Score: 3, Categories: []string{\"*\"}, Timestamp: recommendTime},\n\t\t{Id: \"26\", Score: 1, Categories: []string{\"*\"}, Timestamp: recommendTime},\n\t}, recommends)\n}\n\nfunc (suite *WorkerTestSuite) TestRecommendUserToUser() {\n\tctx := suite.T().Context()\n\tsuite.Config.Recommend.Ranker.Recommenders = []string{\"user-to-user/default\"}\n\tsuite.Config.Recommend.UserToUser = []config.UserToUserConfig{{Name: \"default\"}}\n\tsuite.Config.Recommend.DataSource.PositiveFeedbackTypes = []expression.FeedbackTypeExpression{expression.MustParseFeedbackTypeExpression(\"a\")}\n\t// insert similar users\n\terr := suite.CacheClient.AddScores(ctx, cache.UserToUser, cache.Key(\"default\", \"0\"), []cache.Score{\n\t\t{Id: \"1\", Score: 2},\n\t\t{Id: \"2\", Score: 1.5},\n\t\t{Id: \"3\", Score: 1},\n\t})\n\tsuite.NoError(err)\n\t// insert feedback\n\terr = suite.DataClient.BatchInsertFeedback(ctx, []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"1\", ItemId: \"10\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"1\", ItemId: \"11\"}},\n\t}, true, true, true)\n\tsuite.NoError(err)\n\terr = suite.DataClient.BatchInsertFeedback(ctx, []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"2\", ItemId: \"10\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"2\", ItemId: \"12\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"2\", ItemId: \"48\"}},\n\t}, true, true, true)\n\tsuite.NoError(err)\n\terr = suite.DataClient.BatchInsertFeedback(ctx, []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"3\", ItemId: \"10\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"3\", ItemId: \"13\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"3\", ItemId: \"48\"}},\n\t}, true, true, true)\n\tsuite.NoError(err)\n\t// insert hidden items\n\terr = suite.DataClient.BatchInsertItems(ctx, []data.Item{{ItemId: \"10\", IsHidden: true}})\n\tsuite.NoError(err)\n\t// insert categorized items\n\terr = suite.DataClient.BatchInsertItems(ctx, []data.Item{\n\t\t{ItemId: \"12\", Categories: []string{\"*\"}},\n\t\t{ItemId: \"48\", Categories: []string{\"*\"}},\n\t})\n\tsuite.NoError(err)\n\tsuite.Recommend(ctx, []data.User{{UserId: \"0\"}}, nil)\n\t// read recommend time\n\trecommendTime, err := suite.CacheClient.Get(ctx, cache.Key(cache.RecommendUpdateTime, \"0\")).Time()\n\tsuite.NoError(err)\n\t// read recommend result\n\trecommends, err := suite.CacheClient.SearchScores(ctx, cache.Recommend, \"0\", nil, 0, 3)\n\tsuite.NoError(err)\n\tsuite.Equal([]cache.Score{\n\t\t{Id: \"48\", Score: 2.5, Categories: []string{\"*\"}, Timestamp: recommendTime},\n\t\t{Id: \"11\", Score: 2, Timestamp: recommendTime},\n\t\t{Id: \"12\", Score: 1.5, Categories: []string{\"*\"}, Timestamp: recommendTime},\n\t}, recommends)\n\trecommends, err = suite.CacheClient.SearchScores(ctx, cache.Recommend, \"0\", []string{\"*\"}, 0, 3)\n\tsuite.NoError(err)\n\tsuite.Equal([]cache.Score{\n\t\t{Id: \"48\", Score: 2.5, Categories: []string{\"*\"}, Timestamp: recommendTime},\n\t\t{Id: \"12\", Score: 1.5, Categories: []string{\"*\"}, Timestamp: recommendTime},\n\t}, recommends)\n}\n\nfunc (suite *WorkerTestSuite) TestRecommendLatest() {\n\t// create mock worker\n\tctx := suite.T().Context()\n\tsuite.Config.Recommend.Ranker.Recommenders = []string{\"latest\"}\n\t// insert items\n\terr := suite.DataClient.BatchInsertItems(ctx, []data.Item{\n\t\t{ItemId: \"21\", Timestamp: time.Unix(21, 0)},\n\t\t{ItemId: \"20\", Timestamp: time.Unix(20, 0)},\n\t\t{ItemId: \"19\", Timestamp: time.Unix(19, 0)},\n\t\t{ItemId: \"18\", Timestamp: time.Unix(18, 0)},\n\t\t{ItemId: \"10\", Categories: []string{\"*\"}, Timestamp: time.Unix(10, 0)},\n\t\t{ItemId: \"9\", Categories: []string{\"*\"}, Timestamp: time.Unix(9, 0)},\n\t\t{ItemId: \"8\", Categories: []string{\"*\"}, Timestamp: time.Unix(8, 0)},\n\t})\n\tsuite.NoError(err)\n\t// insert hidden items\n\terr = suite.DataClient.BatchInsertItems(ctx, []data.Item{{ItemId: \"21\", IsHidden: true}})\n\tsuite.NoError(err)\n\tsuite.Recommend(ctx, []data.User{{UserId: \"0\"}}, nil)\n\t// read recommend time\n\trecommendTime, err := suite.CacheClient.Get(ctx, cache.Key(cache.RecommendUpdateTime, \"0\")).Time()\n\tsuite.NoError(err)\n\t// read recommend result\n\trecommends, err := suite.CacheClient.SearchScores(ctx, cache.Recommend, \"0\", nil, 0, 3)\n\tsuite.NoError(err)\n\tsuite.Equal([]cache.Score{\n\t\t{Id: \"20\", Score: 20, Timestamp: recommendTime},\n\t\t{Id: \"19\", Score: 19, Timestamp: recommendTime},\n\t\t{Id: \"18\", Score: 18, Timestamp: recommendTime},\n\t}, recommends)\n\trecommends, err = suite.CacheClient.SearchScores(ctx, cache.Recommend, \"0\", []string{\"*\"}, 0, -1)\n\tsuite.NoError(err)\n\tsuite.Equal([]cache.Score{\n\t\t{Id: \"10\", Score: 10, Categories: []string{\"*\"}, Timestamp: recommendTime},\n\t\t{Id: \"9\", Score: 9, Categories: []string{\"*\"}, Timestamp: recommendTime},\n\t\t{Id: \"8\", Score: 8, Categories: []string{\"*\"}, Timestamp: recommendTime},\n\t}, recommends)\n\t// read recommend digest\n\tdigest, err := suite.CacheClient.Get(ctx, cache.Key(cache.RecommendDigest, \"0\")).String()\n\tsuite.NoError(err)\n\tsuite.Equal(util.MD5(\"latest\"), digest)\n}\n\nfunc (suite *WorkerTestSuite) TestRecommendNonPersonalized() {\n\t// create mock worker\n\tctx := suite.T().Context()\n\tsuite.Config.Recommend.Ranker.Recommenders = []string{\"non-personalized/popular\"}\n\t// insert items\n\terr := suite.DataClient.BatchInsertItems(ctx, []data.Item{\n\t\t{ItemId: \"11\"},\n\t\t{ItemId: \"10\"},\n\t\t{ItemId: \"9\"},\n\t\t{ItemId: \"8\"},\n\t\t{ItemId: \"20\", Categories: []string{\"*\"}},\n\t\t{ItemId: \"19\", Categories: []string{\"*\"}},\n\t\t{ItemId: \"18\", Categories: []string{\"*\"}},\n\t})\n\tsuite.NoError(err)\n\t// insert non-personalized recommendation\n\terr = suite.CacheClient.AddScores(ctx, cache.NonPersonalized, \"popular\", []cache.Score{\n\t\t{Id: \"11\", Score: 31, Categories: []string{\"\"}},\n\t\t{Id: \"10\", Score: 30, Categories: []string{\"\"}},\n\t\t{Id: \"9\", Score: 29, Categories: []string{\"\"}},\n\t\t{Id: \"8\", Score: 28, Categories: []string{\"\"}},\n\t\t{Id: \"20\", Score: 20, Categories: []string{\"\", \"*\"}},\n\t\t{Id: \"19\", Score: 19, Categories: []string{\"\", \"*\"}},\n\t\t{Id: \"18\", Score: 18, Categories: []string{\"\", \"*\"}},\n\t})\n\tsuite.NoError(err)\n\t// insert hidden items\n\terr = suite.DataClient.BatchInsertItems(ctx, []data.Item{{ItemId: \"11\", IsHidden: true}})\n\tsuite.NoError(err)\n\tsuite.Recommend(ctx, []data.User{{UserId: \"0\"}}, nil)\n\t// read recommend time\n\trecommendTime, err := suite.CacheClient.Get(ctx, cache.Key(cache.RecommendUpdateTime, \"0\")).Time()\n\tsuite.NoError(err)\n\t// read recommend result\n\trecommends, err := suite.CacheClient.SearchScores(ctx, cache.Recommend, \"0\", nil, 0, 3)\n\tsuite.NoError(err)\n\tsuite.Equal([]cache.Score{\n\t\t{Id: \"10\", Score: 30, Categories: []string{\"\"}, Timestamp: recommendTime},\n\t\t{Id: \"9\", Score: 29, Categories: []string{\"\"}, Timestamp: recommendTime},\n\t\t{Id: \"8\", Score: 28, Categories: []string{\"\"}, Timestamp: recommendTime},\n\t}, recommends)\n\trecommends, err = suite.CacheClient.SearchScores(ctx, cache.Recommend, \"0\", []string{\"*\"}, 0, -1)\n\tsuite.NoError(err)\n\tsuite.Equal([]cache.Score{\n\t\t{Id: \"20\", Score: 20, Categories: []string{\"\", \"*\"}, Timestamp: recommendTime},\n\t\t{Id: \"19\", Score: 19, Categories: []string{\"\", \"*\"}, Timestamp: recommendTime},\n\t\t{Id: \"18\", Score: 18, Categories: []string{\"\", \"*\"}, Timestamp: recommendTime},\n\t}, recommends)\n}\n\nfunc (suite *WorkerTestSuite) TestRecommend() {\n\tctx := suite.T().Context()\n\tsuite.Config.Recommend.Ranker.Type = \"fm\"\n\tsuite.Config.Recommend.Ranker.Recommenders = nil\n\tsuite.Config.Recommend.DataSource.PositiveFeedbackTypes = []expression.FeedbackTypeExpression{expression.MustParseFeedbackTypeExpression(\"a\")}\n\tsuite.Config.Recommend.CacheSize = 1\n\tsuite.Config.Recommend.NonPersonalized = []config.NonPersonalizedConfig{{Name: \"popular\"}}\n\tsuite.Config.Recommend.ItemToItem = []config.ItemToItemConfig{{Name: \"default\"}}\n\tsuite.Config.Recommend.UserToUser = []config.UserToUserConfig{{Name: \"default\"}}\n\tsuite.MatrixFactorizationItems = logics.NewMatrixFactorizationItems(time.Time{})\n\tsuite.MatrixFactorizationItems.Add(\"4\", []float32{4})\n\tsuite.MatrixFactorizationUsers = logics.NewMatrixFactorizationUsers()\n\tsuite.MatrixFactorizationUsers.Add(\"0\", []float32{1})\n\tsuite.ClickThroughRateModel = new(mockFactorizationMachine)\n\n\t// insert items\n\terr := suite.DataClient.BatchInsertItems(ctx, []data.Item{\n\t\t{ItemId: \"0\", Timestamp: time.Unix(0, 0)},\n\t\t{ItemId: \"1\", Timestamp: time.Unix(1, 0)},\n\t\t{ItemId: \"2\", Timestamp: time.Unix(2, 0)},\n\t\t{ItemId: \"3\", Timestamp: time.Unix(3, 0)},\n\t\t{ItemId: \"4\", Timestamp: time.Unix(4, 0)},\n\t\t{ItemId: \"5\", Categories: []string{\"a\"}, Timestamp: time.Unix(5, 0)},\n\t})\n\tsuite.NoError(err)\n\n\t// insert feedback\n\terr = suite.DataClient.BatchInsertFeedback(ctx, []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"0\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"1\", ItemId: \"1\"}},\n\t}, true, true, true)\n\tsuite.NoError(err)\n\n\t// insert stale recommendation\n\terr = suite.CacheClient.AddScores(ctx, cache.Recommend, \"0\", []cache.Score{{Id: \"999\", Score: 999}})\n\tsuite.NoError(err)\n\n\t// insert non-personalized recommendation\n\terr = suite.CacheClient.AddScores(ctx, cache.NonPersonalized, \"popular\", []cache.Score{{Id: \"3\", Categories: []string{\"\"}}})\n\tsuite.NoError(err)\n\n\t// insert item-to-item recommendation\n\terr = suite.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"0\"), []cache.Score{{Id: \"2\"}})\n\tsuite.NoError(err)\n\n\t// insert user-to-user recommendation\n\terr = suite.CacheClient.AddScores(ctx, cache.UserToUser, cache.Key(\"default\", \"0\"), []cache.Score{{Id: \"1\"}})\n\tsuite.NoError(err)\n\n\tsuite.Recommend(ctx, []data.User{{UserId: \"0\"}}, nil)\n\t// read recommend time\n\trecommendTime, err := suite.CacheClient.Get(ctx, cache.Key(cache.RecommendUpdateTime, \"0\")).Time()\n\tsuite.NoError(err)\n\t// read recommend result\n\trecommends, err := suite.CacheClient.SearchScores(ctx, cache.Recommend, \"0\", nil, 0, 5)\n\tsuite.NoError(err)\n\tsuite.Equal([]cache.Score{\n\t\t{Id: \"5\", Score: 5, Timestamp: recommendTime, Categories: []string{\"a\"}},\n\t\t{Id: \"4\", Score: 4, Timestamp: recommendTime},\n\t\t{Id: \"3\", Score: 3, Timestamp: recommendTime},\n\t\t{Id: \"2\", Score: 2, Timestamp: recommendTime},\n\t\t{Id: \"1\", Score: 1, Timestamp: recommendTime},\n\t}, recommends)\n}\n\nfunc (suite *WorkerTestSuite) TestRecommendRankerNone() {\n\tctx := suite.T().Context()\n\tsuite.Config.Recommend.Ranker.Type = \"none\"\n\tsuite.Config.Recommend.DataSource.PositiveFeedbackTypes = []expression.FeedbackTypeExpression{expression.MustParseFeedbackTypeExpression(\"a\")}\n\tsuite.Config.Recommend.CacheSize = 1\n\tsuite.Config.Recommend.NonPersonalized = []config.NonPersonalizedConfig{{Name: \"popular\"}}\n\tsuite.Config.Recommend.ItemToItem = []config.ItemToItemConfig{{Name: \"default\"}}\n\tsuite.Config.Recommend.UserToUser = []config.UserToUserConfig{{Name: \"default\"}}\n\tsuite.MatrixFactorizationItems = logics.NewMatrixFactorizationItems(time.Time{})\n\tsuite.MatrixFactorizationItems.Add(\"4\", []float32{4})\n\tsuite.MatrixFactorizationUsers = logics.NewMatrixFactorizationUsers()\n\tsuite.MatrixFactorizationUsers.Add(\"0\", []float32{1})\n\tsuite.ClickThroughRateModel = new(mockFactorizationMachine)\n\n\t// insert items\n\terr := suite.DataClient.BatchInsertItems(ctx, []data.Item{\n\t\t{ItemId: \"0\", Timestamp: time.Unix(0, 0)},\n\t\t{ItemId: \"1\", Timestamp: time.Unix(1, 0)},\n\t\t{ItemId: \"2\", Timestamp: time.Unix(2, 0)},\n\t\t{ItemId: \"3\", Timestamp: time.Unix(3, 0)},\n\t\t{ItemId: \"4\", Timestamp: time.Unix(4, 0)},\n\t\t{ItemId: \"5\", Categories: []string{\"a\"}, Timestamp: time.Unix(5, 0)},\n\t})\n\tsuite.NoError(err)\n\n\t// insert feedback\n\terr = suite.DataClient.BatchInsertFeedback(ctx, []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"0\", ItemId: \"0\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"a\", UserId: \"1\", ItemId: \"1\"}},\n\t}, true, true, true)\n\tsuite.NoError(err)\n\n\t// insert non-personalized recommendation\n\terr = suite.CacheClient.AddScores(ctx, cache.NonPersonalized, \"popular\", []cache.Score{{Id: \"3\", Categories: []string{\"\"}}})\n\tsuite.NoError(err)\n\n\t// insert item-to-item recommendation\n\terr = suite.CacheClient.AddScores(ctx, cache.ItemToItem, cache.Key(\"default\", \"0\"), []cache.Score{{Id: \"2\"}})\n\tsuite.NoError(err)\n\n\t// insert user-to-user recommendation\n\terr = suite.CacheClient.AddScores(ctx, cache.UserToUser, cache.Key(\"default\", \"0\"), []cache.Score{{Id: \"1\"}})\n\tsuite.NoError(err)\n\n\tsuite.Recommend(ctx, []data.User{{UserId: \"0\"}}, nil)\n\t// read recommend time\n\trecommendTime, err := suite.CacheClient.Get(ctx, cache.Key(cache.RecommendUpdateTime, \"0\")).Time()\n\tsuite.NoError(err)\n\t// read recommend result\n\trecommends, err := suite.CacheClient.SearchScores(ctx, cache.Recommend, \"0\", nil, 0, 5)\n\tsuite.NoError(err)\n\tsuite.NotEqual([]cache.Score{\n\t\t{Id: \"5\", Score: 5, Timestamp: recommendTime, Categories: []string{\"a\"}},\n\t\t{Id: \"4\", Score: 4, Timestamp: recommendTime},\n\t\t{Id: \"3\", Score: 3, Timestamp: recommendTime},\n\t\t{Id: \"2\", Score: 2, Timestamp: recommendTime},\n\t\t{Id: \"1\", Score: 1, Timestamp: recommendTime},\n\t}, recommends)\n}\n\nfunc marshal(t *testing.T, v interface{}) string {\n\ts, err := json.Marshal(v)\n\tassert.NoError(t, err)\n\treturn string(s)\n}\n\nfunc newRankingDataset() (*dataset.Dataset, *dataset.Dataset) {\n\treturn dataset.NewDataset(time.Now(), 0, 0), dataset.NewDataset(time.Now(), 0, 0)\n}\n\nfunc newClickDataset() (*ctr.Dataset, *ctr.Dataset) {\n\tdataSet := &ctr.Dataset{\n\t\tIndex: dataset.NewUnifiedMapIndexBuilder().Build(),\n\t}\n\treturn dataSet, dataSet\n}\n\ntype mockMaster struct {\n\tprotocol.UnimplementedMasterServer\n\taddr          chan string\n\tgrpcServer    *grpc.Server\n\tcacheFilePath string\n\tdataFilePath  string\n\tmeta          *protocol.Meta\n\trankingModel  []byte\n\tclickModel    []byte\n\tuserIndex     []byte\n}\n\nfunc newMockMaster(t *testing.T) *mockMaster {\n\tcfg := config.GetDefaultConfig()\n\tcfg.Database.DataStore = fmt.Sprintf(\"sqlite://%s/data.db\", t.TempDir())\n\tcfg.Database.CacheStore = fmt.Sprintf(\"sqlite://%s/cache.db\", t.TempDir())\n\n\t// create click model\n\ttrain, test := newClickDataset()\n\tfm := ctr.NewAFM(model.Params{model.NEpochs: 0})\n\tfm.Fit(t.Context(), train, test, &ctr.FitConfig{})\n\tclickModelBuffer := bytes.NewBuffer(nil)\n\terr := ctr.MarshalModel(clickModelBuffer, fm)\n\tassert.NoError(t, err)\n\n\t// create ranking model\n\ttrainSet, testSet := newRankingDataset()\n\tbpr := cf.NewBPR(model.Params{model.NEpochs: 0})\n\tbpr.Fit(t.Context(), trainSet, testSet, cf.NewFitConfig())\n\trankingModelBuffer := bytes.NewBuffer(nil)\n\terr = cf.MarshalModel(rankingModelBuffer, bpr)\n\tassert.NoError(t, err)\n\n\t// create user index\n\tuserIndexBuffer := bytes.NewBuffer(nil)\n\terr = dataset.MarshalIndex(userIndexBuffer, dataset.NewMapIndex())\n\tassert.NoError(t, err)\n\n\treturn &mockMaster{\n\t\taddr: make(chan string),\n\t\tmeta: &protocol.Meta{\n\t\t\tConfig:                        marshal(t, cfg),\n\t\t\tClickThroughRateModelId:       1,\n\t\t\tCollaborativeFilteringModelId: 2,\n\t\t},\n\t\tcacheFilePath: cfg.Database.CacheStore,\n\t\tdataFilePath:  cfg.Database.DataStore,\n\t\tuserIndex:     userIndexBuffer.Bytes(),\n\t\tclickModel:    clickModelBuffer.Bytes(),\n\t\trankingModel:  rankingModelBuffer.Bytes(),\n\t}\n}\n\nfunc (m *mockMaster) GetMeta(_ context.Context, _ *protocol.NodeInfo) (*protocol.Meta, error) {\n\treturn m.meta, nil\n}\n\nfunc (m *mockMaster) Start(t *testing.T) {\n\tlisten, err := net.Listen(\"tcp\", \"localhost:0\")\n\tassert.NoError(t, err)\n\tm.addr <- listen.Addr().String()\n\tvar opts []grpc.ServerOption\n\tm.grpcServer = grpc.NewServer(opts...)\n\tprotocol.RegisterMasterServer(m.grpcServer, m)\n\terr = m.grpcServer.Serve(listen)\n\tassert.NoError(t, err)\n}\n\nfunc (m *mockMaster) Stop() {\n\tm.grpcServer.Stop()\n}\n\nfunc TestWorker_Sync(t *testing.T) {\n\tmaster := newMockMaster(t)\n\tgo master.Start(t)\n\taddress := <-master.addr\n\tconn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials()))\n\tassert.NoError(t, err)\n\tserv := &Worker{\n\t\tPipeline: Pipeline{\n\t\t\tConfig:      config.GetDefaultConfig(),\n\t\t\tCacheClient: new(cache.NoDatabase),\n\t\t\tDataClient:  new(data.NoDatabase),\n\t\t},\n\t\ttestMode:     true,\n\t\tmasterClient: protocol.NewMasterClient(conn),\n\t\tsyncedChan:   make(chan struct{}, 1),\n\t\tticker:       time.NewTicker(time.Minute),\n\t}\n\n\tserv.Sync()\n\tassert.Equal(t, master.dataFilePath, serv.dataPath)\n\tassert.Equal(t, master.cacheFilePath, serv.cachePath)\n\tassert.NoError(t, serv.DataClient.Close())\n\tassert.NoError(t, serv.CacheClient.Close())\n\tassert.Equal(t, int64(1), serv.latestClickThroughRateModelId)\n\tassert.Equal(t, int64(2), serv.latestCollaborativeFilteringModelId)\n\tassert.Zero(t, serv.clickThroughRateModelId)\n\tassert.Zero(t, serv.collaborativeFilteringModelId)\n\tmaster.Stop()\n}\n\ntype mockFactorizationMachine struct {\n\tctr.BaseFactorizationMachines\n}\n\nfunc (m mockFactorizationMachine) Complexity() int {\n\tpanic(\"implement me\")\n}\n\nfunc (m mockFactorizationMachine) SuggestParams(_ goptuna.Trial) model.Params {\n\tpanic(\"implement me\")\n}\n\nfunc (m mockFactorizationMachine) Clear() {\n\tpanic(\"implement me\")\n}\n\nfunc (m mockFactorizationMachine) Invalid() bool {\n\treturn false\n}\n\nfunc (m mockFactorizationMachine) Predict(_, itemId string, _, _ []ctr.Label) float32 {\n\tscore, err := strconv.Atoi(itemId)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn float32(score)\n}\n\nfunc (m mockFactorizationMachine) InternalPredict(_ []int32, _ []float32) float32 {\n\tpanic(\"implement me\")\n}\n\nfunc (m mockFactorizationMachine) Fit(_ context.Context, _, _ dataset.CTRSplit, _ *ctr.FitConfig) ctr.Score {\n\tpanic(\"implement me\")\n}\n\nfunc (m mockFactorizationMachine) Marshal(_ io.Writer) error {\n\tpanic(\"implement me\")\n}\n\nfunc (suite *WorkerTestSuite) TestRankByClickTroughRate() {\n\tctx := suite.T().Context()\n\t// insert a user\n\terr := suite.DataClient.BatchInsertUsers(ctx, []data.User{{UserId: \"1\"}})\n\tsuite.NoError(err)\n\t// insert items\n\terr = suite.DataClient.BatchInsertItems(ctx, []data.Item{\n\t\t{ItemId: \"1\"},\n\t\t{ItemId: \"2\"},\n\t\t{ItemId: \"3\"},\n\t\t{ItemId: \"4\"},\n\t\t{ItemId: \"5\"},\n\t})\n\tsuite.NoError(err)\n\t// rank items\n\titemCache := NewItemCache(suite.DataClient)\n\tresult, err := suite.rankByClickTroughRate(ctx, new(mockFactorizationMachine), &data.User{UserId: \"1\"},\n\t\t[]cache.Score{{Id: \"1\"}, {Id: \"2\"}, {Id: \"3\"}, {Id: \"4\"}, {Id: \"5\"}}, itemCache, time.Now())\n\tsuite.NoError(err)\n\tsuite.Equal([]string{\"5\", \"4\", \"3\", \"2\", \"1\"}, lo.Map(result, func(d cache.Score, _ int) string {\n\t\treturn d.Id\n\t}))\n\tsuite.IsDecreasing(lo.Map(result, func(d cache.Score, _ int) float64 {\n\t\treturn d.Score\n\t}))\n}\n\nfunc (suite *WorkerTestSuite) TestRankByLLM() {\n\tctx := suite.T().Context()\n\tmockAI := reranker.NewMockServer()\n\tgo func() {\n\t\t_ = mockAI.Start()\n\t}()\n\tmockAI.Ready()\n\tdefer mockAI.Close()\n\n\t// insert a user\n\terr := suite.DataClient.BatchInsertUsers(ctx, []data.User{{UserId: \"u1\"}})\n\tsuite.NoError(err)\n\t// insert items used by candidates and feedback\n\terr = suite.DataClient.BatchInsertItems(ctx, []data.Item{{ItemId: \"1\"}, {ItemId: \"2\"}, {ItemId: \"3\"}, {ItemId: \"4\"}, {ItemId: \"5\"}})\n\tsuite.NoError(err)\n\n\tsuite.Config.Recommend.Ranker.RerankerAPI = config.RerankerAPIConfig{\n\t\tURL:       mockAI.URL(),\n\t\tAuthToken: mockAI.AuthToken(),\n\t\tModel:     \"v1\",\n\t}\n\tranker, err := logics.NewChatReranker(suite.Config.Recommend.Ranker.RerankerAPI,\n\t\t\"{{user.UserId}}\", \"{{item.ItemId}}\")\n\tsuite.NoError(err)\n\n\titemCache := NewItemCache(suite.DataClient)\n\trecommendTime := time.Now()\n\tresult, err := suite.rankByLLM(ctx, nil, ranker, &data.User{UserId: \"u1\"}, []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"like\", UserId: \"u1\", ItemId: \"4\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"like\", UserId: \"u1\", ItemId: \"5\"}},\n\t}, []cache.Score{{Id: \"1\"}, {Id: \"2\"}, {Id: \"3\"}}, itemCache, recommendTime)\n\tsuite.NoError(err)\n\tsuite.Equal([]string{\"1\", \"2\", \"3\"}, lo.Map(result, func(d cache.Score, _ int) string {\n\t\treturn d.Id\n\t}))\n\tsuite.Equal([]float64{1, 0.5, float64(1) / 3}, lo.Map(result, func(d cache.Score, _ int) float64 {\n\t\treturn d.Score\n\t}))\n\tfor _, scored := range result {\n\t\tsuite.Equal(recommendTime, scored.Timestamp)\n\t}\n}\n\nfunc (suite *WorkerTestSuite) TestReplacement() {\n\tctx := suite.T().Context()\n\tsuite.Config.Recommend.DataSource.PositiveFeedbackTypes = []expression.FeedbackTypeExpression{\n\t\texpression.MustParseFeedbackTypeExpression(\"p\")}\n\tsuite.Config.Recommend.DataSource.ReadFeedbackTypes = []expression.FeedbackTypeExpression{\n\t\texpression.MustParseFeedbackTypeExpression(\"n\")}\n\tsuite.Config.Recommend.Ranker.Type = \"fm\"\n\tsuite.Config.Recommend.Ranker.Recommenders = []string{\"collaborative\"}\n\tsuite.Config.Recommend.Replacement.EnableReplacement = true\n\tsuite.Config.Recommend.Replacement.PositiveReplacementDecay = 0.8\n\tsuite.Config.Recommend.Replacement.ReadReplacementDecay = 0.7\n\tsuite.ClickThroughRateModel = new(mockFactorizationMachine)\n\n\t// 1. Insert historical items into empty recommendation.\n\t// insert items\n\terr := suite.DataClient.BatchInsertItems(ctx, []data.Item{\n\t\t{ItemId: \"10\"}, {ItemId: \"9\"}, {ItemId: \"8\"}, {ItemId: \"7\"}, {ItemId: \"6\"}, {ItemId: \"5\"},\n\t})\n\tsuite.NoError(err)\n\t// insert feedback\n\terr = suite.DataClient.BatchInsertFeedback(ctx, []data.Feedback{\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"p\", UserId: \"0\", ItemId: \"10\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"n\", UserId: \"0\", ItemId: \"9\"}},\n\t\t{FeedbackKey: data.FeedbackKey{FeedbackType: \"i\", UserId: \"0\", ItemId: \"8\"}},\n\t}, true, false, true)\n\tsuite.NoError(err)\n\tsuite.Recommend(ctx, []data.User{{UserId: \"0\"}}, nil)\n\t// read recommend time\n\trecommendTime, err := suite.CacheClient.Get(ctx, cache.Key(cache.RecommendUpdateTime, \"0\")).Time()\n\tsuite.NoError(err)\n\t// read recommend result\n\trecommends, err := suite.CacheClient.SearchScores(ctx, cache.Recommend, \"0\", nil, 0, 3)\n\tsuite.NoError(err)\n\tsuite.Equal([]cache.Score{\n\t\t{Id: \"10\", Score: 8, Timestamp: recommendTime},\n\t\t{Id: \"9\", Score: 6.3, Timestamp: recommendTime},\n\t}, recommends)\n\n\t// 2. Insert historical items into non-empty recommendation.\n\tsuite.Config.Recommend.CacheExpire = 0\n\tsuite.Config.Recommend.Ranker.Recommenders = []string{\"latest\"}\n\tsuite.Recommend(ctx, []data.User{{UserId: \"0\"}}, nil)\n\t// read recommend time\n\trecommendTime, err = suite.CacheClient.Get(ctx, cache.Key(cache.RecommendUpdateTime, \"0\")).Time()\n\tsuite.NoError(err)\n\t// read recommend result\n\trecommends, err = suite.CacheClient.SearchScores(ctx, cache.Recommend, \"0\", nil, 0, 3)\n\tsuite.NoError(err)\n\tsuite.Equal([]cache.Score{\n\t\t{Id: \"10\", Score: 8, Timestamp: recommendTime},\n\t\t{Id: \"7\", Score: 7, Timestamp: recommendTime},\n\t\t{Id: \"9\", Score: 6.3, Timestamp: recommendTime},\n\t}, recommends)\n}\n\nfunc (suite *WorkerTestSuite) TestUserActivity() {\n\tctx := suite.T().Context()\n\terr := suite.CacheClient.Set(ctx, cache.Time(cache.Key(cache.LastModifyUserTime, \"0\"), time.Now().AddDate(0, 0, -1)))\n\tsuite.NoError(err)\n\terr = suite.CacheClient.Set(ctx, cache.Time(cache.Key(cache.LastModifyUserTime, \"1\"), time.Now().AddDate(0, 0, -10)))\n\tsuite.NoError(err)\n\terr = suite.CacheClient.AddScores(ctx, cache.Recommend, \"0\", []cache.Score{{Id: \"0\", Score: 1, Categories: []string{\"\"}}})\n\tsuite.NoError(err)\n\terr = suite.CacheClient.AddScores(ctx, cache.Recommend, \"1\", []cache.Score{{Id: \"1\", Score: 1, Categories: []string{\"\"}}})\n\tsuite.NoError(err)\n\terr = suite.CacheClient.AddScores(ctx, cache.Recommend, \"2\", []cache.Score{{Id: \"2\", Score: 1, Categories: []string{\"\"}}})\n\tsuite.NoError(err)\n\n\tsuite.True(suite.checkUserActiveTime(ctx, \"0\"))\n\tsuite.True(suite.checkUserActiveTime(ctx, \"1\"))\n\tsuite.True(suite.checkUserActiveTime(ctx, \"2\"))\n\tdocs, err := suite.CacheClient.SearchScores(ctx, cache.Recommend, \"0\", []string{\"\"}, 0, 1)\n\tsuite.NoError(err)\n\tsuite.NotEmpty(docs)\n\tdocs, err = suite.CacheClient.SearchScores(ctx, cache.Recommend, \"1\", []string{\"\"}, 0, 1)\n\tsuite.NoError(err)\n\tsuite.NotEmpty(docs)\n\tdocs, err = suite.CacheClient.SearchScores(ctx, cache.Recommend, \"2\", []string{\"\"}, 0, 1)\n\tsuite.NoError(err)\n\tsuite.NotEmpty(docs)\n\n\tsuite.Config.Recommend.ActiveUserTTL = 5\n\tsuite.True(suite.checkUserActiveTime(ctx, \"0\"))\n\tsuite.False(suite.checkUserActiveTime(ctx, \"1\"))\n\tsuite.True(suite.checkUserActiveTime(ctx, \"2\"))\n\tdocs, err = suite.CacheClient.SearchScores(ctx, cache.Recommend, \"0\", []string{\"\"}, 0, 1)\n\tsuite.NoError(err)\n\tsuite.NotEmpty(docs)\n\tdocs, err = suite.CacheClient.SearchScores(ctx, cache.Recommend, \"1\", []string{\"\"}, 0, 1)\n\tsuite.NoError(err)\n\tsuite.Empty(docs)\n\tdocs, err = suite.CacheClient.SearchScores(ctx, cache.Recommend, \"2\", []string{\"\"}, 0, 1)\n\tsuite.NoError(err)\n\tsuite.NotEmpty(docs)\n}\n\nfunc (suite *WorkerTestSuite) TestHealth() {\n\treq := httptest.NewRequest(\"GET\", \"https://example.com/\", nil)\n\tw := httptest.NewRecorder()\n\tsuite.checkLive(w, req)\n\tsuite.Equal(http.StatusOK, w.Code)\n\tsuite.Equal(marshal(suite.T(), HealthStatus{\n\t\tReady:               true,\n\t\tDataStoreError:      nil,\n\t\tCacheStoreError:     nil,\n\t\tDataStoreConnected:  true,\n\t\tCacheStoreConnected: true,\n\t}), w.Body.String())\n\n\tw = httptest.NewRecorder()\n\tsuite.checkReady(w, req)\n\tsuite.Equal(http.StatusOK, w.Code)\n\tsuite.Equal(marshal(suite.T(), HealthStatus{\n\t\tReady:               true,\n\t\tDataStoreError:      nil,\n\t\tCacheStoreError:     nil,\n\t\tDataStoreConnected:  true,\n\t\tCacheStoreConnected: true,\n\t}), w.Body.String())\n\n\tdataClient, cacheClient := suite.DataClient, suite.CacheClient\n\tsuite.DataClient, suite.CacheClient = data.NoDatabase{}, cache.NoDatabase{}\n\tw = httptest.NewRecorder()\n\tsuite.checkLive(w, req)\n\tsuite.Equal(http.StatusOK, w.Code)\n\tsuite.Equal(marshal(suite.T(), HealthStatus{\n\t\tReady:               false,\n\t\tDataStoreError:      data.ErrNoDatabase,\n\t\tCacheStoreError:     cache.ErrNoDatabase,\n\t\tDataStoreConnected:  false,\n\t\tCacheStoreConnected: false,\n\t}), w.Body.String())\n\n\tw = httptest.NewRecorder()\n\tsuite.checkReady(w, req)\n\tsuite.Equal(http.StatusServiceUnavailable, w.Code)\n\tsuite.DataClient, suite.CacheClient = dataClient, cacheClient\n}\n\nfunc TestWorker(t *testing.T) {\n\tsuite.Run(t, new(WorkerTestSuite))\n}\n"
  }
]