diff --git a/02-semantic-search-rag/README.md b/02-semantic-search-rag/README.md index 5d01bc7..f0e1115 100644 --- a/02-semantic-search-rag/README.md +++ b/02-semantic-search-rag/README.md @@ -15,4 +15,5 @@ username, then build your app inside it. | Contributor | Notes | | ----------- | ----- | +| [`fcenedes/vecdb_bench/`](fcenedes/vecdb_bench/) | Redis-only fork of [vector-db-benchmark](https://github.com/redis-performance/vector-db-benchmark) adapted for **Redis Flex (disk-backed HNSW)** beta. Single config; run with `poetry run ./run_bench.py`, view results with dashboard v2. | | _add yourself_ | | diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/.dockerignore b/02-semantic-search-rag/fcenedes/vecdb_bench/.dockerignore new file mode 100644 index 0000000..95bc655 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/.dockerignore @@ -0,0 +1,121 @@ +# Python virtual environments +venv/ +.venv/ +env/ +.env/ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Poetry +poetry.lock.bak + +# Test and coverage files +.coverage +.pytest_cache/ +.tox/ +.nox/ +htmlcov/ +.coverage.* +coverage.xml +*.cover +.hypothesis/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# Results and data +results/ +# Include datasets.json and random-100 dataset for basic functionality +datasets/* +!datasets/datasets.json +!datasets/random-100/ +*.h5 +*.hdf5 +*.json.gz +*.csv +*.parquet + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# IDE files +.idea/ +.vscode/ +.project +*.swp +*.swo +*~ +*.sublime-project +*.sublime-workspace + +# Git files +.git/ +.gitignore + +# CI/CD files +.github/ + +# Documentation +README.md +LICENSE +*.md +docs/ + +# Temporary files +tmp/ +temp/ +*.tmp +*.temp + +# Log files +*.log +logs/ + +# Archive files +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip +*.bz2 + +# Database files +*.sql +*.sqlite +*.db + +# Docker files themselves +Dockerfile* +.dockerignore +docker-*.sh diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/.gitignore b/02-semantic-search-rag/fcenedes/vecdb_bench/.gitignore new file mode 100644 index 0000000..2e741a4 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/.gitignore @@ -0,0 +1,11 @@ +.idea/ +.pytest_cache/ +__pycache__ +*.pyc +NOTES.md + +results/* +tools/custom/data.json + +*.png +venv/ diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/.pre-commit-config.yaml b/02-semantic-search-rag/fcenedes/vecdb_bench/.pre-commit-config.yaml new file mode 100644 index 0000000..754906c --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/.pre-commit-config.yaml @@ -0,0 +1,30 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +default_language_version: + python: python3.10 + +ci: + autofix_prs: true + autoupdate_commit_msg: '[pre-commit.ci] pre-commit suggestions' + autoupdate_schedule: quarterly + # submodules: true + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: check-added-large-files + + - repo: https://github.com/psf/black + rev: 22.12.0 + hooks: + - id: black + name: "Black: The uncompromising Python code formatter" + + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort + name: "Sort Imports" + args: ["--profile", "black"] diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/DOCKER_README.md b/02-semantic-search-rag/fcenedes/vecdb_bench/DOCKER_README.md new file mode 100644 index 0000000..a4ed1f9 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/DOCKER_README.md @@ -0,0 +1,176 @@ +# Redis Vector Database Benchmark + +A comprehensive benchmarking tool for vector databases, including Redis (both RediSearch and Vector Sets), Weaviate, Milvus, Qdrant, OpenSearch, Postgres, and others... + +In a one-liner cli tool you can get this and much more: + +``` +docker run --rm --network=host redis/vector-db-benchmark:latest run.py --host localhost --engines vectorsets-fp32-default --datasets glove-100-angular --parallels 100 +(...) +================================================================================ +BENCHMARK RESULTS SUMMARY +Experiment: vectorsets-fp32-default - glove-100-angular +================================================================================ + +Precision vs Performance Trade-off: +-------------------------------------------------- +Precision QPS P50 (ms) P95 (ms) +-------------------------------------------------- +0.86 1408.3 61.877 107.548 +0.80 2136.3 38.722 69.102 +0.72 2954.3 25.820 48.072 +0.68 3566.5 20.229 38.581 + +QPS vs Precision Trade-off - vectorsets-fp32-default - glove-100-angular (up and to the right is better): + + 3566 │● + │ ● + │ + 2594 │ + │ ● + │ + 1621 │ ● + │ + │ + 648 │ + │ + │ + 0 │ + └──────────────────────────────────────────────────────────── + 0.680 0.726 0.772 0.817 + Precision (0.0 = 0%, 1.0 = 100%) +================================================================================ + +``` + +## Quick Start + +```bash +# Pull the latest image +docker pull redis/vector-db-benchmark:latest + +# Run with help +docker run --rm redis/vector-db-benchmark:latest run.py --help + +# Check available datasets +docker run --rm redis/vector-db-benchmark:latest run.py --describe datasets + +# Basic Redis benchmark (requires local Redis) +docker run --rm -v $(pwd)/results:/app/results --network=host \ + redis/vector-db-benchmark:latest \ + run.py --host localhost --engines redis-default-simple --datasets random-100 +``` + +## Features + +- **42+ Datasets**: Pre-configured datasets from 25 to 1B+ vectors +- **Multiple Engines**: Redis, Qdrant, Weaviate, Milvus, and more +- **Real-time Monitoring**: Live performance metrics during benchmarks +- **Precision Analysis**: Detailed accuracy vs performance trade-offs +- **Easy Discovery**: `--describe` commands for datasets and engines + +## Available Tags + +- `latest` - Latest development build from update.redisearch branch + +## Redis quick start + +### Redis 8.2 with RediSearch +```bash +# Start Redis 8.2 with built-in vector support +docker run -d --name redis-test -p 6379:6379 redis:8.2-bookworm + +# Run benchmark +docker run --rm -v $(pwd)/results:/app/results --network=host \ + redis/vector-db-benchmark:latest \ + run.py --host localhost --engines redis-default-simple --datasets glove-25-angular +``` + + +## Common Usage Patterns + +### Explore Available Options +```bash +# List all datasets +docker run --rm redis/vector-db-benchmark:latest run.py --describe datasets + +# List all engines +docker run --rm redis/vector-db-benchmark:latest run.py --describe engines +``` + +### Run Benchmarks +```bash +# Quick test with small dataset +docker run --rm -v $(pwd)/results:/app/results --network=host \ + redis/vector-db-benchmark:latest \ + run.py --host localhost --engines redis-default-simple --datasets random-100 + +# Comprehensive benchmark with multiple configurations +docker run --rm -v $(pwd)/results:/app/results --network=host \ + redis/vector-db-benchmark:latest \ + run.py --host localhost --engines "*redis*" --datasets glove-25-angular + +# With Redis authentication +docker run --rm -v $(pwd)/results:/app/results --network=host \ + -e REDIS_AUTH=mypassword -e REDIS_USER=myuser \ + redis/vector-db-benchmark:latest \ + run.py --host localhost --engines redis-default-simple --datasets random-100 +``` + +### Results Analysis +```bash +# View precision summary +jq '.precision_summary' results/*-summary.json + +# View detailed results +jq '.search' results/*-summary.json +``` + +## Volume Mounts + +- `/app/results` - Benchmark results (JSON files) +- `/app/datasets` - Dataset storage (optional, auto-downloaded) + +## Environment Variables + +- `REDIS_HOST` - Redis server hostname (default: localhost) +- `REDIS_PORT` - Redis server port (default: 6379) +- `REDIS_AUTH` - Redis password (default: None) +- `REDIS_USER` - Redis username (default: None) +- `REDIS_CLUSTER` - Enable Redis cluster mode (default: 0) + +## Performance Tips + +1. **Use `--network=host`** for best performance with local Redis +2. **Mount results volume** to persist benchmark data +3. **Start with small datasets** (random-100, glove-25-angular) for testing +4. **Use wildcard patterns** to test multiple configurations: `--engines "*-m-16-*"` + +## Example Output + +```json +{ + "precision_summary": { + "0.91": { + "qps": 1924.5, + "p50": 49.828, + "p95": 58.427 + }, + "0.94": { + "qps": 1819.9, + "p50": 51.68, + "p95": 66.83 + } + } +} +``` + +## Support + +- **GitHub**: [redis-performance/vector-db-benchmark](https://github.com/redis-performance/vector-db-benchmark) +- **Issues**: Report bugs and feature requests on GitHub +- **Documentation**: Full documentation available in the repository + +## License + +This project is licensed under the MIT License - see the repository for details. diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/DOCKER_SETUP.md b/02-semantic-search-rag/fcenedes/vecdb_bench/DOCKER_SETUP.md new file mode 100644 index 0000000..24d55b1 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/DOCKER_SETUP.md @@ -0,0 +1,182 @@ +# Docker Setup and Publishing Guide + +This guide explains how to set up Docker publishing for the `vector-db-benchmark` project to Docker Hub repository `redis/vector-db-benchmark`. + +## 🔐 Required GitHub Secrets + +To enable automated Docker publishing, you need to configure the following secrets in your GitHub repository: + +### Setting up Docker Hub Secrets + +1. **Go to your GitHub repository** → Settings → Secrets and variables → Actions + +2. **Add the following repository secrets:** + + - **`DOCKER_USERNAME`**: Your Docker Hub username + - **`DOCKER_PASSWORD`**: Your Docker Hub access token (NOT your password) + +### Creating a Docker Hub Access Token + +1. **Log in to Docker Hub** at https://hub.docker.com +2. **Go to Account Settings** → Security → Access Tokens +3. **Click "New Access Token"** +4. **Configure the token:** + - Name: `GitHub Actions - vector-db-benchmark` + - Permissions: `Read, Write, Delete` +5. **Copy the generated token** and use it as `DOCKER_PASSWORD` secret + +⚠️ **Important**: Use an access token, not your Docker Hub password, for better security. + +### Credential Validation + +All Docker publishing workflows include automatic credential validation: + +- **PR Validation**: Checks if credentials are available but continues without them (expected for forks) +- **Default Branch/Release Publishing**: **Requires** credentials and fails if not configured +- **Local Testing**: Warns if credentials are missing but continues validation + +This ensures that: +- External contributors can still validate Docker builds in PRs +- Publishing workflows fail fast if credentials are misconfigured +- Local development works regardless of credential status + +## 🚀 Automated Publishing + +Once secrets are configured, Docker images will be automatically published: + +### Default Branch Commits (update.redisearch) +- **Trigger**: Every push to `update.redisearch` branch +- **Tags**: `latest`, `update.redisearch-{sha}`, `update.redisearch-{timestamp}` +- **Platforms**: `linux/amd64`, `linux/arm64` + +### Releases +- **Trigger**: When a GitHub release is published +- **Tags**: `{version}`, `{major}.{minor}`, `{major}`, `latest` +- **Platforms**: `linux/amd64`, `linux/arm64` +- **Security**: Includes Trivy vulnerability scanning + +### Example Tags for Release v1.2.3 +``` +redis/vector-db-benchmark:v1.2.3 +redis/vector-db-benchmark:latest +``` + +## 🛠️ Manual Building and Publishing + +### Local Build +```bash +# Build only +./docker-build.sh + +# Build with custom tag +./docker-build.sh -t v1.0.0 + +# Build and push to Docker Hub +./docker-build.sh -t v1.0.0 --push + +# Multi-platform build and push +./docker-build.sh -p linux/amd64,linux/arm64 --push + +# Run local validation tests (mimics GitHub Action) +./docker-test.sh +``` + +### Prerequisites for Manual Push +```bash +# Login to Docker Hub +docker login + +# Or use environment variables +export DOCKER_USERNAME=your_username +export DOCKER_PASSWORD=your_access_token +./docker-build.sh --push +``` + +## 📦 Using the Docker Images + +### Pull and Run +```bash +# Latest version +docker pull redis/vector-db-benchmark:latest +docker run --rm redis/vector-db-benchmark:latest run.py --help + +# Specific version +docker pull redis/vector-db-benchmark:v1.2.3 +docker run --rm redis/vector-db-benchmark:v1.2.3 run.py --help +``` + +### Example Usage +```bash +# Basic Redis benchmark +docker run --rm --network=host redis/vector-db-benchmark:latest \ + run.py --host localhost --engines redis --dataset random-100 --experiment redis-default-simple + +# With custom Redis host +docker run --rm redis/vector-db-benchmark:latest \ + run.py --host redis-server --engines redis --dataset random-100 --experiment redis-default-simple + +# With results output (mount current directory) +docker run --rm -v $(pwd)/results:/app/results --network=host \ + redis/vector-db-benchmark:latest \ + run.py --host localhost --engines redis --dataset random-100 --experiment redis-default-simple + +# Using with Redis container +docker run -d --name redis-test -p 6379:6379 redis:8.2-rc1-bookworm +docker run --rm --network=host redis/vector-db-benchmark:latest \ + run.py --host localhost --engines redis --experiment redis-default-simple +docker stop redis-test && docker rm redis-test +``` + +## 🔍 Monitoring and Troubleshooting + +### GitHub Actions +- Check workflow runs in **Actions** tab +- View build logs and summaries +- Monitor security scan results + +### PR Validation +- **Automatic Docker build validation** on all pull requests +- Tests both single-platform (`linux/amd64`) and multi-platform builds +- Validates basic functionality and Redis connectivity +- Provides PR comments with build status and details +- Prevents merging PRs with broken Docker builds + +### Docker Hub +- View images at: https://hub.docker.com/r/redis/vector-db-benchmark +- Check image sizes and platforms +- Review vulnerability scan results + +### Common Issues + +1. **Authentication Failed** + - Verify `DOCKER_USERNAME` and `DOCKER_PASSWORD` secrets + - Ensure access token has correct permissions + +2. **Build Failed** + - Check Python version compatibility + - Verify Dockerfile syntax + - Review build logs in Actions + +3. **Push Failed** + - Confirm repository exists on Docker Hub + - Check network connectivity + - Verify permissions + +## 🏗️ Architecture + +### Multi-Stage Build +- **Stage 1**: Python build environment with full toolchain +- **Stage 2**: Minimal runtime with only necessary components +- **Result**: Optimized image size with security best practices + +### Security Features +- Non-root user execution +- Minimal attack surface +- Vulnerability scanning with Trivy +- Signed images (when configured) + +### Performance Optimizations +- Layer caching for faster builds +- Multi-platform support +- Efficient .dockerignore +- Build argument optimization diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/Dockerfile b/02-semantic-search-rag/fcenedes/vecdb_bench/Dockerfile new file mode 100644 index 0000000..4eab04b --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/Dockerfile @@ -0,0 +1,109 @@ +# Multi-stage Dockerfile for vector-db-benchmark +# Stage 1: Build environment +FROM python:3.13-slim AS builder + +# Build arguments for Git metadata +ARG GIT_SHA +ARG GIT_DIRTY + +# Environment variables for Python +ENV PYTHONFAULTHANDLER=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONHASHSEED=random \ + PIP_NO_CACHE_DIR=off \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_DEFAULT_TIMEOUT=100 \ + POETRY_VERSION=1.5.1 + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + wget \ + git \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Install Poetry +RUN pip install "poetry==$POETRY_VERSION" + +# Set working directory +WORKDIR /code + +# Copy dependency files first for better caching +COPY poetry.lock pyproject.toml /code/ + +# Configure Poetry and install dependencies +RUN poetry config virtualenvs.create false \ + && poetry install --no-dev --no-interaction --no-ansi + +# Install additional dependencies +RUN pip install "boto3" + +# Copy source code +COPY . /code + +# Store Git information +RUN if [ -z "$GIT_SHA" ]; then \ + GIT_SHA=$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \ + fi && \ + if [ -z "$GIT_DIRTY" ]; then \ + GIT_DIRTY=$(git diff --no-ext-diff 2>/dev/null | wc -l || echo "0"); \ + fi && \ + echo "Built with GIT_SHA=${GIT_SHA}, GIT_DIRTY=${GIT_DIRTY}" > /code/build_info.txt + +# Stage 2: Runtime environment +FROM python:3.13-slim + +# Environment variables for Python +ENV PYTHONFAULTHANDLER=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONHASHSEED=random + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + wget \ + && rm -rf /var/lib/apt/lists/* + + +# Set working directory +WORKDIR /app + +# Copy Python environment from builder +COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages +COPY --from=builder /usr/local/bin /usr/local/bin + +# Copy application code +COPY --from=builder /code /app + +# Create directories with proper permissions +RUN mkdir -p /app/results /app/datasets && \ + + chmod -R 777 /app/results /app/datasets && \ + chmod -R 755 /app + +# Create entrypoint script to handle user permissions +RUN echo '#!/bin/bash\n\ +# Handle user permissions for volume mounts\n\ +if [ "$1" = "run.py" ]; then\n\ + # Ensure results directory is writable\n\ + mkdir -p /app/results\n\ + chmod 777 /app/results\n\ +fi\n\ +exec python "$@"' > /app/entrypoint.sh && \ + chmod +x /app/entrypoint.sh + + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import sys; sys.exit(0)" || exit 1 + +# Expose common ports (for documentation purposes) +EXPOSE 6379 6380 + +# Set entrypoint + +ENTRYPOINT ["/app/entrypoint.sh"] + + +# Default command (show help) +CMD ["run.py", "--help"] + diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/LICENSE b/02-semantic-search-rag/fcenedes/vecdb_bench/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/README.md b/02-semantic-search-rag/fcenedes/vecdb_bench/README.md new file mode 100644 index 0000000..2090879 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/README.md @@ -0,0 +1,123 @@ +# Semantic search / RAG knowledge base — `fcenedes/vecdb_bench` + +> Location: [`02-semantic-search-rag/fcenedes/vecdb_bench/`](.) · part of +> [Bug Bash Applications](../../../README.md) · use case +> [Semantic search / RAG](../../README.md). + +## What this app does + +`vecdb_bench` is a fork of the +[vector-db-benchmark](https://github.com/redis-performance/vector-db-benchmark) +harness, trimmed down and adapted to stress a **Redis Flex (disk-backed HNSW)** +index in beta. It simulates the flagship semantic-search / RAG workload: bulk-load a +large corpus of embedding vectors into an on-disk vector index, then hammer it with +KNN queries at varying recall targets and concurrency levels, measuring ingestion +throughput, query latency (p50/p95/p99), and recall. All non-Redis engines +(Qdrant, Milvus, Elasticsearch, OpenSearch, pgvector, Weaviate, MongoDB) have been +removed so the harness targets Redis only. + +The upstream harness docs are preserved in +[`UPSTREAM_README.md`](UPSTREAM_README.md). + +## Schema + +The index is created by +[`engine/clients/redis/configure.py`](engine/clients/redis/configure.py). The +single indexed field is a `VECTOR` field using **HNSW**, which is the only algorithm +Redis Flex supports on disk: + +``` +FT.CREATE ... SCHEMA + vector VECTOR HNSW 12 + TYPE FLOAT32 + DIM + DISTANCE_METRIC + M <16|32|64> + EF_CONSTRUCTION <64|128|256|512> + EF_RUNTIME 64 + RERANK TRUE +``` + +Redis Flex requires `M`, `EF_CONSTRUCTION`, `EF_RUNTIME`, and `RERANK` on every +vector field, and only `FLOAT32`/`FLOAT16` element types — all satisfied here. + +## Dataset & scale + +- **Source:** `gist-960-euclidean` (default), an ANN-benchmarks HDF5 dataset of + 1M × 960-dim GIST descriptors with Euclidean distance. Any dataset supported by + the harness works (override with `BENCH_DATASETS`). +- **Size:** ~1M vectors × 960 dims — large enough to exercise the on-disk index + path rather than staying resident in RAM. +- **Ingestion:** parallel bulk upload (`upload_params.parallel = 32`) via the + Redis client in [`engine/clients/redis/`](engine/clients/redis/). + +## How to run + +We use a **single configuration**. All engine/HNSW variants live in one file, +[`experiments/configurations/benchmark.json`](experiments/configurations/benchmark.json), +which `run_bench.py` treats as the single source of truth (via `--engines-file`). +You don't select or edit anything else — just point the harness at your Redis Flex +endpoint and launch it. + +```bash +# 1. Install dependencies +poetry install + +# 2. Point at your Redis Flex (disk-index-capable) endpoint +export REDIS_HOST= +export REDIS_PORT= +export REDIS_AUTH= # optional +export REDIS_USER= # optional +# REDIS_DIALECT defaults to 2 — Redis Flex does not support DIALECT 4. + +# 3. Run the benchmark (single command) +poetry run ./run_bench.py +``` + +Optional overrides (all have sensible defaults): + +| Env var | Default | Purpose | +| ------- | ------- | ------- | +| `BENCH_DATASETS` | `gist-960-euclidean` | dataset to load / query | +| `BENCH_ENGINES` | `redis` | engine name | +| `BENCH_ENGINES_FILE` | `experiments/configurations/benchmark.json` | the single config file | +| `REDIS_DIALECT` | `2` | query dialect (Flex has no DIALECT 4) | + +Results are written as JSON to [`results/`](results/) (sample runs are included so +you can preview the dashboard without running the benchmark first). + +## Viewing the output — dashboard v2 + +View the results with **dashboard v2**, a Dash app that renders Pareto +latency/recall charts and a results table: + +```bash +poetry run python dashboard_v2.py +``` + +Then open . It reads every run in `results/` automatically. + +## Workload / queries + +Pure KNN search driven from `run.py` at two concurrency levels — **serial** +(`parallel: 1`) and **high concurrency** (`parallel: 100`) — each swept across +`EF_RUNTIME`-equivalent search `ef` values of 64/128/256/512. The single config +file crosses this with HNSW build parameters `M ∈ {16, 32, 64}` and +`EF_CONSTRUCTION ∈ {64, 128, 256, 512}`, giving the full recall/latency frontier +per index build. + +## What to watch + +- **On-disk footprint** — the index should live on disk; watch process RAM vs. + index size (Flex is not zero-RAM — some structures stay pinned). +- **Query latency** — p50 / p95 / p99 at `parallel: 1` and `parallel: 100`. +- **Recall** — reported per run; confirm KNN correctness against ground truth. +- **Ingestion throughput** — upload time at `parallel: 32`. +- **Flex-specific edge cases** — `FT.DROPINDEX DD` is rejected (the harness falls + back to drop-without-DD + `FLUSHALL`, see `configure.py`); the default 500 ms + query timeout returns partial results unless `ON_TIMEOUT FAIL` is set. + +## Findings + +_Record bugs, surprises, and observations from your Flex runs here. Link any tickets +you open (e.g. MOD-XXXXX)._ diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/UPSTREAM_README.md b/02-semantic-search-rag/fcenedes/vecdb_bench/UPSTREAM_README.md new file mode 100644 index 0000000..bb01746 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/UPSTREAM_README.md @@ -0,0 +1,407 @@ +# vector-db-benchmark + +A comprehensive benchmarking tool for vector databases, including Redis (both RediSearch and Vector Sets), Weaviate, Milvus, Qdrant, OpenSearch, Postgres, and others... + +In a one-liner cli tool you can get this and much more: + +``` +docker run --rm --network=host redis/vector-db-benchmark:latest run.py --host localhost --engines vectorsets-fp32-default --datasets glove-100-angular --parallels 100 +(...) +================================================================================ +BENCHMARK RESULTS SUMMARY +Experiment: vectorsets-fp32-default - glove-100-angular +================================================================================ + +Precision vs Performance Trade-off: +-------------------------------------------------- +Precision QPS P50 (ms) P95 (ms) +-------------------------------------------------- +0.86 1408.3 61.877 107.548 +0.80 2136.3 38.722 69.102 +0.72 2954.3 25.820 48.072 +0.68 3566.5 20.229 38.581 + +QPS vs Precision Trade-off - vectorsets-fp32-default - glove-100-angular (up and to the right is better): + + 3566 │● + │ ● + │ + 2594 │ + │ ● + │ + 1621 │ ● + │ + │ + 648 │ + │ + │ + 0 │ + └──────────────────────────────────────────────────────────── + 0.680 0.726 0.772 0.817 + Precision (0.0 = 0%, 1.0 = 100%) +================================================================================ + +``` + +> [View results](https://redis.io/blog/benchmarking-results-for-vector-databases/) + +There are various vector search engines available, and each of them may offer +a different set of features and efficiency. But how do we measure the +performance? There is no clear definition and in a specific case you +may worry about a specific thing, while not paying much attention to other aspects. This +project is a general framework for benchmarking different engines under the +same hardware constraints, so you can choose what works best for you. + +Running any benchmark requires choosing an engine, a dataset and defining the +scenario against which it should be tested. A specific scenario may assume +running the server in a single or distributed mode, a different client +implementation and the number of client instances. + + +## Quick Start + +### Quick Start with Docker + +The easiest way to run vector-db-benchmark is using Docker. We provide pre-built images on Docker Hub. + +```bash +# Pull the latest image +docker pull redis/vector-db-benchmark:latest + +# Run with help +docker run --rm redis/vector-db-benchmark:latest run.py --help + +# Check which datasets are available +docker run --rm redis/vector-db-benchmark:latest run.py --describe datasets + +# Basic Redis benchmark with local Redis +docker run --rm -v $(pwd)/results:/app/results --network=host \ + redis/vector-db-benchmark:latest \ + run.py --host localhost --engines redis-default-simple --datasets glove-25-angular + +# At the end of the run, you will find the results in the `results` directory. Lets open the summary one, in the precision summary + +$ jq ".precision_summary" results/*-summary.json +{ + "0.91": { + "qps": 1924.5, + "p50": 49.828, + "p95": 58.427 + }, + "0.94": { + "qps": 1819.9, + "p50": 51.68, + "p95": 66.83 + }, + "0.9775": { + "qps": 1477.8, + "p50": 65.368, + "p95": 73.849 + }, + "0.9950": { + "qps": 1019.8, + "p50": 95.115, + "p95": 106.73 + } +} +``` + +### Using with Redis + +For testing with Redis, start a Redis container first: + +```bash +# Start Redis container +docker run -d --name redis-test -p 6379:6379 redis:8.2-bookworm + +# Run benchmark against Redis + +docker run --rm -v $(pwd)/results:/app/results --network=host \ + redis/vector-db-benchmark:latest \ + run.py --host localhost --engines redis-default-simple --datasets random-100 + +# Or use the convenience script +./docker-run.sh -H localhost -e redis-default-simple -d random-100 + + +# Clean up Redis container when done +docker stop redis-test && docker rm redis-test +``` + +### Available Docker Images + +- **Latest**: `redis/vector-db-benchmark:latest` + +For detailed Docker setup and publishing information, see [DOCKER_SETUP.md](DOCKER_SETUP.md). + + +## Data sets + +We have a number of precomputed data sets. All data sets have been pre-split into train/test and include ground truth data for the top-100 nearest neighbors. + +| Dataset | Dimensions | Train size | Test size | Neighbors | Distance | +| ----------------------------------------------------------------------------------------------------------- | ---------: | ---------: | --------: | --------: | --------- | +| **LAION Image Embeddings (512D)** | | | | | | +| [LAION-1M: subset of LAION 400M English (image embedings)](https://laion.ai/blog/laion-400-open-dataset/) | 512 | 1,000,000 | 10,000 | 100 | Cosine | +| [LAION-10M: subset of LAION 400M English (image embedings)](https://laion.ai/blog/laion-400-open-dataset/) | 512 | 10,000,000 | 10,000 | 100 | Cosine | +| [LAION-20M: subset of LAION 400M English (image embedings)](https://laion.ai/blog/laion-400-open-dataset/) | 512 | 20,000,000 | 10,000 | 100 | Cosine | +| [LAION-40M: subset of LAION 400M English (image embedings)](https://laion.ai/blog/laion-400-open-dataset/) | 512 | 40,000,000 | 10,000 | 100 | Cosine | +| [LAION-100M: subset of LAION 400M English (image embedings)](https://laion.ai/blog/laion-400-open-dataset/) | 512 | 100,000,000 | 10,000 | 100 | Cosine | +| [LAION-200M: subset of LAION 400M English (image embedings)](https://laion.ai/blog/laion-400-open-dataset/) | 512 | 200,000,000 | 10,000 | 100 | Cosine | +| [LAION-400M: from LAION 400M English (image embedings)](https://laion.ai/blog/laion-400-open-dataset/) | 512 | 400,000,000 | 10,000 | 100 | Cosine | +| **LAION Image Embeddings (768D)** | | | | | | +| [LAION-1M: 768D image embeddings](https://laion.ai/blog/laion-400-open-dataset/) | 768 | 1,000,000 | 10,000 | 100 | Cosine | +| [LAION-1B: 768D image embeddings](https://laion.ai/blog/laion-400-open-dataset/) | 768 | 1,000,000,000| 10,000 | 100 | Cosine | +| **Standard Benchmarks** | | | | | | +| [GloVe-25: Word vectors](http://ann-benchmarks.com) | 25 | 1,183,514 | 10,000 | 100 | Cosine | +| [GloVe-100: Word vectors](http://ann-benchmarks.com) | 100 | 1,183,514 | 10,000 | 100 | Cosine | +| [Deep Image-96: CNN image features](http://ann-benchmarks.com) | 96 | 9,990,000 | 10,000 | 100 | Cosine | +| [GIST-960: Image descriptors](http://ann-benchmarks.com) | 960 | 1,000,000 | 1,000 | 100 | L2 | +| **Text and Knowledge Embeddings** | | | | | | +| [DBpedia OpenAI-1M: Knowledge embeddings](https://www.dbpedia.org/) | 1,536 | 1,000,000 | 10,000 | 100 | Cosine | +| [LAION Small CLIP: Small CLIP embeddings](https://laion.ai/blog/laion-400-open-dataset/) | 512 | 100,000 | 1,000 | 100 | Cosine | +| **Yandex Datasets** | | | | | | +| [Yandex T2I: Text-to-image embeddings](https://research.yandex.com/) | 200 | 1,000,000 | 100,000 | 100 | Dot | +| **Random and Synthetic** | | | | | | +| Random-100: Small synthetic dataset | 100 | 100 | 9 | 9 | Cosine | +| Random-100-Euclidean: Small synthetic dataset | 100 | 100 | 9 | 9 | L2 | +| **Filtered Search Datasets** | | | | | | +| H&M-2048: Fashion product embeddings (with filters) | 2,048 | 105,542 | 2,000 | 100 | Cosine | +| H&M-2048: Fashion product embeddings (no filters) | 2,048 | 105,542 | 2,000 | 100 | Cosine | +| ArXiv-384: Academic paper embeddings (with filters) | 384 | 2,205,995 | 10,000 | 100 | Cosine | +| ArXiv-384: Academic paper embeddings (no filters) | 384 | 2,205,995 | 10,000 | 100 | Cosine | +| Random Match Keyword-100: Synthetic keyword matching (with filters) | 100 | 1,000,000 | 10,000 | 100 | Cosine | +| Random Match Keyword-100: Synthetic keyword matching (no filters) | 100 | 1,000,000 | 10,000 | 100 | Cosine | +| Random Match Int-100: Synthetic integer matching (with filters) | 100 | 1,000,000 | 10,000 | 100 | Cosine | +| Random Match Int-100: Synthetic integer matching (no filters) | 100 | 1,000,000 | 10,000 | 100 | Cosine | +| Random Range-100: Synthetic range queries (with filters) | 100 | 1,000,000 | 10,000 | 100 | Cosine | +| Random Range-100: Synthetic range queries (no filters) | 100 | 1,000,000 | 10,000 | 100 | Cosine | +| Random Geo Radius-100: Synthetic geo queries (with filters) | 100 | 1,000,000 | 10,000 | 100 | Cosine | +| Random Geo Radius-100: Synthetic geo queries (no filters) | 100 | 1,000,000 | 10,000 | 100 | Cosine | +| Random Match Keyword-2048: Large synthetic keyword matching (with filters) | 2,048 | 100,000 | 1,000 | 100 | Cosine | +| Random Match Keyword-2048: Large synthetic keyword matching (no filters) | 2,048 | 100,000 | 1,000 | 100 | Cosine | +| Random Match Int-2048: Large synthetic integer matching (with filters) | 2,048 | 100,000 | 1,000 | 100 | Cosine | +| Random Match Int-2048: Large synthetic integer matching (no filters) | 2,048 | 100,000 | 1,000 | 100 | Cosine | +| Random Range-2048: Large synthetic range queries (with filters) | 2,048 | 100,000 | 1,000 | 100 | Cosine | +| Random Range-2048: Large synthetic range queries (no filters) | 2,048 | 100,000 | 1,000 | 100 | Cosine | +| Random Geo Radius-2048: Large synthetic geo queries (with filters) | 2,048 | 100,000 | 1,000 | 100 | Cosine | +| Random Geo Radius-2048: Large synthetic geo queries (no filters) | 2,048 | 100,000 | 1,000 | 100 | Cosine | +| Random Match Keyword Small Vocab-256: Small vocabulary keyword matching (with filters) | 256 | 1,000,000 | 10,000 | 100 | Cosine | +| Random Match Keyword Small Vocab-256: Small vocabulary keyword matching (no filters) | 256 | 1,000,000 | 10,000 | 100 | Cosine | + + +## How to run a benchmark? + +Benchmarks are implemented in server-client mode, meaning that the server is +running in a single machine, and the client is running on another. + +### Run the server + +All engines are served using docker compose. The configuration is in the [servers](./engine/servers/). + +To launch the server instance, run the following command: + +```bash +cd ./engine/servers/ +docker compose up +``` + +Containers are expected to expose all necessary ports, so the client can connect to them. + +### Run the client + +Install dependencies: + +```bash +pip install poetry +poetry install +``` + +Run the benchmark: + +```bash +# Basic usage examples +python run.py --engines redis-default-simple --datasets random-100 +python run.py --engines redis-default-simple --datasets glove-25-angular +python run.py --engines "*-m-16-*" --datasets "glove-*" + +# Using custom engine configurations from a JSON file +python run.py --engines-file custom_engines.json --datasets glove-25-angular + +# Get information about available engines (with pattern matching) +python run.py --engines "*redis*" --describe engines --verbose + +# Get information about engines from a custom file +python run.py --engines-file custom_engines.json --describe engines --verbose + +# Docker usage (recommended) +docker run --rm -v $(pwd)/results:/app/results --network=host \ + redis/vector-db-benchmark:latest \ + run.py --host localhost --engines redis-default-simple --datasets random-100 + +# Get help +python run.py --help +``` + +Command allows you to specify wildcards for engines and datasets. +Results of the benchmarks are stored in the `./results/` directory. + +## Using Custom Engine Configurations + +The benchmark tool supports two ways to specify which engine configurations to use: + +### 1. Pattern Matching (Default) +Use the `--engines` flag with wildcard patterns to select configurations from the `experiments/configurations/` directory: + +```bash +python run.py --engines "*redis*" --datasets glove-25-angular +python run.py --engines "qdrant-m-*" --datasets random-100 +``` + +### 2. Custom Configuration File +Use the `--engines-file` flag to specify a JSON file containing custom engine configurations: + +```bash +python run.py --engines-file my_engines.json --datasets glove-25-angular +``` + +The JSON file should contain an array of engine configuration objects. Each configuration must have a `name` field and follow the same structure as configurations in `experiments/configurations/`: + +```json +[ + { + "name": "my-custom-redis-config", + "engine": "redis", + "connection_params": {}, + "collection_params": { + "algorithm": "hnsw", + "data_type": "FLOAT32", + "hnsw_config": { + "M": 16, + "DISTANCE_METRIC": "L2", + "EF_CONSTRUCTION": 200 + } + }, + "search_params": [ + { + "parallel": 1, + "top": 10, + "search_params": { + "ef": 100, + "data_type": "FLOAT32" + } + } + ], + "upload_params": { + "parallel": 16, + "data_type": "FLOAT32" + } + } +] +``` + +**Note:** You cannot use both `--engines` and `--engines-file` at the same time. + +## How to update benchmark parameters? + +Each engine has a configuration file, which is used to define the parameters for the benchmark. +Configuration files are located in the [configuration](./experiments/configurations/) directory. + +Each step in the benchmark process is using a dedicated configuration's path: + +* `connection_params` - passed to the client during the connection phase. +* `collection_params` - parameters, used to create the collection, indexing parameters are usually defined here. +* `upload_params` - parameters, used to upload the data to the server. +* `search_params` - passed to the client during the search phase. Framework allows multiple search configurations for the same experiment run. + +Exact values of the parameters are individual for each engine. + +## How to register a dataset? + +Datasets are configured in the [datasets/datasets.json](./datasets/datasets.json) file. +Framework will automatically download the dataset and store it in the [datasets](./datasets/) directory. + +## How to implement a new engine? + +There are a few base classes that you can use to implement a new engine. + +* `BaseConfigurator` - defines methods to create collections, setup indexing parameters. +* `BaseUploader` - defines methods to upload the data to the server. +* `BaseSearcher` - defines methods to search the data. + +See the examples in the [clients](./engine/clients) directory. + +Once all the necessary classes are implemented, you can register the engine in the [ClientFactory](./engine/clients/client_factory.py). + +## Running on Kubernetes + +You can run the benchmarks on Kubernetes using the provided manifests in the `k8s/` directory. + +### Prerequisites + +1. A Kubernetes cluster with `kubectl` configured +2. The `vector-db-benchmark` Docker image available (either built locally or pushed to a registry) +3. Access to the target vector databases from within the cluster + +### Setup + +1. **Configure secrets** - Edit `k8s/secrets.yaml` with your database credentials: + +```bash +# Edit the secrets file with your actual credentials +vim k8s/secrets.yaml + +# Apply the secrets +kubectl apply -f k8s/secrets.yaml +``` + +2. **Create persistent volumes** - For storing results and datasets: + +```bash +kubectl apply -f k8s/pvc.yaml +``` + +3. **Run the benchmark job**: + +```bash +kubectl apply -f k8s/benchmark-job.yaml +``` + +### Customizing the Benchmark + +You can customize the benchmark by editing environment variables in `k8s/benchmark-job.yaml`: + +```yaml +env: + - name: BENCH_DATASETS + value: "gist-960-euclidean" # Change dataset + - name: BENCH_ENGINES + value: "redis" # Change engine +``` + +### Monitoring + +```bash +# Watch job status +kubectl get jobs -w + +# View logs +kubectl logs -f job/vector-db-benchmark + +# Get results (copy from PVC) +kubectl cp :/app/results ./results +``` + +### Cleanup + +```bash +# Delete the job +kubectl delete -f k8s/benchmark-job.yaml + +# Delete PVCs (warning: deletes stored data) +kubectl delete -f k8s/pvc.yaml + +# Delete secrets +kubectl delete -f k8s/secrets.yaml +``` + diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/benchmark/__init__.py b/02-semantic-search-rag/fcenedes/vecdb_bench/benchmark/__init__.py new file mode 100644 index 0000000..8a61cb6 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/benchmark/__init__.py @@ -0,0 +1,10 @@ +import os +from pathlib import Path + +# Base directory point to the main directory of the project, so all the data +# loaded from files can refer to it as a root directory + +BASE_DIRECTORY = Path(__file__).parent.parent +DATASETS_DIR = BASE_DIRECTORY / "datasets" +CODE_DIR = os.path.dirname(__file__) +ROOT_DIR = Path(os.path.dirname(CODE_DIR)) diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/benchmark/config_read.py b/02-semantic-search-rag/fcenedes/vecdb_bench/benchmark/config_read.py new file mode 100644 index 0000000..df044b8 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/benchmark/config_read.py @@ -0,0 +1,28 @@ +import glob +import json +import os + +from benchmark import DATASETS_DIR, ROOT_DIR + + +def read_engine_configs() -> dict: + all_configs = {} + engines_config_dir = os.path.join(ROOT_DIR, "experiments", "configurations") + config_files = glob.glob(os.path.join(engines_config_dir, "*.json")) + for config_file in config_files: + with open(config_file, "r") as fd: + configs = json.load(fd) + for config in configs: + all_configs[config["name"]] = config + + return all_configs + + +def read_dataset_config(): + all_configs = {} + datasets_config_path = DATASETS_DIR / "datasets.json" + with open(datasets_config_path, "r") as fd: + configs = json.load(fd) + for config in configs: + all_configs[config["name"]] = config + return all_configs diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/benchmark/dataset.py b/02-semantic-search-rag/fcenedes/vecdb_bench/benchmark/dataset.py new file mode 100644 index 0000000..968f37d --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/benchmark/dataset.py @@ -0,0 +1,330 @@ +import os +import shutil +import tarfile +import bz2 +import urllib.request +import urllib.parse +from dataclasses import dataclass, field +from typing import Dict, List, Optional, Union +import boto3 +import botocore.exceptions +from benchmark import DATASETS_DIR +from dataset_reader.ann_compound_reader import AnnCompoundReader +from dataset_reader.ann_h5_reader import AnnH5Reader +from dataset_reader.ann_h5_multi_reader import AnnH5MultiReader +from dataset_reader.base_reader import BaseReader +from dataset_reader.json_reader import JSONReader +from tqdm import tqdm +from pathlib import Path + + +@dataclass +class DatasetConfig: + vector_size: int + distance: str + name: str + type: str + path: Dict[ + str, List[Dict[str, str]] + ] # Now path is expected to handle multi-file structure for h5-multi + link: Optional[Dict[str, List[Dict[str, str]]]] = None + schema: Optional[Dict[str, str]] = field(default_factory=dict) + vector_count: Optional[int] = None + description: Optional[str] = None + + +READER_TYPE = { + "h5": AnnH5Reader, + "h5-multi": AnnH5MultiReader, + "jsonl": JSONReader, + "tar": AnnCompoundReader, +} + + +# Progress bar for urllib downloads +def show_progress(block_num, block_size, total_size): + percent = round(block_num * block_size / total_size * 100, 2) + print(f"{percent} %", end="\r") + + +def download_with_headers(url, filename=None, progress_callback=None): + """Download a file with proper HTTP headers to avoid 403 errors.""" + # Create a request with proper headers + headers = { + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': '*/*', + 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate', + 'Connection': 'keep-alive', + 'Upgrade-Insecure-Requests': '1', + } + + req = urllib.request.Request(url, headers=headers) + + # Open the URL + with urllib.request.urlopen(req) as response: + # Get the filename from Content-Disposition header or URL + if filename is None: + content_disposition = response.headers.get('Content-Disposition') + if content_disposition and 'filename=' in content_disposition: + filename = content_disposition.split('filename=')[1].strip('"') + else: + filename = os.path.basename(url.split('?')[0]) + + # Create a temporary file + import tempfile + temp_dir = tempfile.gettempdir() + temp_path = os.path.join(temp_dir, filename) + + # Get total size for progress tracking + total_size = int(response.headers.get('Content-Length', 0)) + + # Download the file + with open(temp_path, 'wb') as f: + downloaded = 0 + block_size = 8192 + block_num = 0 + + while True: + chunk = response.read(block_size) + if not chunk: + break + + f.write(chunk) + downloaded += len(chunk) + block_num += 1 + + if progress_callback: + progress_callback(block_num, block_size, total_size) + + if progress_callback: + print() # New line after progress + + return temp_path, None + + +# Progress handler for S3 downloads +class S3Progress(tqdm): + def __init__(self, total_size): + super().__init__( + total=total_size, unit="B", unit_scale=True, desc="Downloading from S3" + ) + + def __call__(self, bytes_amount): + self.update(bytes_amount) + + +class Dataset: + def __init__( + self, + config: dict, + skip_upload: bool, + skip_search: bool, + upload_start_idx: int, + upload_end_idx: int, + ): + self.config = DatasetConfig(**config) + self.skip_upload = skip_upload + self.skip_search = skip_search + self.upload_start_idx = upload_start_idx + self.upload_end_idx = upload_end_idx + + def download(self): + if isinstance(self.config.path, dict): # Handle multi-file datasets + if self.skip_search is False: + # Download query files + for query in self.config.path.get("queries", []): + self._download_file(query["path"], query["link"]) + else: + print( + f"skipping to download query file given skip_search={self.skip_search}" + ) + if self.skip_upload is False: + # Download data files + for data in self.config.path.get("data", []): + start_idx = data["start_idx"] + end_idx = data["end_idx"] + data_path = data["path"] + data_link = data["link"] + if self.upload_start_idx >= end_idx: + print( + f"skipping downloading {data_path} from {data_link} given {self.upload_start_idx}>{end_idx}" + ) + continue + if self.upload_end_idx < start_idx: + print( + f"skipping downloading {data_path} from {data_link} given {self.upload_end_idx}<{start_idx}" + ) + continue + self._download_file(data["path"], data["link"]) + else: + print( + f"skipping to download data/upload files given skip_upload={self.skip_upload}" + ) + + else: # Handle single-file datasets + target_path = DATASETS_DIR / self.config.path + + if target_path.exists(): + print(f"{target_path} already exists") + return + + if self.config.link: + downloaded_withboto = False + if is_s3_link(self.config.link): + print("Use boto3 to download from S3. Faster!") + try: + self._download_from_s3(self.config.link, target_path) + downloaded_withboto = True + except botocore.exceptions.NoCredentialsError: + print("Credentials not found, downloading without boto3") + if not downloaded_withboto: + print(f"Downloading from URL {self.config.link}...") + tmp_path, _ = download_with_headers( + self.config.link, None, show_progress + ) + self._extract_or_move_file(tmp_path, target_path) + + def _download_file(self, relative_path: str, url: str): + target_path = DATASETS_DIR / relative_path + if target_path.exists(): + print(f"{target_path} already exists") + return + + print(f"Downloading from {url} to {target_path}") + tmp_path, _ = download_with_headers(url, None, show_progress) + self._extract_or_move_file(tmp_path, target_path) + + def _extract_or_move_file(self, tmp_path, target_path): + if tmp_path.endswith(".tgz") or tmp_path.endswith(".tar.gz"): + print(f"Extracting: {tmp_path} -> {target_path}") + (DATASETS_DIR / self.config.path).mkdir(exist_ok=True, parents=True) + with tarfile.open(tmp_path) as file: + file.extractall(target_path) + os.remove(tmp_path) + elif tmp_path.endswith(".bz2"): + print(f"Extracting bz2: {tmp_path} -> {target_path}") + Path(target_path).parent.mkdir(exist_ok=True) + # Remove .bz2 extension from target path if present + if str(target_path).endswith(".bz2"): + final_target_path = str(target_path)[:-4] # Remove .bz2 + else: + final_target_path = target_path + + with bz2.BZ2File(tmp_path, 'rb') as f_in: + with open(final_target_path, 'wb') as f_out: + shutil.copyfileobj(f_in, f_out) + os.remove(tmp_path) + else: + print(f"Moving: {tmp_path} -> {target_path}") + Path(target_path).parent.mkdir(exist_ok=True) + shutil.copy2(tmp_path, target_path) + os.remove(tmp_path) + + def _download_from_s3(self, link, target_path): + s3 = boto3.client("s3") + bucket_name, s3_key = parse_s3_url(link) + tmp_path = f"/tmp/{os.path.basename(s3_key)}" + + print( + f"Downloading from S3: {link}... bucket_name={bucket_name}, s3_key={s3_key}" + ) + object_info = s3.head_object(Bucket=bucket_name, Key=s3_key) + total_size = object_info["ContentLength"] + + with open(tmp_path, "wb") as f: + progress = S3Progress(total_size) + s3.download_fileobj(bucket_name, s3_key, f, Callback=progress) + + self._extract_or_move_file(tmp_path, target_path) + + def get_reader(self, normalize: bool) -> BaseReader: + reader_class = READER_TYPE[self.config.type] + + if self.config.type == "h5-multi": + # For h5-multi, we need to pass both data files and query file + data_files = self.config.path["data"] + for data_file_dict in data_files: + data_file_dict["path"] = DATASETS_DIR / data_file_dict["path"] + query_file = DATASETS_DIR / self.config.path["queries"][0]["path"] + return reader_class( + data_files=data_files, + query_file=query_file, + normalize=normalize, + skip_upload=self.skip_upload, + skip_search=self.skip_search, + ) + else: + # For single-file datasets + return reader_class(DATASETS_DIR / self.config.path, normalize=normalize) + + +def is_s3_link(link): + return link.startswith("s3://") or "s3.amazonaws.com" in link + + +def parse_s3_url(s3_url): + if s3_url.startswith("s3://"): + s3_parts = s3_url.replace("s3://", "").split("/", 1) + bucket_name = s3_parts[0] + s3_key = s3_parts[1] if len(s3_parts) > 1 else "" + else: + s3_parts = s3_url.replace("http://", "").replace("https://", "").split("/", 1) + + if ".s3.amazonaws.com" in s3_parts[0]: + bucket_name = s3_parts[0].split(".s3.amazonaws.com")[0] + s3_key = s3_parts[1] if len(s3_parts) > 1 else "" + else: + bucket_name = s3_parts[0] + s3_key = s3_parts[1] if len(s3_parts) > 1 else "" + + return bucket_name, s3_key + + +if __name__ == "__main__": + dataset_s3_split = Dataset( + { + "name": "laion-img-emb-768d-1Billion-cosine", + "vector_size": 768, + "distance": "cosine", + "type": "h5-multi", + "path": { + "data": [ + { + "file_number": 1, + "path": "laion-1b/data/laion-img-emb-768d-1Billion-cosine-data-part1-0_to_10000000.hdf5", + "link": "http://benchmarks.redislabs.s3.amazonaws.com/vecsim/laion-1b/laion-img-emb-768d-1Billion-cosine-data-part1-0_to_10000000.hdf5", + "vector_range": "0-10000000", + "file_size": "30.7 GB", + }, + { + "file_number": 2, + "path": "laion-1b/data/laion-img-emb-768d-1Billion-cosine-data-part10-90000000_to_100000000.hdf5", + "link": "http://benchmarks.redislabs.s3.amazonaws.com/vecsim/laion-1b/laion-img-emb-768d-1Billion-cosine-data-part10-90000000_to_100000000.hdf5", + "vector_range": "90000000-100000000", + "file_size": "30.7 GB", + }, + { + "file_number": 3, + "path": "laion-1b/data/laion-img-emb-768d-1Billion-cosine-data-part100-990000000_to_1000000000.hdf5", + "link": "http://benchmarks.redislabs.s3.amazonaws.com/vecsim/laion-1b/laion-img-emb-768d-1Billion-cosine-data-part100-990000000_to_1000000000.hdf5", + "vector_range": "990000000-1000000000", + "file_size": "30.7 GB", + }, + ], + "queries": [ + { + "path": "laion-1b/laion-img-emb-768d-1Billion-cosine-queries.hdf5", + "link": "http://benchmarks.redislabs.s3.amazonaws.com/vecsim/laion-1b/laion-img-emb-768d-1Billion-cosine-queries.hdf5", + "file_size": "38.7 MB", + }, + ], + }, + }, + skip_upload=True, + skip_search=False, + ) + + dataset_s3_split.download() + reader = dataset_s3_split.get_reader(normalize=False) + print(reader) # Outputs the AnnH5MultiReader instance diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/chart.py b/02-semantic-search-rag/fcenedes/vecdb_bench/chart.py new file mode 100644 index 0000000..c234614 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/chart.py @@ -0,0 +1,178 @@ +import json +import os + +import matplotlib.pyplot as plt +import argparse + + +x_metrics = {"mean_precisions": {"human_label": "Precision"}} +y_metrics = { + "rps": {"mode": "higher-better", "human_label": "Search Queries per second"}, + "mean_time": { + "mode": "lower-better", + "human_label": "Search avg. latency including RTT (seconds)", + }, + "p50_time": { + "mode": "lower-better", + "human_label": "Search p50 latency including RTT (seconds)", + }, + "p99_time": { + "mode": "lower-better", + "human_label": "Search p99 latency including RTT (seconds)", + }, +} + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--dataset", default="glove-100-angular") + parser.add_argument("-o", "--output", default=None) + parser.add_argument( + "-x", + "--x-axis", + help="Which metric to use on the X-axis", + choices=x_metrics.keys(), + default="mean_precisions", + ) + parser.add_argument( + "--x-axis-left", + default=None, + ) + parser.add_argument( + "--x-axis-right", + default=None, + ) + parser.add_argument( + "--x-axis-label", + default=None, + ) + + parser.add_argument( + "-y", + "--y-axis", + help="Which metric to use on the Y-axis", + choices=y_metrics.keys(), + default="rps", + ) + parser.add_argument( + "--y-axis-label", + default=None, + ) + + parser.add_argument( + "--y-axis-bottom", + default=None, + ) + parser.add_argument( + "--y-axis-top", + default=None, + ) + + parser.add_argument( + "--legend", + default="Redis", + ) + + parser.add_argument( + "--results", type=str, help="results folder to process", default="results" + ) + parser.add_argument( + "--clients", type=int, help="consider results from this client count", default=1 + ) + args = parser.parse_args() + final_results_map = {} + x_axis = [] + y_axis = [] + fig, ax = plt.subplots() + + if os.path.exists(args.results): + print(f"working on dir: {args.results}") + print("reading first the upload data") + for filename in os.listdir(args.results): + f = os.path.join(args.results, filename) + setup_name = filename.split(args.dataset)[0] + setup_name = setup_name[0 : len(setup_name) - 1] + + with open(f, "r") as fd: + try: + json_res = json.load(fd) + except json.decoder.JSONDecodeError as e: + error_str = e.__str__() + print( + f"skipping {filename} given here as an error while processing the file {error_str})" + ) + continue + parallel = 1 + if "parallel" in json_res["params"]: + parallel = json_res["params"]["parallel"] + + if args.clients != parallel: + print( + f"skipping {filename} given the client count ({parallel}) is different than the one we wish to plot ({args.clients})" + ) + continue + # query + if ( + args.x_axis in json_res["results"] + and args.y_axis in json_res["results"] + ): + x_val = json_res["results"][args.x_axis] + y_val = json_res["results"][args.y_axis] + x_axis.append(x_val) + y_axis.append(y_val) + + color = "tab:red" + ax.scatter( + x_axis, y_axis, c=color, label=args.legend, marker="^", edgecolors="none" + ) + + ax.legend() + ax.grid(True) + + x_axis_label = args.x_axis + if args.x_axis in x_metrics: + if "human_label" in x_metrics[args.x_axis]: + x_axis_label = x_metrics[args.x_axis]["human_label"] + if args.x_axis_label is not None: + x_axis_label = args.x_axis_label + plt.xlabel(x_axis_label) + + y_axis_label = args.y_axis + y_axis_mode = "higher-better" + if args.y_axis in y_metrics: + if "human_label" in y_metrics[args.y_axis]: + y_axis_label = y_metrics[args.y_axis]["human_label"] + if "mode" in y_metrics[args.y_axis]: + y_axis_mode = y_metrics[args.y_axis]["mode"] + if args.y_axis_label is not None: + y_axis_label = args.y_axis_label + plt.ylabel(y_axis_label) + title_string = ( + f"{x_axis_label} vs {y_axis_label} ({y_axis_mode}).\nclients={args.clients}" + ) + plt.title(title_string) + + x_axis_left, x_axis_right = plt.xlim() + _, y_axis_top = plt.ylim() + y_axis_bottom = 0 + if args.y_axis_bottom is not None: + y_axis_bottom = float(args.y_axis_bottom) + if args.y_axis_top is not None: + y_axis_top = float(args.y_axis_top) + + plt.ylim(y_axis_bottom, y_axis_top) + + if args.x_axis_left is not None: + x_axis_left = float(args.x_axis_left) + if args.x_axis_right is not None: + x_axis_right = float(args.x_axis_right) + + plt.xlim(x_axis_left, x_axis_right) + + output_file = f"{args.y_axis}.png" + + if args.output is not None: + output_file = args.output + + print(f"writing output to {output_file}") + + plt.savefig(output_file) diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/dashboard.py b/02-semantic-search-rag/fcenedes/vecdb_bench/dashboard.py new file mode 100644 index 0000000..eb69691 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/dashboard.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python3 +""" +Vector DB Benchmark Dashboard + +A Dash-based dashboard for visualizing benchmark results. +""" + +import json +import os +from pathlib import Path +from typing import Dict, List, Any + +import dash +from dash import dcc, html, callback, Input, Output +import plotly.express as px +import pandas as pd + + +def load_results(results_dir: str = "results/") -> pd.DataFrame: + """Load all benchmark results from JSON files into a DataFrame.""" + records = [] + + for filepath in Path(results_dir).glob("*.json"): + filename = filepath.name + + # Skip summary files for now - we'll use individual results + if "summary" in filename: + continue + + with open(filepath, "r") as f: + data = json.load(f) + + params = data.get("params", {}) + results = data.get("results", {}) + + record = { + "filename": filename, + "dataset": params.get("dataset", ""), + "experiment": params.get("experiment", ""), + "engine": params.get("engine", ""), + } + + # Determine if this is a search or upload result + if "search" in filename: + record["type"] = "search" + record["parallel"] = params.get("parallel", 1) + # ================================================================= + # FIX: Handle different search params for different engines + # Redis uses: search_params.ef + # MongoDB uses: search_params.numCandidates + # ================================================================= + search_params = params.get("search_params", {}) + # Try Redis ef first, then MongoDB numCandidates + ef_runtime = search_params.get("ef", 0) + if ef_runtime == 0: + ef_runtime = search_params.get("numCandidates", 0) + record["search_ef"] = ef_runtime + record["rps"] = results.get("rps", 0) + record["mean_time"] = results.get("mean_time", 0) * 1000 # Convert to ms + record["p95_time"] = results.get("p95_time", 0) * 1000 # Convert to ms + record["p99_time"] = results.get("p99_time", 0) * 1000 # Convert to ms + record["mean_precisions"] = results.get("mean_precisions", 0) + elif "upload" in filename: + record["type"] = "upload" + record["parallel"] = params.get("parallel", 1) + record["upload_time"] = results.get("upload_time", 0) + record["total_time"] = results.get("total_time", 0) + memory_usage = results.get("memory_usage", {}) + + # ================================================================= + # Memory breakdown for Redis: + # - used_memory: Total Redis memory (bytes) + # - index_info.total_index_memory_sz_mb: Index memory (MB) + # - Data memory = used_memory - index_memory + # MongoDB doesn't report memory, so all values will be 0 + # ================================================================= + used_memory_bytes = memory_usage.get("used_memory", [0]) + used_memory_bytes = used_memory_bytes[0] if used_memory_bytes else 0 + + index_info = memory_usage.get("index_info", {}) + index_memory_mb = float(index_info.get("total_index_memory_sz_mb", 0) or 0) + index_memory_bytes = index_memory_mb * 1024 * 1024 + + # Calculate data memory (total - index) + data_memory_bytes = max(0, used_memory_bytes - index_memory_bytes) + + record["total_memory_gb"] = used_memory_bytes / (1024 ** 3) + record["index_memory_gb"] = index_memory_bytes / (1024 ** 3) + record["data_memory_gb"] = data_memory_bytes / (1024 ** 3) + # Keep memory_gb for backward compatibility + record["memory_gb"] = record["total_memory_gb"] + else: + continue + + records.append(record) + + return pd.DataFrame(records) + + +def get_unique_values(df: pd.DataFrame, column: str) -> List[str]: + """Get unique non-empty values from a column.""" + return sorted([v for v in df[column].unique() if v]) + + +# Load data +df = load_results() + +# Get unique values for dropdowns +datasets = get_unique_values(df, "dataset") +experiments = get_unique_values(df, "experiment") +parallel_values = sorted(df[df["type"] == "search"]["parallel"].unique()) + +# Metric options +METRIC_OPTIONS = [ + {"label": "Queries Per Second (RPS)", "value": "rps"}, + {"label": "Average Latency (ms)", "value": "mean_time"}, + {"label": "P95 Latency (ms)", "value": "p95_time"}, + {"label": "Index Build Time (s)", "value": "build_time"}, # Stacked bar: upload + indexing + {"label": "Total Memory Usage (GB)", "value": "memory_gb"}, +] + +# Create a lookup dict for metric labels +METRIC_LABELS = {opt["value"]: opt["label"] for opt in METRIC_OPTIONS} + +# Create Dash app +app = dash.Dash(__name__, title="Vector DB Benchmark Dashboard") + +app.layout = html.Div([ + html.H1("Vector DB Benchmark Dashboard", style={"textAlign": "center"}), + + html.Div([ + html.Div([ + html.Label("Dataset:"), + dcc.Dropdown( + id="dataset-dropdown", + options=[{"label": d, "value": d} for d in datasets], + value=datasets[0] if datasets else None, + clearable=False, + ), + ], style={"width": "30%", "display": "inline-block", "padding": "10px"}), + + html.Div([ + html.Label("Number of Clients (Parallel):"), + dcc.Dropdown( + id="parallel-dropdown", + options=[{"label": str(p), "value": p} for p in parallel_values], + value=parallel_values[0] if parallel_values else None, + clearable=False, + ), + ], style={"width": "30%", "display": "inline-block", "padding": "10px"}), + + html.Div([ + html.Label("Metric:"), + dcc.Dropdown( + id="metric-dropdown", + options=METRIC_OPTIONS, + value="rps", + clearable=False, + ), + ], style={"width": "30%", "display": "inline-block", "padding": "10px"}), + ], style={"display": "flex", "justifyContent": "center"}), + + html.Div([ + dcc.Graph(id="benchmark-graph", style={"height": "70vh"}), + ]), + + html.Div([ + html.P("Each point represents a configuration (build params + ef_search). Lines connect points from the same build config. Hover for details.", + style={"textAlign": "center", "color": "gray"}), + ]), +], style={"padding": "20px"}) + + +@callback( + Output("benchmark-graph", "figure"), + [Input("dataset-dropdown", "value"), + Input("parallel-dropdown", "value"), + Input("metric-dropdown", "value")] +) +def update_graph(dataset: str, parallel: int, metric: str): + """Update the graph based on selected filters.""" + if not dataset: + return px.line(title="No data available") + + # Handle upload metrics separately (no precision axis) + if metric in ["build_time", "memory_gb"]: + filtered_df = df[(df["dataset"] == dataset) & (df["type"] == "upload")] + if filtered_df.empty: + return px.bar(title=f"No upload data available for {dataset}") + + # ================================================================= + # Build Time: Stacked bar chart showing upload_time + indexing_time + # Memory: Simple bar chart with average + # ================================================================= + if metric == "build_time": + # Get fastest run per experiment (by total_time) + idx = filtered_df.groupby("experiment")["total_time"].idxmin() + agg_df = filtered_df.loc[idx].copy() + + # Calculate indexing time (total - upload) + agg_df["indexing_time"] = agg_df["total_time"] - agg_df["upload_time"] + + # Reshape for stacked bar chart + stacked_df = pd.melt( + agg_df, + id_vars=["experiment"], + value_vars=["upload_time", "indexing_time"], + var_name="phase", + value_name="time_seconds" + ) + + # Rename phases for display + stacked_df["phase"] = stacked_df["phase"].map({ + "upload_time": "Upload (data insertion)", + "indexing_time": "Indexing (index build)" + }) + + fig = px.bar( + stacked_df, + x="experiment", + y="time_seconds", + color="phase", + title=f"Index Build Time by Build Config (Fastest Run)", + labels={"experiment": "Build Config", "time_seconds": "Time (seconds)", "phase": "Phase"}, + barmode="stack", + ) + fig.update_layout(xaxis_tickangle=-45, showlegend=True, legend_title="Phase") + else: + # ================================================================= + # Memory: Stacked bar chart showing data + index memory + # For Redis: data_memory_gb + index_memory_gb = total_memory_gb + # For MongoDB: No memory data available (all zeros) + # ================================================================= + # Get average per experiment + agg_df = filtered_df.groupby("experiment").agg({ + "data_memory_gb": "mean", + "index_memory_gb": "mean", + "total_memory_gb": "mean", + }).reset_index() + + # Check if we have any memory data + if agg_df["total_memory_gb"].sum() > 0: + # Reshape for stacked bar chart + stacked_df = pd.melt( + agg_df, + id_vars=["experiment"], + value_vars=["data_memory_gb", "index_memory_gb"], + var_name="memory_type", + value_name="memory_gb" + ) + + # Rename for display + stacked_df["memory_type"] = stacked_df["memory_type"].map({ + "data_memory_gb": "Data (vectors + metadata)", + "index_memory_gb": "Index (HNSW graph)" + }) + + fig = px.bar( + stacked_df, + x="experiment", + y="memory_gb", + color="memory_type", + title=f"Memory Usage by Build Config (Average)", + labels={"experiment": "Build Config", "memory_gb": "Memory (GB)", "memory_type": "Type"}, + barmode="stack", + ) + fig.update_layout(xaxis_tickangle=-45, showlegend=True, legend_title="Memory Type") + else: + # No memory data - show empty chart with message + fig = px.bar( + agg_df, + x="experiment", + y="total_memory_gb", + color="experiment", + title=f"Memory Usage by Build Config (No data available)", + labels={"experiment": "Build Config", "total_memory_gb": "Memory (GB)"}, + ) + fig.update_layout(xaxis_tickangle=-45, showlegend=True, legend_title="Build Config") + + return fig + + # Search metrics - line chart with precision on X-axis + filtered_df = df[ + (df["dataset"] == dataset) & + (df["parallel"] == parallel) & + (df["type"] == "search") + ] + if filtered_df.empty: + return px.line(title=f"No search data for {dataset} with {parallel} clients") + + # Aggregate by experiment and search_ef (take mean of multiple runs) + agg_df = filtered_df.groupby(["experiment", "search_ef"]).agg({ + "rps": "mean", + "mean_precisions": "mean", + "mean_time": "mean", + "p95_time": "mean", + }).reset_index() + + # Sort by precision for proper line drawing + agg_df = agg_df.sort_values(["experiment", "mean_precisions"]) + + fig = px.line( + agg_df, + x="mean_precisions", + y=metric, + color="experiment", + markers=True, + title=f"Precision vs {METRIC_LABELS[metric]} ({parallel} clients)", + labels={ + "mean_precisions": "Precision", + metric: METRIC_LABELS[metric], + "experiment": "Build Config", + }, + hover_data=["search_ef", "rps", "mean_time", "p95_time"], + ) + + fig.update_traces(marker=dict(size=10)) + + fig.update_layout( + showlegend=True, + legend_title="Build Config", + xaxis=dict(tickformat=".0%"), + ) + + return fig + + +if __name__ == "__main__": + print("Starting Vector DB Benchmark Dashboard...") + print(f"Loaded {len(df)} result records") + print(f"Datasets: {datasets}") + print(f"Experiments: {experiments}") + print(f"Parallel values: {parallel_values}") + app.run(debug=True, host="0.0.0.0", port=8050) + diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/dashboard_v2.py b/02-semantic-search-rag/fcenedes/vecdb_bench/dashboard_v2.py new file mode 100755 index 0000000..65d9726 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/dashboard_v2.py @@ -0,0 +1,554 @@ +#!/usr/bin/env python3 +""" +Vector DB Benchmark Dashboard v2 +Modern look and feel inspired by Redis benchmark charts. +""" + +import json +import os +from pathlib import Path +from typing import Dict, List, Any + +import dash +from dash import dcc, html, callback, Input, Output, State +import plotly.graph_objects as go +import pandas as pd + +# Color palette matching their scheme - maps engine names to colors +ENGINE_COLORS = { + "redis": "#DC244C", # Redis RED + "mongodb": "#00684A", # MongoDB green + "milvus": "#1493cc", + "weaviate": "#01cc26", + "qdrant": "#bc1439", + "elastic": "#f9b110", +} + +# Fallback colors for unknown engines +FALLBACK_COLORS = [ + "#DC244C", "#4A90D9", "#50C878", "#9B59B6", + "#F39C12", "#1ABC9C", "#E74C3C", "#3498DB", +] + +# Which metrics are "lower is better" +LOWER_IS_BETTER = { + "rps": False, + "mean_time": True, + "p95_time": True, + "p99_time": True, + "upload_time": True, + "total_time": True, +} + + +def get_engine_color(engine: str, index: int) -> str: + """Get color for an engine, with fallback.""" + # Direct match on engine name + if engine in ENGINE_COLORS: + return ENGINE_COLORS[engine] + # Fallback + return FALLBACK_COLORS[index % len(FALLBACK_COLORS)] + + +def filter_pareto_points(df: pd.DataFrame, metric: str) -> pd.DataFrame: + """ + Filter to keep only Pareto-optimal points. + + A point is Pareto-optimal if no other point has both: + - Higher precision AND better metric value + + This creates a smooth frontier curve. + """ + if df.empty: + return df + + lower_is_better = LOWER_IS_BETTER.get(metric, False) + + # Sort by precision descending (highest first) + df = df.sort_values("mean_precisions", ascending=False).copy() + + pareto_points = [] + best_value = None + + for _, row in df.iterrows(): + current_value = row[metric] + + if best_value is None: + # First point (highest precision) is always included + pareto_points.append(row) + best_value = current_value + else: + # Check if this point improves the metric + if lower_is_better: + is_better = current_value < best_value + else: + is_better = current_value > best_value + + if is_better: + pareto_points.append(row) + best_value = current_value + + return pd.DataFrame(pareto_points) + + +def load_results(results_dir: str = "results/") -> pd.DataFrame: + """Load all benchmark results from JSON files into a DataFrame.""" + records = [] + + for filepath in Path(results_dir).glob("*.json"): + filename = filepath.name + if "summary" in filename: + continue + + with open(filepath, "r") as f: + data = json.load(f) + + params = data.get("params", {}) + results = data.get("results", {}) + + record = { + "filename": filename, + "dataset": params.get("dataset", ""), + "experiment": params.get("experiment", ""), + "engine": params.get("engine", ""), + } + + if "search" in filename: + record["type"] = "search" + record["parallel"] = params.get("parallel", 1) + search_params = params.get("search_params", {}) + ef_runtime = search_params.get("ef", 0) or search_params.get("numCandidates", 0) + record["search_ef"] = ef_runtime + record["rps"] = results.get("rps", 0) + record["mean_time"] = results.get("mean_time", 0) * 1000 + record["p95_time"] = results.get("p95_time", 0) * 1000 + record["p99_time"] = results.get("p99_time", 0) * 1000 + record["mean_precisions"] = results.get("mean_precisions", 0) + elif "upload" in filename: + record["type"] = "upload" + record["parallel"] = params.get("parallel", 1) + record["upload_time"] = results.get("upload_time", 0) + record["total_time"] = results.get("total_time", 0) + memory_usage = results.get("memory_usage", {}) + used_memory_bytes = memory_usage.get("used_memory", [0]) + used_memory_bytes = used_memory_bytes[0] if used_memory_bytes else 0 + index_info = memory_usage.get("index_info", {}) + index_memory_mb = float(index_info.get("total_index_memory_sz_mb", 0) or 0) + index_memory_bytes = index_memory_mb * 1024 * 1024 + data_memory_bytes = max(0, used_memory_bytes - index_memory_bytes) + record["total_memory_gb"] = used_memory_bytes / (1024 ** 3) + record["index_memory_gb"] = index_memory_bytes / (1024 ** 3) + record["data_memory_gb"] = data_memory_bytes / (1024 ** 3) + else: + continue + + records.append(record) + + return pd.DataFrame(records) + + +# Load data +df = load_results() +datasets = sorted([v for v in df["dataset"].unique() if v]) +parallel_values = sorted(df[df["type"] == "search"]["parallel"].unique(), reverse=True) +experiments = sorted([v for v in df["experiment"].unique() if v]) + +# Create Dash app with custom CSS +app = dash.Dash(__name__, title="Vector DB Benchmark") + +# Custom CSS styles +app.index_string = ''' + + + + {%metas%} + {%title%} + {%favicon%} + {%css%} + + + + {%app_entry%} +
+ {%config%} + {%scripts%} + {%renderer%} +
+ + +''' + +# Layout +app.layout = html.Div([ + # Header + html.Div([ + html.H1("Vector DB Benchmark"), + html.P("Compare vector search performance across different engines and configurations"), + ], className="page-header"), + + # Main wrapper + html.Div([ + # Controls row + html.Div([ + # Dataset selector + html.Div([ + html.Label("Dataset"), + dcc.Dropdown( + id="dataset-selector", + options=[{"label": d, "value": d} for d in datasets], + value=datasets[0] if datasets else None, + clearable=False, + style={"minWidth": "200px"}, + ), + ], className="control-group"), + + # Clients selector + html.Div([ + html.Label("Clients"), + dcc.Dropdown( + id="parallel-selector", + options=[{"label": str(p), "value": p} for p in parallel_values], + value=parallel_values[0] if parallel_values else 1, + clearable=False, + style={"minWidth": "80px"}, + ), + ], className="control-group"), + + # Metric radio buttons + html.Div([ + html.Div([ + html.Div("RPS", id="btn-rps", className="radio-item selected", n_clicks=0), + html.Div("Avg Latency", id="btn-mean_time", className="radio-item", n_clicks=0), + html.Div("p95 Latency", id="btn-p95_time", className="radio-item", n_clicks=0), + html.Div("Index Time", id="btn-upload_time", className="radio-item", n_clicks=0), + ], className="radio-group"), + ]), + ], className="controls"), + + # Legend + html.Div(id="legend-container", className="legend-hint"), + + # Chart + html.Div([ + dcc.Graph(id="main-chart", config={"displayModeBar": False}), + ], className="chart-container"), + + # Precision slider + html.Div([ + dcc.Slider( + id="precision-slider", + min=0.5, + max=1.0, + step=0.01, + value=0.9, + marks={i/10: f"{i/10:.1f}" for i in range(5, 11)}, + tooltip={"placement": "bottom", "always_visible": True}, + ), + html.Span("Drag to set minimum precision cutoff for the table below", className="slider-label"), + ], className="precision-slider"), + + # Data table + html.Div(id="data-table-container"), + + # Hidden store for selected metric + dcc.Store(id="selected-metric", data="rps"), + + ], className="benchmark-wrapper"), +]) + + +@callback( + Output("selected-metric", "data"), + Output("btn-rps", "className"), + Output("btn-mean_time", "className"), + Output("btn-p95_time", "className"), + Output("btn-upload_time", "className"), + Input("btn-rps", "n_clicks"), + Input("btn-mean_time", "n_clicks"), + Input("btn-p95_time", "n_clicks"), + Input("btn-upload_time", "n_clicks"), + State("selected-metric", "data"), +) +def update_metric_selection(n1, n2, n3, n4, current_metric): + """Handle metric button clicks.""" + ctx = dash.callback_context + if not ctx.triggered: + return current_metric, "radio-item selected", "radio-item", "radio-item", "radio-item" + + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + metric_map = { + "btn-rps": "rps", + "btn-mean_time": "mean_time", + "btn-p95_time": "p95_time", + "btn-upload_time": "upload_time", + } + + selected = metric_map.get(button_id, current_metric) + classes = {k: "radio-item selected" if v == selected else "radio-item" for k, v in metric_map.items()} + + return selected, classes["btn-rps"], classes["btn-mean_time"], classes["btn-p95_time"], classes["btn-upload_time"] + + + +@callback( + Output("main-chart", "figure"), + Output("legend-container", "children"), + Input("dataset-selector", "value"), + Input("parallel-selector", "value"), + Input("selected-metric", "data"), + Input("precision-slider", "value"), +) +def update_chart(dataset, parallel, metric, precision_cutoff): + """Update the main chart based on selections.""" + if not dataset: + return go.Figure(), [] + + # Handle Index Time: join upload data with search data to get precision + if metric == "upload_time": + upload_df = df[(df["dataset"] == dataset) & (df["type"] == "upload")] + search_df = df[(df["dataset"] == dataset) & (df["type"] == "search")] + + if upload_df.empty: + return go.Figure().update_layout(title="No upload data available"), [] + + # Get best precision per experiment from search results + precision_by_exp = search_df.groupby("experiment")["mean_precisions"].max().to_dict() + + # Get fastest upload per experiment and add precision + idx = upload_df.groupby("experiment")["total_time"].idxmin() + upload_df = upload_df.loc[idx].copy() + upload_df["mean_precisions"] = upload_df["experiment"].map(precision_by_exp) + + # Drop experiments without precision data + upload_df = upload_df.dropna(subset=["mean_precisions"]) + + if upload_df.empty: + return go.Figure().update_layout(title="No matching upload/search data"), [] + + fig = go.Figure() + legend_items = [] + + y_label = "Total Build Time (s)" + + # Aggregate by engine and apply Pareto filtering (lower time is better) + for i, engine in enumerate(sorted(upload_df["engine"].unique())): + engine_df = upload_df[upload_df["engine"] == engine].copy() + + # For build time, lower is better - use Pareto filtering + engine_df = filter_pareto_points(engine_df, "total_time") + engine_df = engine_df.sort_values("mean_precisions") + + color = get_engine_color(engine, i) + + fig.add_trace(go.Scatter( + x=engine_df["mean_precisions"], + y=engine_df["total_time"], + mode="lines+markers", + name=engine, + line=dict(color=color, width=2.5, shape="spline"), + marker=dict(size=8, color=color), + hovertemplate=( + f"{engine}
" + f"Precision: %{{x:.4f}}
" + f"{y_label}: %{{y:.1f}}s
" + f"Config: %{{customdata}}" + ), + customdata=engine_df["experiment"], + )) + + legend_items.append(html.Div([ + html.Span(className="legend-dot", style={"backgroundColor": color}), + html.Span(engine), + ], className="legend-item")) + + # Add precision cutoff line + y_max = upload_df["total_time"].max() * 1.1 + fig.add_trace(go.Bar( + name="Precision cutoff", + x=[precision_cutoff], + y=[y_max], + marker_color="rgba(255, 0, 0, 0.2)", + width=0.005, + hoverinfo="skip", + )) + + fig.update_layout( + showlegend=False, + plot_bgcolor="white", + paper_bgcolor="white", + barmode="overlay", + xaxis=dict(title="Precision", gridcolor="#f1f3f8", range=[0.5, 1.0]), + yaxis=dict(title=y_label, gridcolor="#f1f3f8", zeroline=False, rangemode="tozero"), + margin=dict(l=60, r=20, t=20, b=40), + hovermode="closest", + ) + return fig, legend_items + + # Search metrics - line chart with precision on X-axis + filtered_df = df[ + (df["dataset"] == dataset) & + (df["type"] == "search") & + (df["parallel"] == parallel) + ] + + if filtered_df.empty: + return go.Figure().update_layout(title="No search data available"), [] + + fig = go.Figure() + legend_items = [] + + y_label = { + "rps": "Queries Per Second", + "mean_time": "Avg Latency (ms)", + "p95_time": "p95 Latency (ms)", + }.get(metric, metric) + + # ================================================================= + # Aggregate by ENGINE (not experiment) - combine all redis-* into "redis" + # Apply Pareto filtering to ALL data points (like their JS implementation) + # ================================================================= + for i, engine in enumerate(sorted(filtered_df["engine"].unique())): + engine_df = filtered_df[filtered_df["engine"] == engine].copy() + + # Apply Pareto filtering directly to ALL points for this engine + # This keeps the best trade-off points regardless of config + engine_df = filter_pareto_points(engine_df, metric) + engine_df = engine_df.sort_values("mean_precisions") + + # Get color based on engine name + color = get_engine_color(engine, i) + + fig.add_trace(go.Scatter( + x=engine_df["mean_precisions"], + y=engine_df[metric], + mode="lines+markers", + name=engine, + line=dict(color=color, width=2.5, shape="spline"), + marker=dict(size=8, color=color), + hovertemplate=( + f"{engine}
" + f"Precision: %{{x:.4f}}
" + f"{y_label}: %{{y:.2f}}
" + f"ef/numCandidates: %{{customdata[0]}}
" + f"Config: %{{customdata[1]}}" + ), + customdata=list(zip(engine_df["search_ef"], engine_df["experiment"])), + )) + + legend_items.append(html.Div([ + html.Span(className="legend-dot", style={"backgroundColor": color}), + html.Span(engine), + ], className="legend-item")) + + # Get y-axis max for the precision cutoff line + y_max = filtered_df[metric].max() * 1.1 + + # Add precision cutoff vertical line (like their red bar) + fig.add_trace(go.Bar( + name="Precision cutoff", + x=[precision_cutoff], + y=[y_max], + marker_color="rgba(255, 0, 0, 0.2)", + width=0.005, + hoverinfo="skip", + )) + + fig.update_layout( + showlegend=False, + plot_bgcolor="white", + paper_bgcolor="white", + barmode="overlay", + xaxis=dict( + title="Precision", + gridcolor="#f1f3f8", + range=[0.5, 1.0], + ), + yaxis=dict( + title=y_label, + gridcolor="#f1f3f8", + zeroline=False, + rangemode="tozero", + ), + margin=dict(l=60, r=20, t=20, b=40), + hovermode="closest", + ) + + return fig, legend_items + + +@callback( + Output("data-table-container", "children"), + Input("dataset-selector", "value"), + Input("parallel-selector", "value"), + Input("precision-slider", "value"), + Input("selected-metric", "data"), +) +def update_table(dataset, parallel, min_precision, metric): + """Update the data table based on precision slider.""" + if not dataset or metric == "upload_time": + return html.Div() + + filtered_df = df[ + (df["dataset"] == dataset) & + (df["type"] == "search") & + (df["parallel"] == parallel) & + (df["mean_precisions"] >= min_precision) + ] + + if filtered_df.empty: + return html.Div("No data above precision threshold", style={"color": "#9ca3af", "textAlign": "center", "padding": "20px"}) + + # Get best result per experiment (highest RPS) + idx = filtered_df.groupby("experiment")["rps"].idxmax() + best_df = filtered_df.loc[idx].sort_values("rps", ascending=False) + + rows = [] + for _, row in best_df.iterrows(): + rows.append(html.Tr([ + html.Td(row["experiment"]), + html.Td(f"{row['mean_precisions']:.4f}"), + html.Td(f"{row['rps']:.1f}"), + html.Td(f"{row['mean_time']:.2f} ms"), + html.Td(f"{row['p95_time']:.2f} ms"), + html.Td(f"ef={int(row['search_ef'])}"), + ])) + + return html.Table([ + html.Thead(html.Tr([ + html.Th("Experiment"), + html.Th("Precision"), + html.Th("RPS"), + html.Th("Avg Latency"), + html.Th("p95 Latency"), + html.Th("Config"), + ])), + html.Tbody(rows), + ], className="data-table") + + +if __name__ == "__main__": + app.run(debug=True, port=8055) + diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/__init__.py b/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/ann_compound_reader.py b/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/ann_compound_reader.py new file mode 100644 index 0000000..a2b0330 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/ann_compound_reader.py @@ -0,0 +1,39 @@ +import json +from typing import Iterator, List + +import numpy as np + +from dataset_reader.base_reader import Query +from dataset_reader.json_reader import JSONReader + + +class AnnCompoundReader(JSONReader): + """ + A reader created specifically to read the format used in + https://github.com/qdrant/ann-filtering-benchmark-datasets, in which vectors + and their metadata are stored in separate files. + """ + + VECTORS_FILE = "vectors.npy" + QUERIES_FILE = "tests.jsonl" + + def read_vectors(self) -> Iterator[List[float]]: + vectors = np.load(self.path / self.VECTORS_FILE) + for vector in vectors: + if self.normalize: + vector = vector / np.linalg.norm(vector) + yield vector.tolist() + + def read_queries(self) -> Iterator[Query]: + with open(self.path / self.QUERIES_FILE) as payloads_fp: + for idx, row in enumerate(payloads_fp): + row_json = json.loads(row) + vector = np.array(row_json["query"]) + if self.normalize: + vector /= np.linalg.norm(vector) + yield Query( + vector=vector.tolist(), + meta_conditions=row_json["conditions"], + expected_result=row_json["closest_ids"], + expected_scores=row_json["closest_scores"], + ) diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/ann_h5_multi_reader.py b/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/ann_h5_multi_reader.py new file mode 100644 index 0000000..64dfd7f --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/ann_h5_multi_reader.py @@ -0,0 +1,140 @@ +from typing import Iterator, List +import h5py +import numpy as np +import os +from benchmark import DATASETS_DIR +from dataset_reader.base_reader import BaseReader, Query, Record + + +class AnnH5MultiReader(BaseReader): + def __init__( + self, + data_files: str, + query_file: str, + normalize: bool = False, + skip_upload: bool = False, + skip_search: bool = False, + ): + """ + Args: + data_dir (str): Directory containing the HDF5 data files. + query_file (str): Path to the HDF5 query file. + normalize (bool): Whether to normalize the vectors. + """ + self.data_files = data_files + self.query_file = query_file + self.normalize = normalize + self.skip_upload = skip_upload + self.skip_search = skip_search + + # # Load the list of data files (assumes they're named in a consistent format) + # self.data_files = sorted( + # [ + # os.path.join(self.data_dir, f) + # for f in os.listdir(self.data_dir) + # if f.endswith(".hdf5") + # ] + # ) + + def read_queries(self) -> Iterator[Query]: + """Reads the queries from the query file.""" + with h5py.File(self.query_file, "r") as data: + for vector, expected_result, expected_scores in zip( + data["test"], data["neighbors"], data["distances"] + ): + if self.normalize: + vector /= np.linalg.norm(vector) + yield Query( + vector=vector.tolist(), + meta_conditions=None, + expected_result=expected_result.tolist(), + expected_scores=expected_scores.tolist(), + ) + + def read_data( + self, start_idx: int = 0, end_idx: int = None, chunk_size: int = 10_000, *args, **kwargs + ) -> Iterator[Record]: + """ + Reads the 'train' data vectors from multiple HDF5 files based on the specified range. + + Args: + start_idx (int): Start index for the range of vectors. + end_idx (int): End index for the range of vectors. + + Yields: + Record: A Record object for each vector in the specified range. + """ + if end_idx is None: + raise ValueError("You must specify an end index.") + + current_idx = start_idx + vectors_yielded = 0 + + for data_file in self.data_files: + # Extract the range of vectors covered by this file from the filename + file_start = data_file["start_idx"] + file_end = data_file["end_idx"] + path = data_file["path"] + + if current_idx >= end_idx: + break + + # Only read the file if it overlaps with the requested range + if file_start < end_idx and file_end > start_idx: + with h5py.File(path, "r") as data: + train_vectors = data["train"] + # Determine the slice to read from the current file + file_data_start = max(file_start, start_idx) - file_start + file_data_end = min(file_end, end_idx) - file_start + + # Read in chunks instead of the whole slice + for chunk_start in range( + file_data_start, file_data_end, chunk_size + ): + chunk_end = min(chunk_start + chunk_size, file_data_end) + vectors_chunk = train_vectors[chunk_start:chunk_end] + + for vector in vectors_chunk: + if self.normalize: + vector /= np.linalg.norm(vector) + yield Record( + id=current_idx + vectors_yielded, + vector=vector.tolist(), + metadata=None, + ) + vectors_yielded += 1 + + +if __name__ == "__main__": + # Directory containing the data split into multiple parts + data_dir = os.path.join(DATASETS_DIR, "laion-1b", "data") + + data_files = sorted( + [os.path.join(data_dir, f) for f in os.listdir(data_dir) if f.endswith(".hdf5")] + ) + + # Path to the query file + query_file = os.path.join( + DATASETS_DIR, "laion-1b", "laion-img-emb-768d-1Billion-cosine-queries.hdf5" + ) + + reader = AnnH5MultiReader(data_dir, query_file, normalize=False) + + # Example of reading queries and counting them + print("Reading queries...") + query_count = sum(1 for _ in reader.read_queries()) + print(f"Number of queries: {query_count}") + + # Example of reading vectors from 10M to 30M and asserting the length + start_idx = 15_000_000 + end_idx = 16_000_001 + print(f"Reading vectors from {start_idx} to {end_idx}...") + data_vectors = list(reader.read_data(start_idx, end_idx)) + + # Assert the length matches the expected range + expected_length = end_idx - start_idx + actual_length = len(data_vectors) + assert ( + actual_length == expected_length + ), f"Expected {expected_length} vectors, but got {actual_length}" + print(f"Successfully read {actual_length} vectors.") diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/ann_h5_reader.py b/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/ann_h5_reader.py new file mode 100644 index 0000000..950270c --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/ann_h5_reader.py @@ -0,0 +1,58 @@ +import itertools +from typing import Iterator + +import h5py +import numpy as np + +from benchmark import DATASETS_DIR +from dataset_reader.base_reader import BaseReader, Query, Record + + +class AnnH5Reader(BaseReader): + def __init__(self, path, normalize=False): + self.path = path + self.normalize = normalize + + def read_queries(self) -> Iterator[Query]: + data = h5py.File(self.path) + distances = data["distances"] if "distances" in data else itertools.repeat(None) + + for vector, expected_result, expected_scores in zip( + data["test"], data["neighbors"], distances + ): + if self.normalize: + vector /= np.linalg.norm(vector) + yield Query( + vector=vector.tolist(), + meta_conditions=None, + expected_result=expected_result.tolist(), + expected_scores=expected_scores.tolist() if expected_scores is not None else None, + ) + + def read_data(self, *args, **kwargs) -> Iterator[Record]: + data = h5py.File(self.path) + + for idx, vector in enumerate(data["train"]): + if self.normalize: + vector /= np.linalg.norm(vector) + yield Record(id=idx, vector=vector.tolist(), metadata=None) + + +if __name__ == "__main__": + import os + + # h5py file 4 keys: + # `train` - float vectors (num vectors 1183514) + # `test` - float vectors (num vectors 10000) + # `neighbors` - int - indices of nearest neighbors for test (num items 10k, each item + # contains info about 100 nearest neighbors) + # `distances` - float - distances for nearest neighbors for test vectors + + test_path = os.path.join( + DATASETS_DIR, "glove-100-angular", "glove-100-angular.hdf5" + ) + record = next(AnnH5Reader(test_path).read_data()) + print(record, end="\n\n") + + query = next(AnnH5Reader(test_path).read_queries()) + print(query) diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/base_reader.py b/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/base_reader.py new file mode 100644 index 0000000..e4c3ee6 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/base_reader.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass +from typing import Iterator, List, Optional + + +@dataclass +class Record: + id: int + vector: List[float] + metadata: Optional[dict] + + +@dataclass +class Query: + vector: List[float] + meta_conditions: Optional[dict] + expected_result: Optional[List[int]] + expected_scores: Optional[List[float]] = None + + +class BaseReader: + def read_data(self, *args, **kwargs) -> Iterator[Record]: + raise NotImplementedError() + + def read_queries(self) -> Iterator[Query]: + raise NotImplementedError() + + def prefetch(self, vector, *items) -> List: + raise NotImplementedError() diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/json_reader.py b/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/json_reader.py new file mode 100644 index 0000000..e6bab83 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/json_reader.py @@ -0,0 +1,78 @@ +import json +from pathlib import Path +from typing import Iterator, List, Optional + +import numpy as np + +from dataset_reader.base_reader import BaseReader, Query, Record + + +class JSONReader(BaseReader): + VECTORS_FILE = "vectors.jsonl" + PAYLOADS_FILE = "payloads.jsonl" + QUERIES_FILE = "queries.jsonl" + NEIGHBOURS_FILE = "neighbours.jsonl" + + def __init__(self, path: Path, normalize=False): + self.path = path + self.normalize = normalize + + def read_payloads(self) -> Iterator[dict]: + if not (self.path / self.PAYLOADS_FILE).exists(): + while True: + yield {} + with open(self.path / self.PAYLOADS_FILE, "r") as json_fp: + for json_line in json_fp: + line = json.loads(json_line) + yield line + + def read_vectors(self) -> Iterator[List[float]]: + with open(self.path / self.VECTORS_FILE, "r") as json_fp: + for json_line in json_fp: + vector = json.loads(json_line) + if self.normalize: + vector = vector / np.linalg.norm(vector) + yield vector + + def read_neighbours(self) -> Iterator[Optional[List[int]]]: + if not (self.path / self.NEIGHBOURS_FILE).exists(): + while True: + yield None + + with open(self.path / self.NEIGHBOURS_FILE, "r") as json_fp: + for json_line in json_fp: + line = json.loads(json_line) + yield line + + def read_query_vectors(self) -> Iterator[List[float]]: + with open(self.path / self.QUERIES_FILE, "r") as json_fp: + for json_line in json_fp: + vector = json.loads(json_line) + if self.normalize: + vector /= np.linalg.norm(vector) + yield vector + + def read_queries(self) -> Iterator[Query]: + for idx, (vector, neighbours) in enumerate( + zip(self.read_query_vectors(), self.read_neighbours()) + ): + # ToDo: add meta_conditions + + yield Query(vector=vector, meta_conditions=None, expected_result=neighbours) + + def read_data(self, *args, **kwargs) -> Iterator[Record]: + for idx, (vector, payload) in enumerate( + zip(self.read_vectors(), self.read_payloads()) + ): + yield Record(id=idx, vector=vector, metadata=payload) + + +if __name__ == "__main__": + from benchmark import DATASETS_DIR + + test_path = DATASETS_DIR / "random-100" + record = next(JSONReader(test_path).read_data()) + print(record, end="\n\n") + + query = next(JSONReader(test_path).read_queries()) + print(query) diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/splitter.py b/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/splitter.py new file mode 100644 index 0000000..6df6225 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/splitter.py @@ -0,0 +1,94 @@ +import os +import h5py +import numpy as np +from tqdm import tqdm +import argparse + +CHUNK_SIZE = 20000 # Number of records to process at a time + + +def split_hdf5_file( + input_path, data_output_dir, start_idx, end_idx, part, normalize=False +): + """ + Split a specified range of the 'train' dataset from the HDF5 file into a single file. + + Args: + input_path (str): Path to the input HDF5 file. + data_output_dir (str): Directory where the output file will be saved. + start_idx (int): Start index of the dataset. + end_idx (int): End index of the dataset. + part (int): Part number for the output file naming. + normalize (bool): Whether to normalize the dataset or not. + """ + with h5py.File(input_path, "r") as data_file: + train_shape = data_file["train"].shape + print(f"Processing train data part {part}: elements {start_idx} to {end_idx}") + + # Define the output path for this part + data_output_path = os.path.join( + data_output_dir, + f"laion-img-emb-768d-1Billion-cosine-data-part{part}-{start_idx}_to_{end_idx}.hdf5", + ) + + with h5py.File(data_output_path, "w") as data_output: + train_dset = data_output.create_dataset( + "train", + shape=(end_idx - start_idx, train_shape[1]), + dtype=data_file["train"].dtype, + ) + + # Create a progress bar for the data splitting process + with tqdm( + total=end_idx - start_idx, + unit="vectors", + desc=f"Processing train data part {part}", + ) as pbar: + for i in range(start_idx, end_idx, CHUNK_SIZE): + chunk_end = min(i + CHUNK_SIZE, end_idx) + train_dset[i - start_idx : chunk_end - start_idx] = data_file[ + "train" + ][i:chunk_end] + pbar.update(chunk_end - i) + + print( + f"Train data part {part} (elements {start_idx} to {end_idx}) saved to {data_output_path}" + ) + + +if __name__ == "__main__": + # Parse command-line arguments + parser = argparse.ArgumentParser( + description="Split HDF5 train dataset into specified parts." + ) + parser.add_argument( + "--input_path", + required=False, + type=str, + default="laion-img-emb-768d-1Billion-cosine.hdf5", + help="Path to the input HDF5 file", + ) + parser.add_argument( + "--data_output_dir", + type=str, + required=False, + default="data", + help="Directory where the split dataset will be saved", + ) + parser.add_argument( + "--start_idx", type=int, help="Start index for the dataset range to process" + ) + parser.add_argument( + "--end_idx", type=int, help="End index for the dataset range to process" + ) + parser.add_argument("--part", type=int, help="Part number for the output file") + + args = parser.parse_args() + + # Ensure the output directory exists + os.makedirs(args.data_output_dir, exist_ok=True) + + # Split the dataset into the specified range + split_hdf5_file( + args.input_path, args.data_output_dir, args.start_idx, args.end_idx, args.part + ) diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/verify.py b/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/verify.py new file mode 100644 index 0000000..8055569 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/dataset_reader/verify.py @@ -0,0 +1,53 @@ +import os +import h5py + +EXPECTED_VECTORS = 10_000_000 # Expected number of vectors per file + + +def verify_hdf5_files(directory): + """ + Verifies that each HDF5 file in the given directory contains 10 million vectors in the 'train' dataset. + + Args: + directory (str): Directory containing the HDF5 files to check. + """ + hdf_files = [f for f in os.listdir(directory) if f.endswith(".hdf5")] + + if not hdf_files: + print("No HDF5 files found in the directory.") + return + + all_verified = True + + for hdf_file in sorted(hdf_files): + file_path = os.path.join(directory, hdf_file) + + with h5py.File(file_path, "r") as data_file: + if "train" in data_file: + train_shape = data_file["train"].shape + num_vectors = train_shape[0] + print(f"Checking {hdf_file}: contains {num_vectors} vectors.") + + if num_vectors != EXPECTED_VECTORS: + print( + f"ERROR: {hdf_file} contains {num_vectors} vectors, expected {EXPECTED_VECTORS}." + ) + all_verified = False + else: + print(f"ERROR: 'train' dataset not found in {hdf_file}.") + all_verified = False + + if all_verified: + print( + "All HDF5 files verified successfully, each containing 10 million vectors." + ) + else: + print("Some files contain discrepancies. Please check the log above.") + + +if __name__ == "__main__": + # Define the path to the directory containing the HDF5 files + directory = "./data" # Replace with your actual directory + + # Verify the HDF5 files + verify_hdf5_files(directory) diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/docker-build.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/docker-build.sh new file mode 100755 index 0000000..c5834f1 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/docker-build.sh @@ -0,0 +1,176 @@ +#!/bin/bash + +# Docker build script for vector-db-benchmark +# This script builds the Docker image with proper Git information + +set -e + +# Default values +IMAGE_NAME="redis/vector-db-benchmark" +TAG="latest" +PLATFORM="" +PUSH=false + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to print colored output +print_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to show usage +usage() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " -n, --name NAME Docker image name (default: redis/vector-db-benchmark)" + echo " -t, --tag TAG Docker image tag (default: latest)" + echo " -p, --platform PLATFORM Target platform (e.g., linux/amd64,linux/arm64)" + echo " --push Push image to Docker Hub after building" + echo " -h, --help Show this help message" + echo "" + echo "Examples:" + echo " $0 # Build with defaults (latest tag)" + echo " $0 -t v1.0.0 --push # Build and push version tag" + echo " $0 -p linux/amd64,linux/arm64 --push # Multi-platform build and push" + echo "" + echo "Docker Hub Repository: redis/vector-db-benchmark" +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -n|--name) + IMAGE_NAME="$2" + shift 2 + ;; + -t|--tag) + TAG="$2" + shift 2 + ;; + -p|--platform) + PLATFORM="$2" + shift 2 + ;; + --push) + PUSH=true + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + print_error "Unknown option: $1" + usage + exit 1 + ;; + esac +done + +# Prepare for build +print_info "Preparing Docker build..." + +# Build Docker image +FULL_IMAGE_NAME="${IMAGE_NAME}:${TAG}" +print_info "Building Docker image: $FULL_IMAGE_NAME" + +# Prepare build command +if [[ -n "$PLATFORM" ]]; then + # Multi-platform build requires buildx + print_info "Target platform(s): $PLATFORM" + print_info "Setting up Docker Buildx for multi-platform build..." + + # Create buildx builder if it doesn't exist + if ! docker buildx ls | grep -q "multiplatform"; then + print_info "Creating multiplatform builder..." + docker buildx create --name multiplatform --use --bootstrap + else + print_info "Using existing multiplatform builder..." + docker buildx use multiplatform + fi + + BUILD_CMD="docker buildx build --platform $PLATFORM" + if [[ "$PUSH" == "true" ]]; then + BUILD_CMD="$BUILD_CMD --push" + else + BUILD_CMD="$BUILD_CMD --load" + print_warning "Multi-platform builds without --push will only load the native platform image" + fi +else + # Single platform build uses regular docker build + BUILD_CMD="docker build" +fi + +# Add tags +BUILD_CMD="$BUILD_CMD -t $FULL_IMAGE_NAME ." + +print_info "Executing: $BUILD_CMD" + +# Execute build +if eval $BUILD_CMD; then + print_info "✅ Docker image built successfully: $FULL_IMAGE_NAME" + + # Show image size (only for single platform builds or when image is loaded locally) + if [[ -z "$PLATFORM" ]] || [[ "$PUSH" != "true" ]]; then + IMAGE_SIZE=$(docker images --format "table {{.Size}}" $FULL_IMAGE_NAME 2>/dev/null | tail -n 1) + if [[ -n "$IMAGE_SIZE" && "$IMAGE_SIZE" != "SIZE" ]]; then + print_info "Image size: $IMAGE_SIZE" + fi + fi + + # Handle push for single platform builds (multi-platform builds push automatically with buildx) + if [[ "$PUSH" == "true" && -z "$PLATFORM" ]]; then + print_info "🚀 Pushing image to Docker Hub..." + + # Check if logged in to Docker Hub + if ! docker info | grep -q "Username:"; then + print_warning "Not logged in to Docker Hub. Please run: docker login" + print_info "Or set DOCKER_USERNAME and DOCKER_PASSWORD environment variables" + + if [[ -n "$DOCKER_USERNAME" && -n "$DOCKER_PASSWORD" ]]; then + print_info "Using environment variables for Docker login..." + echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin + else + print_error "❌ Docker Hub authentication required" + exit 1 + fi + fi + + # Push the image + if docker push $FULL_IMAGE_NAME; then + print_info "✅ Image pushed successfully to Docker Hub: $FULL_IMAGE_NAME" + else + print_error "❌ Failed to push image to Docker Hub" + exit 1 + fi + elif [[ "$PUSH" == "true" && -n "$PLATFORM" ]]; then + print_info "✅ Multi-platform image pushed successfully to Docker Hub: $FULL_IMAGE_NAME" + fi + + echo "" + print_info "To run the container:" + echo " docker run --rm $FULL_IMAGE_NAME run.py --help" + echo "" + print_info "To run with Redis connection:" + echo " docker run --rm --network=host $FULL_IMAGE_NAME run.py --host localhost --engines redis" + echo "" + if [[ "$PUSH" == "true" ]]; then + print_info "Image available on Docker Hub: https://hub.docker.com/r/redis/vector-db-benchmark" + fi +else + print_error "❌ Docker build failed" + exit 1 +fi diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/docker-run.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/docker-run.sh new file mode 100755 index 0000000..920fb8a --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/docker-run.sh @@ -0,0 +1,163 @@ +#!/bin/bash + +# Docker run script for vector-db-benchmark +# This script provides convenient ways to run the benchmark in Docker + +set -e + +# Default values +IMAGE_NAME="redis/vector-db-benchmark:latest" +REDIS_HOST="localhost" +REDIS_PORT="6379" +ENGINES="redis" +DATASET="random-100" +EXPERIMENT="redis-default-simple" +NETWORK="" +EXTRA_ARGS="" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_example() { + echo -e "${BLUE}[EXAMPLE]${NC} $1" +} + +# Function to show usage +usage() { + echo "Usage: $0 [OPTIONS] [-- EXTRA_ARGS]" + echo "" + echo "Options:" + echo " -i, --image IMAGE Docker image name (default: redis/vector-db-benchmark:latest)" + echo " -H, --host HOST Redis host (default: localhost)" + echo " -p, --port PORT Redis port (default: 6379)" + echo " -e, --engines ENGINES Engines to test (default: redis)" + echo " -d, --dataset DATASET Dataset to use (default: random-100)" + echo " -x, --experiment EXP Experiment configuration (default: redis-default-simple)" + echo " -n, --network NET Docker network to use" + echo " -h, --help Show this help message" + echo "" + echo "Examples:" + print_example "$0 # Run with defaults (help)" + print_example "$0 -H redis -e redis -d random-100 -x redis-default-simple # Basic Redis benchmark" + print_example "$0 -n redis-net -H redis-server # Use custom network" + print_example "$0 -- --skip-upload --skip-search # Pass extra arguments" + echo "" + echo "Common Redis setups:" + print_example "# Start Redis container first:" + print_example "docker run -d --name redis-test -p 6379:6379 redis:8.2-rc1-bookworm" + echo "" + print_example "# Then run benchmark:" + print_example "$0 -H localhost -e redis -d random-100" + echo "" + print_example "# With results output (mount current directory):" + print_example "docker run --rm -v \$(pwd)/results:/app/results --network host redis/vector-db-benchmark:latest run.py --host localhost --engines redis" +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -i|--image) + IMAGE_NAME="$2" + shift 2 + ;; + -H|--host) + REDIS_HOST="$2" + shift 2 + ;; + -p|--port) + REDIS_PORT="$2" + shift 2 + ;; + -e|--engines) + ENGINES="$2" + shift 2 + ;; + -d|--dataset) + DATASET="$2" + shift 2 + ;; + -x|--experiment) + EXPERIMENT="$2" + shift 2 + ;; + -n|--network) + NETWORK="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + --) + shift + EXTRA_ARGS="$*" + break + ;; + *) + print_error "Unknown option: $1" + usage + exit 1 + ;; + esac +done + +# Build Docker run command +DOCKER_CMD="docker run --rm -it" + +# Add network if specified +if [[ -n "$NETWORK" ]]; then + DOCKER_CMD="$DOCKER_CMD --network $NETWORK" + print_info "Using Docker network: $NETWORK" +fi + +# Mount results directory +DOCKER_CMD="$DOCKER_CMD -v \$(pwd)/results:/app/results" + +# Add image +DOCKER_CMD="$DOCKER_CMD $IMAGE_NAME" + +# If no extra args provided, show help +if [[ -z "$EXTRA_ARGS" && "$ENGINES" == "redis" && "$DATASET" == "random-100" && "$EXPERIMENT" == "redis-default-simple" && "$REDIS_HOST" == "localhost" ]]; then + print_info "No specific configuration provided, showing help:" + DOCKER_CMD="$DOCKER_CMD run.py --help" +else + # Add benchmark arguments + DOCKER_CMD="$DOCKER_CMD run.py --host $REDIS_HOST --engines $ENGINES --dataset $DATASET --experiment $EXPERIMENT" + + # Add extra arguments if provided + if [[ -n "$EXTRA_ARGS" ]]; then + DOCKER_CMD="$DOCKER_CMD $EXTRA_ARGS" + fi + + print_info "Configuration:" + print_info " Redis: $REDIS_HOST:$REDIS_PORT" + print_info " Engines: $ENGINES" + print_info " Dataset: $DATASET" + print_info " Experiment: $EXPERIMENT" + if [[ -n "$EXTRA_ARGS" ]]; then + print_info " Extra args: $EXTRA_ARGS" + fi +fi + +print_info "Executing: $DOCKER_CMD" +echo "" + +# Execute the command +eval $DOCKER_CMD diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/docker-test.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/docker-test.sh new file mode 100755 index 0000000..db57196 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/docker-test.sh @@ -0,0 +1,160 @@ +#!/bin/bash + +# Docker test script for local validation +# This script mimics the GitHub Action validation locally + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_step() { + echo -e "${BLUE}[STEP]${NC} $1" +} + +# Configuration +IMAGE_NAME="vector-db-benchmark-test" +TAG="local-test" +FULL_IMAGE_NAME="${IMAGE_NAME}:${TAG}" + +print_info "Starting Docker validation tests..." + +# Step 0: Check Docker Hub credentials (optional for local testing) +print_step "Checking Docker Hub credentials..." +if [[ -n "$DOCKER_USERNAME" && -n "$DOCKER_PASSWORD" ]]; then + print_info "✅ Docker Hub credentials found in environment" +elif docker info | grep -q "Username:"; then + print_info "✅ Already logged in to Docker Hub" +else + print_warning "⚠️ Docker Hub credentials not found" + print_info "Set DOCKER_USERNAME and DOCKER_PASSWORD environment variables or run 'docker login' for publishing" +fi + +# Step 1: Build the image +print_step "Building Docker image..." +if ./docker-build.sh -n "$IMAGE_NAME" -t "$TAG"; then + print_info "✅ Docker build successful" +else + print_error "❌ Docker build failed" + exit 1 +fi + +# Step 2: Test basic functionality +print_step "Testing basic functionality..." + +# Test help command +print_info "Testing --help command..." +if docker run --rm "$FULL_IMAGE_NAME" run.py --help > /dev/null; then + print_info "✅ Help command works" +else + print_error "❌ Help command failed" + exit 1 +fi + +# Test Python environment +print_info "Testing Python environment..." +if docker run --rm "$FULL_IMAGE_NAME" -c "import sys; print(f'Python {sys.version}'); import redis; print('Redis module available')" > /dev/null; then + print_info "✅ Python environment works" +else + print_error "❌ Python environment test failed" + exit 1 +fi + +# Step 3: Test with Redis using Docker +print_step "Testing Redis connectivity and benchmark execution..." +print_info "Starting Redis container for testing..." + +# Start Redis container +REDIS_CONTAINER_NAME="vector-benchmark-test-redis" +if docker run -d --name "$REDIS_CONTAINER_NAME" -p 6379:6379 redis:8.2-rc1-bookworm > /dev/null 2>&1; then + print_info "Redis container started successfully" + + # Wait for Redis to be ready + print_info "Waiting for Redis to be ready..." + sleep 5 + + # Test basic connection + if timeout 10 docker run --rm --network=host "$FULL_IMAGE_NAME" \ + -c "import redis; r = redis.Redis(host='localhost', port=6379); r.ping(); print('Redis connection successful')" > /dev/null 2>&1; then + print_info "✅ Redis connectivity test passed" + + # Test benchmark execution with specific configuration + print_info "Testing benchmark execution with redis-default-simple configuration..." + if timeout 120 docker run --rm --network=host -v "$(pwd)/results:/app/results" "$FULL_IMAGE_NAME" \ + run.py --host localhost --engines redis --dataset random-100 --experiment redis-default-simple > /dev/null 2>&1; then + print_info "✅ Benchmark execution test passed" + else + print_warning "⚠️ Benchmark execution test failed (this may be expected without proper dataset setup)" + fi + else + print_warning "⚠️ Redis connectivity test failed" + fi + + # Clean up Redis container + print_info "Stopping and removing Redis test container..." + docker stop "$REDIS_CONTAINER_NAME" > /dev/null 2>&1 + docker rm "$REDIS_CONTAINER_NAME" > /dev/null 2>&1 +else + print_warning "⚠️ Failed to start Redis container, skipping connectivity test" +fi + +# Step 4: Test file output permissions +print_step "Testing file output permissions..." +TEMP_DIR=$(mktemp -d) +if docker run --rm -v "$TEMP_DIR:/app/results" "$FULL_IMAGE_NAME" \ + -c "import os; os.makedirs('/app/results', exist_ok=True); open('/app/results/test.txt', 'w').write('test'); print('File write successful')" > /dev/null 2>&1; then + if [ -f "$TEMP_DIR/test.txt" ]; then + print_info "✅ File output test passed" + else + print_warning "⚠️ Test file not created" + fi +else + print_warning "⚠️ File output test completed with warnings" +fi +rm -rf "$TEMP_DIR" + +# Step 5: Test image size +print_step "Checking image size..." +IMAGE_SIZE=$(docker images --format "table {{.Size}}" "$FULL_IMAGE_NAME" | tail -n 1) +print_info "Image size: $IMAGE_SIZE" + +# Step 6: Test benchmark configuration loading +print_step "Testing benchmark configuration loading..." +if docker run --rm "$FULL_IMAGE_NAME" \ + -c "import json; import os; print('Configuration loading test'); print(os.listdir('/app'))" > /dev/null 2>&1; then + print_info "✅ Configuration loading test passed" +else + print_warning "⚠️ Configuration loading test completed with warnings" +fi + +# Step 7: Clean up +print_step "Cleaning up..." +docker rmi "$FULL_IMAGE_NAME" > /dev/null 2>&1 || true + +print_info "🎉 All Docker validation tests completed successfully!" +print_info "" +print_info "Summary:" +print_info " ✅ Docker build successful" +print_info " ✅ Basic functionality tests passed" +print_info " ✅ Redis container connectivity tested" +print_info " ✅ Benchmark execution tested" +print_info " ✅ Image size: $IMAGE_SIZE" +print_info "" +print_info "The Docker setup is ready for production use!" diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/docs/dataset-validation.md b/02-semantic-search-rag/fcenedes/vecdb_bench/docs/dataset-validation.md new file mode 100644 index 0000000..fa17fca --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/docs/dataset-validation.md @@ -0,0 +1,128 @@ +# Dataset Validation + +This document describes the dataset validation system for the vector-db-benchmark project. + +## Overview + +The validation system ensures that: +- All datasets in `datasets/datasets.json` have the required fields +- Field types and values are correct and reasonable +- Dataset names are unique +- The `--describe` functionality works correctly + +## Validation Components + +### 1. Local Validation Script + +**File**: `validate_datasets.py` + +Run locally to validate datasets: + +```bash +# Basic validation +python validate_datasets.py + +# Strict mode (treat warnings as errors) +python validate_datasets.py --strict +``` + +**What it checks:** +- JSON structure and syntax +- Required fields: `name`, `vector_size`, `distance`, `type`, `path`, `vector_count`, `description` +- Field types (handles special cases like `h5-multi` datasets with dict paths) +- Data consistency (positive vector sizes/counts, valid distance metrics) +- Unique dataset names +- `--describe datasets` and `--describe engines` functionality + +### 2. GitHub Action + +**File**: `.github/workflows/validate-datasets.yml` + +Automatically runs on: +- Push to files: `datasets/datasets.json`, `run.py`, `benchmark/dataset.py` +- Pull requests affecting the same files + +The action simply runs the validation script: +```yaml +- name: Validate datasets.json + run: python validate_datasets.py +``` + +## Required Dataset Fields + +Each dataset in `datasets/datasets.json` must have: + +| Field | Type | Description | Example | +|-------|------|-------------|---------| +| `name` | string | Unique dataset identifier | `"glove-100-angular"` | +| `vector_size` | integer | Vector dimensions | `100` | +| `distance` | string | Distance metric | `"cosine"`, `"l2"`, `"dot"`, `"euclidean"` | +| `type` | string | Dataset format | `"h5"`, `"tar"`, `"jsonl"`, `"h5-multi"` | +| `path` | string/dict | File path or multi-file structure | `"glove-100/file.hdf5"` | +| `vector_count` | integer/null | Number of vectors | `1183514` or `null` | +| `description` | string/null | Human-readable description | `"Word vectors"` or `null` | + +### Optional Fields + +| Field | Type | Description | +|-------|------|-------------| +| `link` | string | Download URL | +| `schema` | dict | Additional metadata fields | + +## Special Cases + +### Multi-file Datasets (h5-multi) + +For large datasets split across multiple files, the `path` field can be a dictionary: + +```json +{ + "name": "laion-img-emb-768d-1Billion-cosine", + "type": "h5-multi", + "path": { + "data": [ + { + "file_number": "1", + "path": "laion-1b/part1.hdf5", + "link": "http://example.com/part1.hdf5", + "start_idx": 0, + "end_idx": 10000000 + } + ], + "queries": [ + { + "path": "laion-1b/queries.hdf5", + "link": "http://example.com/queries.hdf5" + } + ] + } +} +``` + +## Validation Warnings + +The validator may show warnings for: +- **Round vector counts**: Numbers like 1,000,000 that look like estimates +- **Missing descriptions**: Datasets with `null` descriptions +- **Missing download links**: Non-local datasets without download URLs +- **Large vector sizes**: Dimensions > 4096 (flagged for verification) + +Warnings don't fail validation but should be reviewed. + +## Adding New Datasets + +1. Add your dataset to `datasets/datasets.json` +2. Run `python validate_datasets.py` locally +3. Fix any errors or warnings +4. Commit and push (GitHub Action will validate automatically) + +## Testing Describe Functionality + +The validation includes testing the `--describe` commands: + +```bash +python run.py --describe datasets +python run.py --describe engines +``` + +This ensures the new dataset display functionality works correctly. diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/__init__.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/__init__.py new file mode 100644 index 0000000..a549555 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/__init__.py @@ -0,0 +1,8 @@ +from engine.base_client.client import BaseClient +from engine.base_client.configure import BaseConfigurator +from engine.base_client.search import BaseSearcher +from engine.base_client.upload import BaseUploader + + +class IncompatibilityError(Exception): + pass diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/client.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/client.py new file mode 100644 index 0000000..970ed2c --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/client.py @@ -0,0 +1,507 @@ +import json +import os +from datetime import datetime +from pathlib import Path +from typing import List, Dict, Any, Optional +import warnings + +from benchmark import ROOT_DIR +from benchmark.dataset import Dataset +from dataset_reader.base_reader import BaseReader +from engine.base_client.configure import BaseConfigurator +from engine.base_client.distances import Distance +from engine.base_client.search import BaseSearcher +from engine.base_client.upload import BaseUploader + +RESULTS_DIR = ROOT_DIR / "results" +RESULTS_DIR.mkdir(exist_ok=True) + +DETAILED_RESULTS = bool(int(os.getenv("DETAILED_RESULTS", False))) +REPETITIONS = int(os.getenv("REPETITIONS", 3)) + + + +def format_precision_key(precision_value: float) -> str: + """Format precision value according to the rule: 0.01 increments up to 0.97, then 0.0025 increments from 0.97 to 1.0""" + if precision_value <= 0.97: + # Round to nearest 0.01 for values up to 0.97 + rounded = round(precision_value, 2) + return f"{rounded:.2f}" + else: + # Round to nearest 0.0025 for values from 0.97 to 1.0 + # 0.0025 = 1/400, so multiply by 400, round, then divide by 400 + rounded = round(precision_value * 400) / 400 + return f"{rounded:.4f}" + + +def analyze_precision_performance(search_results: Dict[str, Any]) -> tuple[Dict[str, Dict[str, Any]], Dict[str, Dict[str, float]]]: + """Analyze search results to find best RPS at each actual precision level achieved. + + Returns: + tuple: (precision_dict, precision_summary_dict) + - precision_dict: Full precision analysis with config details + - precision_summary_dict: Simplified summary with just QPS, P50, P95 + """ + precision_dict = {} + precision_summary_dict = {} + + # First, collect all actual precision levels achieved by experiments and format them + precision_mapping = {} # Maps formatted precision to actual precision + for experiment_data in search_results.values(): + mean_precision = experiment_data["results"]["mean_precisions"] + formatted_precision = format_precision_key(mean_precision) + + # Keep track of the best (highest) actual precision for each formatted precision + if formatted_precision not in precision_mapping or mean_precision > precision_mapping[formatted_precision]: + precision_mapping[formatted_precision] = mean_precision + + # For each formatted precision level, find the best RPS among experiments that round to this level + for formatted_precision in precision_mapping.keys(): + best_rps = 0 + best_config = None + best_experiment_id = None + best_p50_time = 0 + best_p95_time = 0 + + for experiment_id, experiment_data in search_results.items(): + mean_precision = experiment_data["results"]["mean_precisions"] + rps = experiment_data["results"]["rps"] + + # Check if this experiment's precision rounds to the current formatted precision + if format_precision_key(mean_precision) == formatted_precision and rps > best_rps: + best_rps = rps + # ================================================================= + # FIX: Handle different param structures across engines + # Redis uses: params.search_params.ef + # MongoDB uses: params.numCandidates (no search_params wrapper) + # To revert: Remove the .get() fallback and use direct access + # ================================================================= + search_params = experiment_data["params"].get("search_params", {}) + if not search_params: + # For engines like MongoDB that don't use search_params wrapper, + # extract all non-standard params as the search config + search_params = { + k: v for k, v in experiment_data["params"].items() + if k not in ("dataset", "experiment", "engine", "parallel") + } + best_config = { + "parallel": experiment_data["params"]["parallel"], + "search_params": search_params + } + best_experiment_id = experiment_id + best_p50_time = experiment_data["results"]["p50_time"] + best_p95_time = experiment_data["results"]["p95_time"] + + # Add to precision dict with the formatted precision as key + if best_config is not None: + # Full precision analysis (existing format) + precision_dict[formatted_precision] = { + "rps": best_rps, + "config": best_config, + "experiment_id": best_experiment_id + } + + # Simplified precision summary + precision_summary_dict[formatted_precision] = { + "qps": round(best_rps, 1), + "p50": round(best_p50_time * 1000, 3), # Convert to ms + "p95": round(best_p95_time * 1000, 3) # Convert to ms + } + + return precision_dict, precision_summary_dict + +warnings.filterwarnings("ignore", category=DeprecationWarning) + + +class BaseClient: + def __init__( + self, + name: str, # name of the experiment + engine: str, # name of the engine + configurator: BaseConfigurator, + uploader: BaseUploader, + searchers: List[BaseSearcher], + ): + self.name = name + self.configurator = configurator + self.uploader = uploader + self.searchers = searchers + self.engine = engine + + def save_search_results( + self, dataset_name: str, results: dict, search_id: int, search_params: dict + ): + now = datetime.now() + timestamp = now.strftime("%Y-%m-%d-%H-%M-%S") + pid = os.getpid() # Get the current process ID + experiment_id = f"{self.name}-{dataset_name}-search-{search_id}-{pid}-{timestamp}" + experiments_file = ( + f"{experiment_id}.json" + ) + experiment_result = { + "params": { + "dataset": dataset_name, + "experiment": self.name, + "engine": self.engine, + **search_params, + }, + "results": results, + } + result_path = RESULTS_DIR / experiments_file + with open(result_path, "w") as out: + out.write( + json.dumps( + experiment_result, + indent=2, + ) + ) + return result_path,experiment_id,experiment_result + + def save_upload_results( + self, dataset_name: str, results: dict, upload_params: dict,upload_start_idx:int,upload_end_idx:int, + ): + now = datetime.now() + timestamp = now.strftime("%Y-%m-%d-%H-%M-%S") + experiments_file = f"{self.name}-{dataset_name}-upload-{upload_start_idx}-{upload_end_idx}-{timestamp}.json" + with open(RESULTS_DIR / experiments_file, "w") as out: + upload_stats = { + "params": { + "experiment": self.name, + "engine": self.engine, + "dataset": dataset_name, + "start_idx": upload_start_idx, + "end_idx": upload_end_idx, + **upload_params, + }, + "results": results, + } + out.write(json.dumps(upload_stats, indent=2)) + + def run_experiment( + self, + dataset: Dataset, + skip_upload: bool = False, + skip_search: bool = False, + skip_if_exists: bool = True, + parallels: [int] = [], + upload_start_idx: int = 0, + upload_end_idx: int = -1, + num_queries: int = -1, + ef_runtime: List[int] = [], + ): + results = {"upload": {}, "search": {}} + execution_params = self.configurator.execution_params( + distance=dataset.config.distance, vector_size=dataset.config.vector_size + ) + reader = dataset.get_reader(execution_params.get("normalize", False)) + + if skip_if_exists: + pid = os.getpid() # Get the current process ID + glob_pattern = f"{self.name}-{dataset.config.name}-search-*-{pid}-*.json" + existing_results = list(RESULTS_DIR.glob(glob_pattern)) + if len(existing_results) == len(self.searchers): + print( + f"Skipping run for {self.name} since it already ran {len(self.searchers)} search configs previously" + ) + return + + if not skip_upload: + print("Experiment stage: Configure") + self.configurator.configure(dataset) + range_max_str = ":" + if upload_end_idx > 0: + range_max_str += f"{upload_end_idx}" + print(f"Experiment stage: Upload. Vector range [{upload_start_idx}{range_max_str}]") + upload_stats = self.uploader.upload( + distance=dataset.config.distance, records=reader.read_data(upload_start_idx,upload_end_idx) + ) + + if not DETAILED_RESULTS: + # Remove verbose stats from upload results + upload_stats.pop("latencies", None) + + self.save_upload_results( + dataset.config.name, + upload_stats, + upload_params={ + **self.uploader.upload_params, + **self.configurator.collection_params, + }, + upload_start_idx=upload_start_idx, + upload_end_idx=upload_end_idx, + ) + + if not skip_search: + print("Experiment stage: Search") + for search_id, searcher in enumerate(self.searchers): + if skip_if_exists: + pid = os.getpid() # Get the current process ID + glob_pattern = ( + f"{self.name}-{dataset.config.name}-search-{search_id}-{pid}-*.json" + ) + existing_results = list(RESULTS_DIR.glob(glob_pattern)) + print("Pattern", glob_pattern, "Results:", existing_results) + if len(existing_results) >= 1: + print( + f"Skipping search {search_id} as it already exists", + ) + continue + + search_params = {**searcher.search_params} + ef = "default" + if "search_params" in search_params: + ef = search_params["search_params"].get("ef", "default") + client_count = search_params.get("parallel", 1) + + # Filter by client count if parallels is specified + filter_client_count = len(parallels) > 0 + if filter_client_count and (client_count not in parallels): + print(f"\tSkipping ef runtime: {ef}; #clients {client_count}") + continue + + # Filter by ef runtime if ef_runtime is specified + filter_ef_runtime = len(ef_runtime) > 0 + if filter_ef_runtime and isinstance(ef, int) and (ef not in ef_runtime): + print(f"\tSkipping ef runtime: {ef}; #clients {client_count} (not in ef_runtime filter)") + continue + + if (precision := search_params.get("calibration_precision", None)) is not None: + top = search_params["top"] + calibration_param = search_params["calibration_param"] + calibration_value, calibration_precision = calibrate( + searcher, + calibration_param, + top, + precision, + dataset.config.distance, + reader, + ) + print( + f"Calibrated {top=} {precision=} {calibration_value=} {calibration_precision=!s}" + ) + searcher.search_params["search_params"][calibration_param] = calibration_value + + for repetition in range(1, REPETITIONS + 1): + print( + f"\tRunning repetition {repetition} ef runtime: {ef}; #clients {client_count}" + ) + + search_stats = searcher.search_all( + dataset.config.distance, reader.read_queries(), num_queries + ) + # ensure we specify the client count in the results + search_params["parallel"] = client_count + if not DETAILED_RESULTS: + # Remove verbose stats from search results + search_stats.pop("latencies", None) + search_stats.pop("precisions", None) + + result_path,experiment_id,experiment_result = self.save_search_results( + dataset.config.name, search_stats, search_id, search_params + ) + results["search"][experiment_id] = experiment_result + + # Print single line summary with QPS, P50, and P95 latency + qps = round(search_stats.get("rps", 0),1) + p50_latency = round(search_stats.get("p50_time", 0) * 1000,3) # Convert to ms + p95_latency = round(search_stats.get("p95_time", 0) * 1000,3) # Convert to ms + precision = search_stats.get("mean_precisions", 0) + print( + f"\t→ QPS: {qps:.1f}, P50: {p50_latency:.2f}ms, P95: {p95_latency:.2f}ms, Precision: {precision:.4f}" + ) + + print( + f"\tSaved {experiment_id} in {result_path}" + ) + + print("Experiment stage: Done") + + # Add precision analysis if search results exist + if results["search"]: + precision_analysis, precision_summary = analyze_precision_performance(results["search"]) + if precision_analysis: # Only add if we have precision data + results["precision"] = precision_analysis + results["precision_summary"] = precision_summary + print(f"Added precision analysis with {len(precision_analysis)} precision thresholds") + print(f"Added precision summary with {len(precision_summary)} precision levels") + + # Display results table and chart + self._display_results_summary(precision_summary, dataset.config.name) + + summary_file = f"{self.name}-{dataset.config.name}-summary.json" + summary_path = RESULTS_DIR / summary_file + with open(summary_path, "w") as out: + out.write( + json.dumps( + results, + indent=2, + ) + ) + print("Results saved to: ", RESULTS_DIR) + print("Summary saved to: ", summary_path) + + def _display_results_summary(self, precision_summary: Dict[str, Dict[str, float]], dataset_name: str = ""): + """Display results table and ASCII chart after benchmark completion.""" + if not precision_summary: + return + + print("\n" + "="*80) + print("BENCHMARK RESULTS SUMMARY") + if dataset_name: + print(f"Experiment: {self.name} - {dataset_name}") + print("="*80) + + # Results table + print("\nPrecision vs Performance Trade-off:") + print("-" * 50) + print(f"{'Precision':<10} {'QPS':<8} {'P50 (ms)':<10} {'P95 (ms)':<10}") + print("-" * 50) + + # Sort by precision (descending for better readability) + sorted_data = sorted(precision_summary.items(), key=lambda x: float(x[0]), reverse=True) + + for precision, metrics in sorted_data: + qps = metrics.get('qps', 0) + p50 = metrics.get('p50', 0) + p95 = metrics.get('p95', 0) + print(f"{precision:<10} {qps:<8.1f} {p50:<10.3f} {p95:<10.3f}") + + # ASCII scatter plot + print(f"\n{self._create_ascii_scatter_plot(precision_summary, dataset_name)}") + print("="*80 + "\n") + + def _create_ascii_scatter_plot(self, precision_summary: Dict[str, Dict[str, float]], dataset_name: str = "") -> str: + """Create simple ASCII scatter plot with precision on X-axis and QPS on Y-axis.""" + if not precision_summary: + return "No data available for chart." + + # Sort by precision + sorted_data = sorted(precision_summary.items(), key=lambda x: float(x[0])) + + # Extract data + precisions = [float(data[0]) for data in sorted_data] + qps_values = [data[1].get('qps', 0) for data in sorted_data] + + if not precisions or not qps_values: + return "No valid data for chart." + + # Chart dimensions + width = 60 + height = 12 + + # Calculate scales + min_precision = min(precisions) + max_precision = max(precisions) + min_qps = 0 # Always start from 0 for proper perspective + max_qps = max(qps_values) + + precision_range = max_precision - min_precision + qps_range = max_qps - min_qps + + if precision_range == 0: + precision_range = 0.1 + if qps_range == 0: + qps_range = 100 + + # Create grid + grid = [[' ' for _ in range(width)] for _ in range(height)] + + # Plot points + for precision, qps in zip(precisions, qps_values): + # Convert to grid coordinates + x = int((precision - min_precision) / precision_range * (width - 1)) + y = int((max_qps - qps) / qps_range * (height - 1)) # Flip Y axis + + # Ensure within bounds + x = max(0, min(width - 1, x)) + y = max(0, min(height - 1, y)) + + grid[y][x] = '●' + + # Build chart + experiment_info = f" - {self.name}" + if dataset_name: + experiment_info += f" - {dataset_name}" + chart = f"QPS vs Precision Trade-off{experiment_info} (up and to the right is better):\n\n" + + # Y-axis labels and grid + for i, row in enumerate(grid): + if i % 3 == 0: # Show Y labels every 3 rows + qps_val = max_qps - (i / (height - 1)) * qps_range + chart += f"{qps_val:>6.0f} │" + else: + chart += " │" + + chart += "".join(row) + "\n" + + # Add 0 line at the bottom + chart += f" 0 │" + " " * width + "\n" + + # X-axis + chart += " └" + "─" * width + "\n" + chart += " " + + # X-axis labels + for i in range(0, width, 15): # Show X labels every 15 chars + precision_val = min_precision + (i / (width - 1)) * precision_range + chart += f"{precision_val:.3f}".ljust(15) + + chart += "\n Precision (0.0 = 0%, 1.0 = 100%)" + + return chart + + def delete_client(self): + self.uploader.delete_client() + self.configurator.delete_client() + + for s in self.searchers: + s.delete_client() + +def calibrate( + searcher: BaseSearcher, + calibration_param: str, + min_value: int, + precision: float, + distance: Distance, + reader: BaseReader, + max_value: int = 1000, +) -> tuple[int, float]: + """Calibrate searcher for a given precision.""" + if min_value > max_value: + raise ValueError( + f"{min_value=} cannot be greater than {max_value=}" + ) + lower_bound = min_value + upper_bound = max_value + lower_bound_visited = False + upper_bound_visited = False + current = (lower_bound + upper_bound) // 2 + previous = current + current_precision = 0 + while True: + searcher.search_params["search_params"][calibration_param] = current + search_stats = searcher.search_all(distance, reader.read_queries()) + previous_precision = current_precision + current_precision = search_stats["mean_precisions"] + if current_precision == precision: + return current, current_precision + elif current_precision > precision: + upper_bound = current + upper_bound_visited = True + else: + lower_bound = current + lower_bound_visited = True + next_value = (lower_bound + upper_bound) // 2 + if ( + (lower_bound_visited and next_value == lower_bound) + or (upper_bound_visited and next_value == upper_bound) + ): + if abs(previous_precision - precision) < abs(current_precision - precision): + final_precision = previous_precision + final_value = previous + else: + final_precision = current_precision + final_value = current + return final_value, final_precision + previous = current + current = next_value diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/configure.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/configure.py new file mode 100644 index 0000000..1a4aaae --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/configure.py @@ -0,0 +1,28 @@ +from typing import Optional + +from benchmark.dataset import Dataset + + +class BaseConfigurator: + DISTANCE_MAPPING = {} + + def __init__(self, host, collection_params: dict, connection_params: dict): + self.host = host + self.collection_params = collection_params + self.connection_params = connection_params + + def clean(self): + raise NotImplementedError() + + def recreate(self, dataset: Dataset, collection_params): + raise NotImplementedError() + + def configure(self, dataset: Dataset) -> Optional[dict]: + self.clean() + return self.recreate(dataset, self.collection_params) or {} + + def execution_params(self, distance, vector_size) -> dict: + return {} + + def delete_client(self): + pass diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/distances.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/distances.py new file mode 100644 index 0000000..1381e0a --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/distances.py @@ -0,0 +1,15 @@ +from enum import Enum + + +class Distance(str, Enum): + DOT = "dot" + COSINE = "cosine" + L2 = "l2" + + @classmethod + def from_name(cls, name) -> "Distance": + name = name.upper().replace("-", "_") + distance = cls.__members__.get(name) + if distance is not None: + return distance + raise ValueError(f"Unknown distance: <{name}>") diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/parser.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/parser.py new file mode 100644 index 0000000..cc82a1f --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/parser.py @@ -0,0 +1,101 @@ +from enum import Enum +from typing import Any, Dict, List, Optional, Union + + +class FilterType(str, Enum): + FULL_MATCH = "match" + RANGE = "range" + GEO = "geo" + + +FieldValue = Union[str, int, float] +MetaConditions = Dict[str, List[Any]] + + +class BaseConditionParser: + def parse(self, meta_conditions: Optional[MetaConditions]) -> Optional[Any]: + """ + The parse method accepts the meta conditions stored in a dict-like + internal benchmark structure and converts it into the representation + used by a specific engine. + + The internal representation has the following structure: + { + "or": [ + {"a": {"match": {"value": 80}}}, + {"a": {"match": {"value": 2}}} + ] + } + + There is always an operator ("and" / "or") and a list of operands. + + :param meta_conditions: + :return: + """ + if meta_conditions is None or 0 == len(meta_conditions): + return None + return self.build_condition( + and_subfilters=self.create_condition_subfilters(meta_conditions.get("and")), + or_subfilters=self.create_condition_subfilters(meta_conditions.get("or")), + ) + + def build_condition( + self, and_subfilters: Optional[List[Any]], or_subfilters: Optional[List[Any]] + ) -> Optional[Any]: + raise NotImplementedError + + def create_condition_subfilters(self, entries) -> Optional[List[Any]]: + if entries is None: + return None + + output_filters = [] + for entry in entries: + for field_name, field_filters in entry.items(): + for condition_type, value in field_filters.items(): + condition = self.build_filter( + field_name, FilterType(condition_type), value + ) + output_filters.append(condition) + return output_filters + + def build_filter( + self, field_name: str, filter_type: FilterType, criteria: Dict[str, Any] + ): + if FilterType.FULL_MATCH == filter_type: + return self.build_exact_match_filter( + field_name, value=criteria.get("value") + ) + if FilterType.RANGE == filter_type: + return self.build_range_filter( + field_name, + lt=criteria.get("lt"), + gt=criteria.get("gt"), + lte=criteria.get("lte"), + gte=criteria.get("gte"), + ) + if FilterType.GEO == filter_type: + return self.build_geo_filter( + field_name, + lon=criteria.get("lon"), + lat=criteria.get("lat"), + radius=criteria.get("radius"), + ) + raise NotImplementedError + + def build_exact_match_filter(self, field_name: str, value: FieldValue) -> Any: + raise NotImplementedError + + def build_range_filter( + self, + field_name: str, + lt: Optional[FieldValue], + gt: Optional[FieldValue], + lte: Optional[FieldValue], + gte: Optional[FieldValue], + ) -> Any: + raise NotImplementedError + + def build_geo_filter( + self, field_name: str, lat: float, lon: float, radius: float + ) -> Any: + raise NotImplementedError diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/search.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/search.py new file mode 100644 index 0000000..7377418 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/search.py @@ -0,0 +1,247 @@ +import functools +import time +from multiprocessing import Process, Queue +from typing import Iterable, List, Optional, Tuple +from itertools import islice + +import numpy as np +import tqdm +import os +from ml_dtypes import bfloat16 + +from dataset_reader.base_reader import Query +from engine.base_client.utils import check_data_type + +DEFAULT_TOP = 10 +MAX_QUERIES = int(os.getenv("MAX_QUERIES", -1)) + + + +class BaseSearcher: + MP_CONTEXT = None + + def __init__(self, host, connection_params, search_params): + self.host = host + self.connection_params = connection_params + self.search_params = search_params + + @classmethod + def init_client( + cls, host: str, distance, connection_params: dict, search_params: dict + ): + raise NotImplementedError() + + @classmethod + def get_mp_start_method(cls): + return None + + @classmethod + def search_one( + cls, vector: List[float], meta_conditions, top: Optional[int] + ) -> List[Tuple[int, float]]: + raise NotImplementedError() + + @classmethod + def _search_one(cls, query, top: Optional[int] = None): + if top is None: + top = ( + len(query.expected_result) + if query.expected_result is not None and len(query.expected_result) > 0 + else DEFAULT_TOP + ) + + start = time.perf_counter() + search_res = cls.search_one(query.vector, query.meta_conditions, top) + end = time.perf_counter() + + precision = 1.0 + if query.expected_result: + ids = set(x[0] for x in search_res) + precision = len(ids.intersection(query.expected_result[:top])) / top + return precision, end - start + + def search_all( + self, + distance, + queries: Iterable[Query], + num_queries: int = -1, + ): + parallel = self.search_params.get("parallel", 1) + top = self.search_params.get("top", None) + single_search_params = self.search_params.get("search_params", None) + if single_search_params: + data_type = check_data_type(single_search_params.get("data_type", "FLOAT32").upper()) + else: + data_type = np.float32 # Default data type if not specified + # setup_search may require initialized client + self.init_client( + self.host, distance, self.connection_params, self.search_params + ) + self.setup_search() + + search_one = functools.partial(self.__class__._search_one, top=top) + + # Convert queries to a list for potential reuse + # Also, converts query vectors to bytes beforehand, preparing them for sending to client without affecting search time measurements + queries_list = [] + for query in queries: + query.vector = np.array(query.vector).astype(data_type).tobytes() + queries_list.append(query) + + # Handle MAX_QUERIES environment variable + if MAX_QUERIES > 0: + queries_list = queries_list[:MAX_QUERIES] + print(f"Limiting queries to [0:{MAX_QUERIES-1}]") + + # Handle num_queries parameter + if num_queries > 0: + # If we need more queries than available, use a cycling generator + if num_queries > len(queries_list) and len(queries_list) > 0: + print(f"Requested {num_queries} queries but only {len(queries_list)} are available.") + print(f"Using a cycling generator to efficiently process queries.") + + # Create a cycling generator function + def cycling_query_generator(queries, total_count): + """Generate queries by cycling through the available ones.""" + count = 0 + while count < total_count: + for query in queries: + if count < total_count: + yield query + count += 1 + else: + break + + # Use the generator instead of creating a full list + used_queries = cycling_query_generator(queries_list, num_queries) + # We need to know the total count for the progress bar + total_query_count = num_queries + else: + used_queries = queries_list[:num_queries] + total_query_count = len(used_queries) + print(f"Using {num_queries} queries") + else: + used_queries = queries_list + total_query_count = len(used_queries) + + if parallel == 1: + # Create a progress bar with the correct total + pbar = tqdm.tqdm(total=total_query_count, desc="Processing queries", unit="queries") + + # Single-threaded execution + start = time.perf_counter() + + # Process queries with progress updates + results = [] + for query in used_queries: + results.append(search_one(query)) + pbar.update(1) + + # Close the progress bar + pbar.close() + + total_time = time.perf_counter() - start + else: + # Dynamically calculate chunk size based on total_query_count + chunk_size = max(1, total_query_count // parallel) + + # If used_queries is a generator, we need to handle it differently + if hasattr(used_queries, '__next__'): + # For generators, we'll create chunks on-the-fly + query_chunks = [] + remaining = total_query_count + while remaining > 0: + current_chunk_size = min(chunk_size, remaining) + chunk = [next(used_queries) for _ in range(current_chunk_size)] + query_chunks.append(chunk) + remaining -= current_chunk_size + else: + # For lists, we can use the chunked_iterable function + query_chunks = list(chunked_iterable(used_queries, chunk_size)) + + # Create a queue to collect results + result_queue = Queue() + + # Create worker processes + processes = [] + for chunk in query_chunks: + process = Process(target=worker_function, args=(self, distance, search_one, chunk, result_queue)) + processes.append(process) + + # Start worker processes + for process in processes: + process.start() + + # Collect results from all worker processes + results = [] + min_start_time = time.perf_counter() + for _ in processes: + proc_start_time, chunk_results = result_queue.get() + results.extend(chunk_results) + + # Update min_start_time if necessary + if proc_start_time < min_start_time: + min_start_time = proc_start_time + + # Stop measuring time for the critical work + total_time = time.perf_counter() - min_start_time + + # Wait for all worker processes to finish + for process in processes: + process.join() + + # Extract precisions and latencies (outside the timed section) + precisions, latencies = zip(*results) + + self.__class__.delete_client() + + return { + "total_time": total_time, + "mean_time": np.mean(latencies), + "mean_precisions": np.mean(precisions), + "std_time": np.std(latencies), + "min_time": np.min(latencies), + "max_time": np.max(latencies), + "rps": len(latencies) / total_time, + "p50_time": np.percentile(latencies, 50), + "p95_time": np.percentile(latencies, 95), + "p99_time": np.percentile(latencies, 99), + "precisions": precisions, + "latencies": latencies, + } + + def setup_search(self): + pass + + def post_search(self): + pass + + @classmethod + def delete_client(cls): + pass + + +def chunked_iterable(iterable, size): + """Yield successive chunks of a given size from an iterable.""" + it = iter(iterable) + while chunk := list(islice(it, size)): + yield chunk + +# Function to be executed by each worker process +def worker_function(self, distance, search_one, chunk, result_queue): + self.init_client( + self.host, + distance, + self.connection_params, + self.search_params, + ) + self.setup_search() + + start_time = time.perf_counter() + results = process_chunk(chunk, search_one) + result_queue.put((start_time, results)) + +def process_chunk(chunk, search_one): + """Process a chunk of queries using the search_one function.""" + # No progress bar in worker processes to avoid cluttering the output + return [search_one(query) for query in chunk] diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/upload.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/upload.py new file mode 100644 index 0000000..2bda547 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/upload.py @@ -0,0 +1,114 @@ +import time +from multiprocessing import get_context +from typing import Iterable, List, Optional, Tuple + +import tqdm + +from dataset_reader.base_reader import Record +from engine.base_client.utils import iter_batches + + +class BaseUploader: + client = None + + def __init__(self, host, connection_params, upload_params): + self.host = host + self.connection_params = connection_params + self.upload_params = upload_params + + @classmethod + def get_mp_start_method(cls): + return None + + @classmethod + def init_client(cls, host, distance, connection_params: dict, upload_params: dict): + raise NotImplementedError() + + def upload( + self, + distance, + records: Iterable[Record], + ) -> dict: + latencies = [] + start = time.perf_counter() + parallel = self.upload_params.get("parallel", 1) + batch_size = self.upload_params.get("batch_size", 64) + + self.init_client( + self.host, distance, self.connection_params, self.upload_params + ) + + if parallel == 1: + for batch in iter_batches(tqdm.tqdm(records), batch_size): + latencies.append(self._upload_batch(batch)) + else: + ctx = get_context(self.get_mp_start_method()) + with ctx.Pool( + processes=int(parallel), + initializer=self.__class__.init_client, + initargs=( + self.host, + distance, + self.connection_params, + self.upload_params, + ), + ) as pool: + try: + latencies = list( + pool.imap( + self.__class__._upload_batch, + iter_batches(tqdm.tqdm(records), batch_size), + ) + ) + except Exception as e: + raise e + + upload_time = time.perf_counter() - start + + print("Upload time: {}".format(upload_time)) + + post_upload_stats = self.post_upload(distance) + + total_time = time.perf_counter() - start + + print(f"Total import time: {total_time}") + + memory_usage = self.get_memory_usage() + self.delete_client() + + return { + "post_upload": post_upload_stats, + "upload_time": upload_time, + "total_time": total_time, + "latencies": latencies, + "parallel": parallel, + "batch_size": batch_size, + "memory_usage": memory_usage, + } + + @classmethod + def _upload_batch( + cls, batch: Tuple[List[int], List[list], List[Optional[dict]]] + ) -> float: + ids, vectors, metadata = batch + start = time.perf_counter() + cls.upload_batch(ids, vectors, metadata) + return time.perf_counter() - start + + @classmethod + def post_upload(cls, distance): + return {} + + @classmethod + def get_memory_usage(cls): + return {} + + @classmethod + def upload_batch( + cls, ids: List[int], vectors: List[list], metadata: List[Optional[dict]] + ): + raise NotImplementedError() + + @classmethod + def delete_client(cls): + pass diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/utils.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/utils.py new file mode 100644 index 0000000..e051717 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/base_client/utils.py @@ -0,0 +1,39 @@ +from typing import Any, Iterable + +from ml_dtypes import bfloat16 +import numpy as np + +from dataset_reader.base_reader import Record + + +def iter_batches(records: Iterable[Record], n: int) -> Iterable[Any]: + ids = [] + vectors = [] + metadata = [] + + for record in records: + ids.append(record.id) + vectors.append(record.vector) + metadata.append(record.metadata) + + if len(vectors) >= n: + yield [ids, vectors, metadata] + ids, vectors, metadata = [], [], [] + if len(ids) > 0: + yield [ids, vectors, metadata] + + +def check_data_type(data_type: str): + valid_data_types = ["FLOAT32", "FLOAT64", "FLOAT16", "BFLOAT16"] + if data_type.upper() not in valid_data_types: + raise ValueError( + f"Invalid data type: {data_type}. Valid options are: {valid_data_types}" + ) + if data_type == "FLOAT32": + return np.float32 + if data_type == "FLOAT64": + return np.float64 + if data_type == "FLOAT16": + return np.float16 + if data_type == "BFLOAT16": + return bfloat16 diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/__init__.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/client_factory.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/client_factory.py new file mode 100644 index 0000000..e92bd7a --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/client_factory.py @@ -0,0 +1,116 @@ +from abc import ABC +import importlib +from typing import Dict, List, Type + +from engine.base_client.client import ( + BaseClient, + BaseConfigurator, + BaseSearcher, + BaseUploader, +) + +# Dictionary to store dynamically imported client classes +_engine_classes = {} + +def _import_engine_classes(engine_name: str) -> Dict[str, Type]: + """ + Dynamically import client classes for a specific engine. + + Args: + engine_name: The name of the engine (e.g., 'redis', 'qdrant') + + Returns: + Dictionary with configurator, uploader, and searcher classes + """ + if engine_name in _engine_classes: + return _engine_classes[engine_name] + + # Handle special case for vectorsets which uses redis prefix + if engine_name == "vectorsets": + module_name = f"engine.clients.vectorsets" + class_prefix = "RedisVset" + else: + module_name = f"engine.clients.{engine_name}" + # Convert first letter to uppercase for class name + class_prefix = engine_name[0].upper() + engine_name[1:] + + try: + module = importlib.import_module(module_name) + configurator_class = getattr(module, f"{class_prefix}Configurator") + uploader_class = getattr(module, f"{class_prefix}Uploader") + searcher_class = getattr(module, f"{class_prefix}Searcher") + + _engine_classes[engine_name] = { + "configurator": configurator_class, + "uploader": uploader_class, + "searcher": searcher_class + } + + return _engine_classes[engine_name] + except (ImportError, AttributeError) as e: + raise ImportError(f"Failed to import classes for engine '{engine_name}': {e}") + +# Empty dictionaries that will be populated on demand +ENGINE_CONFIGURATORS = {} +ENGINE_UPLOADERS = {} +ENGINE_SEARCHERS = {} + + +class ClientFactory(ABC): + def __init__(self, host): + self.host = host + self.engine = None + + def _create_configurator(self, experiment) -> BaseConfigurator: + self.engine = experiment["engine"] + engine_name = experiment["engine"] + + # Dynamically import engine classes if not already imported + if engine_name not in _engine_classes: + _import_engine_classes(engine_name) + # Add to the global dictionaries for compatibility + ENGINE_CONFIGURATORS[engine_name] = _engine_classes[engine_name]["configurator"] + ENGINE_UPLOADERS[engine_name] = _engine_classes[engine_name]["uploader"] + ENGINE_SEARCHERS[engine_name] = _engine_classes[engine_name]["searcher"] + + engine_configurator_class = _engine_classes[engine_name]["configurator"] + engine_configurator = engine_configurator_class( + self.host, + collection_params={**experiment.get("collection_params", {})}, + connection_params={**experiment.get("connection_params", {})}, + ) + return engine_configurator + + def _create_uploader(self, experiment) -> BaseUploader: + engine_name = experiment["engine"] + engine_uploader_class = _engine_classes[engine_name]["uploader"] + engine_uploader = engine_uploader_class( + self.host, + connection_params={**experiment.get("connection_params", {})}, + upload_params={**experiment.get("upload_params", {})}, + ) + return engine_uploader + + def _create_searchers(self, experiment) -> List[BaseSearcher]: + engine_name = experiment["engine"] + engine_searcher_class: Type[BaseSearcher] = _engine_classes[engine_name]["searcher"] + + engine_searchers = [ + engine_searcher_class( + self.host, + connection_params={**experiment.get("connection_params", {})}, + search_params=search_params, + ) + for search_params in experiment.get("search_params", [{}]) + ] + + return engine_searchers + + def build_client(self, experiment): + return BaseClient( + name=experiment["name"], + engine=experiment["engine"], + configurator=self._create_configurator(experiment), + uploader=self._create_uploader(experiment), + searchers=self._create_searchers(experiment), + ) diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/__init__.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/__init__.py new file mode 100644 index 0000000..a143774 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/__init__.py @@ -0,0 +1,3 @@ +from engine.clients.redis.configure import RedisConfigurator +from engine.clients.redis.search import RedisSearcher +from engine.clients.redis.upload import RedisUploader diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/config.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/config.py new file mode 100644 index 0000000..8048559 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/config.py @@ -0,0 +1,20 @@ +import os + +REDIS_PORT = int(os.getenv("REDIS_PORT", 6379)) +REDIS_AUTH = os.getenv("REDIS_AUTH", None) +REDIS_USER = os.getenv("REDIS_USER", None) +REDIS_CLUSTER = bool(int(os.getenv("REDIS_CLUSTER", 0))) +# One of BATCHES and ADHOC_BF +# check https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/vectors/#pre-filter-query-attributes-hybrid-approach +REDIS_HYBRID_POLICY = os.getenv("REDIS_HYBRID_POLICY", "") +# Query dialect. Defaults to 4 (optimized SORTBY). Redis Flex does not support +# DIALECT 4 yet, so set REDIS_DIALECT=2 when benchmarking a Flex instance. +REDIS_DIALECT = int(os.getenv("REDIS_DIALECT", 4)) +REDIS_KEEP_DOCUMENTS = bool(os.getenv("REDIS_KEEP_DOCUMENTS", 0)) +REDIS_JUST_INDEX = bool(os.getenv("REDIS_JUST_INDEX", 0)) +GPU_STATS = bool(int(os.getenv("GPU_STATS", 0))) +GPU_STATS_ENDPOINT = os.getenv("GPU_STATS_ENDPOINT", None) + + +# 60 seconds timeout +REDIS_QUERY_TIMEOUT = int(os.getenv("REDIS_QUERY_TIMEOUT", 90 * 1000)) diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/configure.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/configure.py new file mode 100644 index 0000000..df752d6 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/configure.py @@ -0,0 +1,140 @@ +import redis +import time +from redis import Redis, RedisCluster +from redis.commands.search.field import ( + GeoField, + NumericField, + TextField, + VectorField, + TagField, +) + +from benchmark.dataset import Dataset +from engine.base_client.configure import BaseConfigurator +from engine.base_client.distances import Distance +from engine.clients.redis.config import ( + REDIS_PORT, + REDIS_AUTH, + REDIS_USER, + REDIS_CLUSTER, + REDIS_KEEP_DOCUMENTS, +) + + +class RedisConfigurator(BaseConfigurator): + DISTANCE_MAPPING = { + Distance.L2: "L2", + Distance.COSINE: "COSINE", + Distance.DOT: "IP", + } + FIELD_MAPPING = { + "int": NumericField, + "keyword": TagField, + "text": TextField, + "float": NumericField, + "geo": GeoField, + } + + def __init__(self, host, collection_params: dict, connection_params: dict): + super().__init__(host, collection_params, connection_params) + redis_constructor = RedisCluster if REDIS_CLUSTER else Redis + self._is_cluster = True if REDIS_CLUSTER else False + self.client = redis_constructor( + host=host, port=REDIS_PORT, password=REDIS_AUTH, username=REDIS_USER + ) + + def clean(self): + conns = [self.client] + if self._is_cluster: + conns = [ + self.client.get_redis_connection(node) + for node in self.client.get_primaries() + ] + for conn in conns: + index = conn.ft() + try: + index.dropindex(delete_documents=(not REDIS_KEEP_DOCUMENTS)) + except redis.ResponseError as e: + str_err = e.__str__() + if ( + "Unknown Index name" not in str_err + and "Index does not exist" not in str_err + and "no such index" not in str_err + and "SEARCH_INDEX_NOT_FOUND" not in str_err + ): + # google memorystore does not support the DD argument, and + # redis flex does not support it either. in that case we drop + # the index without DD and flushall to clear the documents. + if ( + "wrong number of arguments for FT.DROPINDEX command" in str_err + or "DD is not supported" in str_err + ): + print( + "Given the FT.DROPINDEX command with DD failed, we're " + "dropping the index without DD and flushing the entire DB..." + ) + try: + index.dropindex(delete_documents=False) + except redis.ResponseError: + pass + if REDIS_KEEP_DOCUMENTS is False: + conn.flushall() + else: + raise e + + def recreate(self, dataset: Dataset, collection_params): + self.clean() + + payload_fields = [ + self.FIELD_MAPPING[field_type]( + name=field_name, + sortable=True, + ) + for field_name, field_type in dataset.config.schema.items() + if field_type != "keyword" + ] + payload_fields += [ + TagField( + name=field_name, + separator=";", + sortable=True, + ) + for field_name, field_type in dataset.config.schema.items() + if field_type == "keyword" + ] + algorithm_config = {} + # by default we use hnsw + algo = collection_params.get("algorithm", "hnsw") + data_type = collection_params.get("data_type", "float32") + algorithm_config = collection_params.get(f"{algo}_config", {}) + print(f"Using algorithm {algo} with config {algorithm_config}") + index_fields = [ + VectorField( + name="vector", + algorithm=algo, + attributes={ + "TYPE": data_type, + "DIM": dataset.config.vector_size, + "DISTANCE_METRIC": self.DISTANCE_MAPPING[dataset.config.distance], + **algorithm_config, + }, + ) + ] + payload_fields + + conns = [self.client] + if self._is_cluster: + conns = [ + self.client.get_redis_connection(node) + for node in self.client.get_primaries() + ] + for conn in conns: + search_namespace = conn.ft() + try: + search_namespace.create_index(fields=index_fields) + except redis.ResponseError as e: + if "Index already exists" not in e.__str__(): + raise e + + +if __name__ == "__main__": + pass diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/helper.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/helper.py new file mode 100644 index 0000000..73e530b --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/helper.py @@ -0,0 +1,20 @@ +from typing import Tuple + +MIN_LAT, MAX_LAT = -85.05112878, 85.05112878 + + +def convert_to_redis_coords(lon: float, lat: float) -> Tuple[float, float]: + """ + Redis uses a different coordinate system for storing the geocoordinates + (EPSG:900913 / EPSG:3785 / OSGEO:41001) which is a subset of the WSG84 used + by the other engines. Redis can only represent longitudes from -180 to 180 + degrees and latitudes are from -85.05112878 to 85.05112878 degrees. + :param lon: + :param lat: + :return: + """ + if MIN_LAT <= lat <= MAX_LAT: + return lon, lat + if lat < MIN_LAT: + return lon, MIN_LAT + return lon, MAX_LAT diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/parser.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/parser.py new file mode 100644 index 0000000..ae4068d --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/parser.py @@ -0,0 +1,93 @@ +from collections import ChainMap +from typing import Any, Dict, List, Optional, Tuple + +from engine.base_client.parser import BaseConditionParser, FieldValue +from engine.clients.redis.helper import convert_to_redis_coords + +QueryParamsTuple = Tuple[str, Dict[str, Any]] + + +class RedisConditionParser(BaseConditionParser): + def __init__(self) -> None: + super().__init__() + self.counter = 0 + + def build_condition( + self, + and_subfilters: Optional[List[QueryParamsTuple]], + or_subfilters: Optional[List[QueryParamsTuple]], + ) -> Tuple[str, Dict[str, Any]]: + and_clauses, and_params = ( + list(zip(*and_subfilters)) if and_subfilters else ([], []) + ) + or_clauses, or_params = list(zip(*or_subfilters)) if or_subfilters else ([], []) + + clause = [] + if len(and_clauses) > 0: + clause.append("(" + " ".join(and_clauses) + ")") + if len(or_clauses) > 0: + clause.append("(" + " | ".join(or_clauses) + ")") + params = list(and_params) + list(or_params) + return " ".join(clause), dict(ChainMap(*params)) + + def build_exact_match_filter(self, field_name: str, value: FieldValue) -> Any: + param_name = f"{field_name}_{self.counter}" + self.counter += 1 + if isinstance(value, str): + return f"@{field_name}:{{${param_name}}}", {param_name: value} + # field is numeric, value is an integer + return f"@{field_name}:[${param_name} ${param_name}]", {param_name: value} + + def build_range_filter( + self, + field_name: str, + lt: Optional[FieldValue], + gt: Optional[FieldValue], + lte: Optional[FieldValue], + gte: Optional[FieldValue], + ) -> Any: + param_prefix = f"{field_name}_{self.counter}" + self.counter += 1 + params = dict() + filters = list() + + if lt is not None: + params[f"{param_prefix}_lt"] = lt + filters.append(("-inf", f"(${param_prefix}_lt")) + + if gt is not None: + params[f"{param_prefix}_gt"] = gt + filters.append((f"(${param_prefix}_gt", "+inf")) + + if lte is not None: + params[f"{param_prefix}_lte"] = lte + filters.append(("-inf", f"${param_prefix}_lte")) + + if gte is not None: + params[f"{param_prefix}_gte"] = gte + filters.append((f"${param_prefix}_gte", "+inf")) + + clauses = [] + for filter_entry in filters: + if filter_entry is None: + continue + left, right = filter_entry + clauses.append(f"@{field_name}:[{left} {right}]") + + return " ".join(clauses), params + + def build_geo_filter( + self, field_name: str, lat: float, lon: float, radius: float + ) -> Any: + param_prefix = f"{field_name}_{self.counter}" + self.counter += 1 + lon, lat = convert_to_redis_coords(lon, lat) + params = { + f"{param_prefix}_lon": lon, + f"{param_prefix}_lat": lat, + f"{param_prefix}_radius": radius, + } + return ( + f"@{field_name}:[${param_prefix}_lon ${param_prefix}_lat ${param_prefix}_radius m]", + params, + ) diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/search.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/search.py new file mode 100644 index 0000000..14ae607 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/search.py @@ -0,0 +1,105 @@ +import random +from typing import List, Tuple +import numpy as np +from redis import Redis, RedisCluster +from redis.commands.search.query import Query +from engine.base_client.utils import check_data_type +from engine.base_client.search import BaseSearcher +from engine.clients.redis.config import ( + REDIS_PORT, + REDIS_QUERY_TIMEOUT, + REDIS_AUTH, + REDIS_USER, + REDIS_CLUSTER, + REDIS_HYBRID_POLICY, + REDIS_DIALECT, +) + +from engine.clients.redis.parser import RedisConditionParser + + +class RedisSearcher(BaseSearcher): + search_params = {} + client = None + parser = RedisConditionParser() + + @classmethod + def init_client(cls, host, distance, connection_params: dict, search_params: dict): + redis_constructor = RedisCluster if REDIS_CLUSTER else Redis + cls.client = redis_constructor( + host=host, port=REDIS_PORT, password=REDIS_AUTH, username=REDIS_USER + ) + cls.search_params = search_params + cls.knn_conditions = "" + cls.algorithm = cls.search_params.get("algorithm", "hnsw").upper() + cls.hybrid_policy = REDIS_HYBRID_POLICY + + if cls.algorithm == "HNSW": + # 'EF_RUNTIME' is irrelevant for 'ADHOC_BF' policy + if cls.hybrid_policy != "ADHOC_BF": + cls.knn_conditions = "EF_RUNTIME $EF" + elif cls.algorithm == "SVS-VAMANA": + cls.knn_conditions = "SEARCH_WINDOW_SIZE $SEARCH_WINDOW_SIZE" + cls.data_type = "FLOAT32" + if "search_params" in cls.search_params: + cls.data_type = ( + cls.search_params["search_params"].get("data_type", "FLOAT32").upper() + ) + cls.np_data_type = check_data_type(cls.data_type) + cls._is_cluster = True if REDIS_CLUSTER else False + + # In the case of CLUSTER API enabled we randomly select the starting primary shard + # when doing the client initialization to evenly distribute the load among the cluster + cls.conns = [cls.client] + if cls._is_cluster: + cls.conns = [ + cls.client.get_redis_connection(node) + for node in cls.client.get_primaries() + ] + cls._ft = cls.conns[random.randint(0, len(cls.conns)) - 1].ft() + + @classmethod + def search_one(cls, vector, meta_conditions, top) -> List[Tuple[int, float]]: + conditions = cls.parser.parse(meta_conditions) + hybrid_policy = "" + if cls.hybrid_policy != "": + hybrid_policy = '=>{$HYBRID_POLICY: '+ cls.hybrid_policy + ' }' + if conditions is None: + prefilter_condition = "*" + params = {} + else: + prefilter_condition, params = conditions + + q = ( + Query( + f"{prefilter_condition}=>[KNN $K @vector $vec_param {cls.knn_conditions} AS vector_score]{hybrid_policy}" + ) + .sort_by("vector_score", asc=True) + .paging(0, top) + .return_fields("vector_score") + # performance is optimized for sorting operations on DIALECT 4 in different scenarios. + # check SORTBY details in https://redis.io/commands/ft.search/ + # Redis Flex does not support DIALECT 4; override via REDIS_DIALECT=2. + .dialect(REDIS_DIALECT) + .timeout(REDIS_QUERY_TIMEOUT) + ) + # Handle case where vector is already in bytes format (after commit a9a7488) + if isinstance(vector, bytes): + vec_param = vector + else: + vec_param = np.array(vector).astype(cls.np_data_type).tobytes() + + params_dict = { + "vec_param": vec_param, + "K": top, + **params, + } + if cls.algorithm == "HNSW": + # 'EF_RUNTIME' is irrelevant for 'ADHOC_BF' policy + if cls.hybrid_policy != "ADHOC_BF": + params_dict["EF"] = cls.search_params["search_params"]["ef"] + if cls.algorithm == "SVS-VAMANA": + params_dict["SEARCH_WINDOW_SIZE"] = cls.search_params["search_params"]["SEARCH_WINDOW_SIZE"] + results = cls._ft.search(q, query_params=params_dict) + + return [(int(result.id), float(result.vector_score)) for result in results.docs] diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/upload.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/upload.py new file mode 100644 index 0000000..4d055ab --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/redis/upload.py @@ -0,0 +1,156 @@ +import time +from typing import List, Optional +from ml_dtypes import bfloat16 +import requests +import json +import random +import numpy as np +from redis import Redis, RedisCluster +from engine.base_client.upload import BaseUploader +from engine.base_client.utils import check_data_type +from engine.clients.redis.config import ( + REDIS_PORT, + REDIS_AUTH, + REDIS_USER, + REDIS_CLUSTER, + GPU_STATS, + GPU_STATS_ENDPOINT, + REDIS_JUST_INDEX, +) +from engine.clients.redis.helper import convert_to_redis_coords + + +class RedisUploader(BaseUploader): + client = None + host = None + client_decode = None + upload_params = {} + + @classmethod + def init_client(cls, host, distance, connection_params, upload_params): + redis_constructor = RedisCluster if REDIS_CLUSTER else Redis + cls.client = redis_constructor( + host=host, port=REDIS_PORT, password=REDIS_AUTH, username=REDIS_USER + ) + cls.host = host + cls.client_decode = redis_constructor( + host=host, + port=REDIS_PORT, + password=REDIS_AUTH, + username=REDIS_USER, + decode_responses=True, + ) + cls.upload_params = upload_params + cls.algorithm = cls.upload_params.get("algorithm", "hnsw").upper() + cls.data_type = cls.upload_params.get("data_type", "FLOAT32").upper() + cls.np_data_type = check_data_type(cls.data_type) + cls._is_cluster = True if REDIS_CLUSTER else False + + @classmethod + def upload_batch( + cls, ids: List[int], vectors: List[list], metadata: Optional[List[dict]] + ): + if REDIS_JUST_INDEX: + return + for i in range(len(ids)): + idx = ids[i] + vector_key = str(idx) + vec = vectors[i] + meta = metadata[i] if metadata else {} + geopoints = {} + payload = {} + if meta is not None: + for k, v in meta.items(): + # This is a patch for arxiv-titles dataset where we have a list of "labels", and + # we want to index all of them under the same TAG field (whose separator is ';'). + if k == "labels": + payload[k] = ";".join(v) + if ( + v is not None + and not isinstance(v, dict) + and not isinstance(v, list) + ): + payload[k] = v + # Redis treats geopoints differently and requires putting them as + # a comma-separated string with lat and lon coordinates + geopoints = { + k: ",".join(map(str, convert_to_redis_coords(v["lon"], v["lat"]))) + for k, v in meta.items() + if isinstance(v, dict) + } + cls.client.hset( + vector_key, + mapping={ + "vector": np.array(vec).astype(cls.np_data_type).tobytes(), + **payload, + **geopoints, + }, + ) + + + @classmethod + def post_upload(cls, _distance): + if cls.algorithm != "HNSW" and cls.algorithm != "FLAT" and cls.algorithm != "SVS-VAMANA": + print(f"TODO: FIXME!! Avoiding calling ft.info for {cls.algorithm}...") + return {} + index_info = cls.client.ft().info() + # redisearch / memorystore for redis + if "percent_indexed" in index_info: + percent_index = float(index_info["percent_indexed"]) + while percent_index < 1.0: + print( + "waiting for index to be fully processed. current percent index: {}".format( + percent_index * 100.0 + ) + ) + time.sleep(1) + percent_index = float(cls.client.ft().info()["percent_indexed"]) + # memorydb + if "current_lag" in index_info: + current_lag = float(index_info["current_lag"]) + while current_lag > 0: + print( + "waiting for index to be fully processed. current current_lag: {}".format( + current_lag + ) + ) + time.sleep(1) + current_lag = int(cls.client.ft().info()["current_lag"]) + return {} + + def get_memory_usage(cls): + used_memory = [] + conns = [cls.client_decode] + if cls._is_cluster: + conns = [ + cls.client_decode.get_redis_connection(node) + for node in cls.client_decode.get_primaries() + ] + for conn in conns: + used_memory_shard = conn.info("memory")["used_memory"] + used_memory.append(used_memory_shard) + index_info = {} + device_info = {} + if cls.algorithm != "HNSW" and cls.algorithm != "FLAT" and cls.algorithm != "SVS-VAMANA": + print(f"TODO: FIXME!! Avoiding calling ft.info for {cls.algorithm}...") + else: + index_info = cls.client_decode.ft().info() + if GPU_STATS: + url = f"http://{cls.host}:5000/" + if GPU_STATS_ENDPOINT is not None: + url = GPU_STATS_ENDPOINT + try: + print(f"Quering GPU stats from endpoint {url}...") + # Send GET request to the server + response = requests.get(url) + device_info = json.loads(response.text) + print("Retrieved device info:", device_info) + except requests.exceptions.RequestException as e: + # Handle any exceptions that may occur + print("An error occurred while querying gpu stats:", e) + + return { + "used_memory": used_memory, + "index_info": index_info, + "device_info": device_info, + } diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/vectorsets/__init__.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/vectorsets/__init__.py new file mode 100644 index 0000000..c21498c --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/vectorsets/__init__.py @@ -0,0 +1,3 @@ +from engine.clients.vectorsets.configure import RedisVsetConfigurator +from engine.clients.vectorsets.search import RedisVsetSearcher +from engine.clients.vectorsets.upload import RedisVsetUploader diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/vectorsets/config.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/vectorsets/config.py new file mode 100644 index 0000000..e9ef607 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/vectorsets/config.py @@ -0,0 +1,9 @@ +import os + +REDIS_PORT = int(os.getenv("REDIS_PORT", 6379)) +REDIS_AUTH = os.getenv("REDIS_AUTH", None) +REDIS_USER = os.getenv("REDIS_USER", None) +REDIS_CLUSTER = bool(int(os.getenv("REDIS_CLUSTER", 0))) + +# 90 seconds timeout +REDIS_QUERY_TIMEOUT = int(os.getenv("REDIS_QUERY_TIMEOUT", 90 * 1000)) diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/vectorsets/configure.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/vectorsets/configure.py new file mode 100644 index 0000000..95d111b --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/vectorsets/configure.py @@ -0,0 +1,42 @@ +import redis +from redis import Redis, RedisCluster + +from benchmark.dataset import Dataset +from engine.base_client.configure import BaseConfigurator +from engine.clients.vectorsets.config import ( + REDIS_AUTH, + REDIS_CLUSTER, + REDIS_PORT, + REDIS_USER, +) + + +class RedisVsetConfigurator(BaseConfigurator): + + def __init__(self, host, collection_params: dict, connection_params: dict): + super().__init__(host, collection_params, connection_params) + redis_constructor = RedisCluster if REDIS_CLUSTER else Redis + self._is_cluster = True if REDIS_CLUSTER else False + self.client = redis_constructor( + host=host, port=REDIS_PORT, password=REDIS_AUTH, username=REDIS_USER + ) + + def clean(self): + conns = [self.client] + if self._is_cluster: + conns = [ + self.client.get_redis_connection(node) + for node in self.client.get_primaries() + ] + for conn in conns: + try: + conn.flushall() + except redis.ResponseError as e: + print(e) + + def recreate(self, dataset: Dataset, collection_params): + pass + + +if __name__ == "__main__": + pass diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/vectorsets/search.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/vectorsets/search.py new file mode 100644 index 0000000..6f45e1f --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/vectorsets/search.py @@ -0,0 +1,52 @@ +import random +from typing import List, Tuple + +from redis import Redis, RedisCluster + + +from engine.base_client.search import BaseSearcher +from engine.clients.vectorsets.config import ( + REDIS_AUTH, + REDIS_CLUSTER, + REDIS_PORT, + REDIS_QUERY_TIMEOUT, + REDIS_USER, +) +from engine.clients.redis.parser import RedisConditionParser + + +class RedisVsetSearcher(BaseSearcher): + search_params = {} + client = None + parser = RedisConditionParser() + + @classmethod + def init_client(cls, host, distance, connection_params: dict, search_params: dict): + redis_constructor = RedisCluster if REDIS_CLUSTER else Redis + cls.client = redis_constructor( + host=host, port=REDIS_PORT, password=REDIS_AUTH, username=REDIS_USER + ) + cls.search_params = search_params + cls._is_cluster = True if REDIS_CLUSTER else False + # In the case of CLUSTER API enabled we randomly select the starting primary shard + # when doing the client initialization to evenly distribute the load among the cluster + cls.conns = [cls.client] + if cls._is_cluster: + cls.conns = [ + cls.client.get_redis_connection(node) + for node in cls.client.get_primaries() + ] + cls._ft = cls.conns[random.randint(0, len(cls.conns)) - 1].ft() + + @classmethod + def search_one(cls, vector, meta_conditions, top) -> List[Tuple[int, float]]: + ef = cls.search_params["search_params"]["ef"] + response = cls.client.execute_command("VSIM", "idx", "FP32", vector, "WITHSCORES", "COUNT", top, "EF", ef) + # decode responses + # every even cell is id, every odd is the score + # scores needs to be 1 - scores since on vector sets 1 is identical, 0 is opposite vector + ids = [int(response[i]) for i in range(0, len(response), 2)] + scores = [1 - float(response[i]) for i in range(1, len(response), 2)] + # we need to return a list of tuples + # where the first element is the id and the second is the score + return list(zip(ids, scores)) diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/vectorsets/upload.py b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/vectorsets/upload.py new file mode 100644 index 0000000..aec62df --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/engine/clients/vectorsets/upload.py @@ -0,0 +1,71 @@ +from typing import List, Optional + +import numpy as np +from redis import Redis, RedisCluster + +from engine.base_client.upload import BaseUploader +from engine.clients.vectorsets.config import ( + REDIS_AUTH, + REDIS_CLUSTER, + REDIS_PORT, + REDIS_USER, +) +from engine.clients.redis.helper import convert_to_redis_coords + + +class RedisVsetUploader(BaseUploader): + client = None + upload_params = {} + + @classmethod + def init_client(cls, host, distance, connection_params, upload_params): + redis_constructor = RedisCluster if REDIS_CLUSTER else Redis + cls.client = redis_constructor( + host=host, port=REDIS_PORT, password=REDIS_AUTH, username=REDIS_USER + ) + cls.client_decode = redis_constructor( + host=host, + port=REDIS_PORT, + password=REDIS_AUTH, + username=REDIS_USER, + decode_responses=True, + ) + cls.upload_params = upload_params + cls._is_cluster = True if REDIS_CLUSTER else False + + @classmethod + def upload_batch( + cls, ids: List[int], vectors: List[list], metadata: Optional[List[dict]] + ): + upload_params = cls.upload_params + hnsw_params = upload_params.get("hnsw_config") + M = hnsw_params.get("M", 16) + efc = hnsw_params.get("EF_CONSTRUCTION", 200) + quant = hnsw_params.get("quant", "NOQUANT") + + p = cls.client.pipeline(transaction=False) + for i in range(len(ids)): + idx = ids[i] + vec = vectors[i] + vec = np.array(vec).astype(np.float32).tobytes() + p.execute_command("VADD", "idx", "FP32", vec, idx, quant, "M", M, "EF", efc, "CAS") + p.execute() + + @classmethod + def post_upload(cls, _distance): + return {} + + def get_memory_usage(cls): + used_memory = [] + conns = [cls.client_decode] + if cls._is_cluster: + conns = [ + cls.client_decode.get_redis_connection(node) + for node in cls.client_decode.get_primaries() + ] + for conn in conns: + used_memory_shard = conn.info("memory")["used_memory"] + used_memory.append(used_memory_shard) + + return {"used_memory": sum(used_memory), + "shards": len(used_memory)} diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/experiments/configurations/benchmark.json b/02-semantic-search-rag/fcenedes/vecdb_bench/experiments/configurations/benchmark.json new file mode 100644 index 0000000..60d2de1 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/experiments/configurations/benchmark.json @@ -0,0 +1,158 @@ +[ + { + "name": "redis-m-16-ef-64", + "engine": "redis", + "connection_params": {}, + "collection_params": { + "hnsw_config": { "M": 16, "EF_CONSTRUCTION": 64, "EF_RUNTIME": 64, "RERANK": "TRUE" } + }, + "search_params": [ + { "parallel": 1, "search_params": { "ef": 64 } }, { "parallel": 1, "search_params": { "ef": 128 } }, { "parallel": 1, "search_params": { "ef": 256 } }, { "parallel": 1, "search_params": { "ef": 512 } }, + { "parallel": 100, "search_params": { "ef": 64 } }, { "parallel": 100, "search_params": { "ef": 128 } }, { "parallel": 100, "search_params": { "ef": 256 } }, { "parallel": 100, "search_params": { "ef": 512 } } + ], + "upload_params": { "parallel": 32 } + }, + { + "name": "redis-m-16-ef-128", + "engine": "redis", + "connection_params": {}, + "collection_params": { + "hnsw_config": { "M": 16, "EF_CONSTRUCTION": 128, "EF_RUNTIME": 64, "RERANK": "TRUE" } + }, + "search_params": [ + { "parallel": 1, "search_params": { "ef": 64 } }, { "parallel": 1, "search_params": { "ef": 128 } }, { "parallel": 1, "search_params": { "ef": 256 } }, { "parallel": 1, "search_params": { "ef": 512 } }, + { "parallel": 100, "search_params": { "ef": 64 } }, { "parallel": 100, "search_params": { "ef": 128 } }, { "parallel": 100, "search_params": { "ef": 256 } }, { "parallel": 100, "search_params": { "ef": 512 } } + ], + "upload_params": { "parallel": 32 } + }, + { + "name": "redis-m-16-ef-256", + "engine": "redis", + "connection_params": {}, + "collection_params": { + "hnsw_config": { "M": 16, "EF_CONSTRUCTION": 256, "EF_RUNTIME": 64, "RERANK": "TRUE" } + }, + "search_params": [ + { "parallel": 1, "search_params": { "ef": 64 } }, { "parallel": 1, "search_params": { "ef": 128 } }, { "parallel": 1, "search_params": { "ef": 256 } }, { "parallel": 1, "search_params": { "ef": 512 } }, + { "parallel": 100, "search_params": { "ef": 64 } }, { "parallel": 100, "search_params": { "ef": 128 } }, { "parallel": 100, "search_params": { "ef": 256 } }, { "parallel": 100, "search_params": { "ef": 512 } } + ], + "upload_params": { "parallel": 32 } + }, + { + "name": "redis-m-16-ef-512", + "engine": "redis", + "connection_params": {}, + "collection_params": { + "hnsw_config": { "M": 16, "EF_CONSTRUCTION": 512, "EF_RUNTIME": 64, "RERANK": "TRUE" } + }, + "search_params": [ + { "parallel": 1, "search_params": { "ef": 64 } }, { "parallel": 1, "search_params": { "ef": 128 } }, { "parallel": 1, "search_params": { "ef": 256 } }, { "parallel": 1, "search_params": { "ef": 512 } }, + { "parallel": 100, "search_params": { "ef": 64 } }, { "parallel": 100, "search_params": { "ef": 128 } }, { "parallel": 100, "search_params": { "ef": 256 } }, { "parallel": 100, "search_params": { "ef": 512 } } + ], + "upload_params": { "parallel": 32 } + }, + { + "name": "redis-m-32-ef-64", + "engine": "redis", + "connection_params": {}, + "collection_params": { + "hnsw_config": { "M": 32, "EF_CONSTRUCTION": 64, "EF_RUNTIME": 64, "RERANK": "TRUE" } + }, + "search_params": [ + { "parallel": 1, "search_params": { "ef": 64 } }, { "parallel": 1, "search_params": { "ef": 128 } }, { "parallel": 1, "search_params": { "ef": 256 } }, { "parallel": 1, "search_params": { "ef": 512 } }, + { "parallel": 100, "search_params": { "ef": 64 } }, { "parallel": 100, "search_params": { "ef": 128 } }, { "parallel": 100, "search_params": { "ef": 256 } }, { "parallel": 100, "search_params": { "ef": 512 } } + ], + "upload_params": { "parallel": 32 } + }, + { + "name": "redis-m-32-ef-128", + "engine": "redis", + "connection_params": {}, + "collection_params": { + "hnsw_config": { "M": 32, "EF_CONSTRUCTION": 128, "EF_RUNTIME": 64, "RERANK": "TRUE" } + }, + "search_params": [ + { "parallel": 1, "search_params": { "ef": 64 } }, { "parallel": 1, "search_params": { "ef": 128 } }, { "parallel": 1, "search_params": { "ef": 256 } }, { "parallel": 1, "search_params": { "ef": 512 } }, + { "parallel": 100, "search_params": { "ef": 64 } }, { "parallel": 100, "search_params": { "ef": 128 } }, { "parallel": 100, "search_params": { "ef": 256 } }, { "parallel": 100, "search_params": { "ef": 512 } } + ], + "upload_params": { "parallel": 32 } + }, + { + "name": "redis-m-32-ef-256", + "engine": "redis", + "connection_params": {}, + "collection_params": { + "hnsw_config": { "M": 32, "EF_CONSTRUCTION": 256, "EF_RUNTIME": 64, "RERANK": "TRUE" } + }, + "search_params": [ + { "parallel": 1, "search_params": { "ef": 64 } }, { "parallel": 1, "search_params": { "ef": 128 } }, { "parallel": 1, "search_params": { "ef": 256 } }, { "parallel": 1, "search_params": { "ef": 512 } }, + { "parallel": 100, "search_params": { "ef": 64 } }, { "parallel": 100, "search_params": { "ef": 128 } }, { "parallel": 100, "search_params": { "ef": 256 } }, { "parallel": 100, "search_params": { "ef": 512 } } + ], + "upload_params": { "parallel": 32 } + }, + { + "name": "redis-m-32-ef-512", + "engine": "redis", + "connection_params": {}, + "collection_params": { + "hnsw_config": { "M": 32, "EF_CONSTRUCTION": 512, "EF_RUNTIME": 64, "RERANK": "TRUE" } + }, + "search_params": [ + { "parallel": 1, "search_params": { "ef": 64 } }, { "parallel": 1, "search_params": { "ef": 128 } }, { "parallel": 1, "search_params": { "ef": 256 } }, { "parallel": 1, "search_params": { "ef": 512 } }, + { "parallel": 100, "search_params": { "ef": 64 } }, { "parallel": 100, "search_params": { "ef": 128 } }, { "parallel": 100, "search_params": { "ef": 256 } }, { "parallel": 100, "search_params": { "ef": 512 } } + ], + "upload_params": { "parallel": 32 } + }, + { + "name": "redis-m-64-ef-64", + "engine": "redis", + "connection_params": {}, + "collection_params": { + "hnsw_config": { "M": 64, "EF_CONSTRUCTION": 64, "EF_RUNTIME": 64, "RERANK": "TRUE" } + }, + "search_params": [ + { "parallel": 1, "search_params": { "ef": 64 } }, { "parallel": 1, "search_params": { "ef": 128 } }, { "parallel": 1, "search_params": { "ef": 256 } }, { "parallel": 1, "search_params": { "ef": 512 } }, + { "parallel": 100, "search_params": { "ef": 64 } }, { "parallel": 100, "search_params": { "ef": 128 } }, { "parallel": 100, "search_params": { "ef": 256 } }, { "parallel": 100, "search_params": { "ef": 512 } } + ], + "upload_params": { "parallel": 32 } + }, + { + "name": "redis-m-64-ef-128", + "engine": "redis", + "connection_params": {}, + "collection_params": { + "hnsw_config": { "M": 64, "EF_CONSTRUCTION": 128, "EF_RUNTIME": 64, "RERANK": "TRUE" } + }, + "search_params": [ + { "parallel": 1, "search_params": { "ef": 64 } }, { "parallel": 1, "search_params": { "ef": 128 } }, { "parallel": 1, "search_params": { "ef": 256 } }, { "parallel": 1, "search_params": { "ef": 512 } }, + { "parallel": 100, "search_params": { "ef": 64 } }, { "parallel": 100, "search_params": { "ef": 128 } }, { "parallel": 100, "search_params": { "ef": 256 } }, { "parallel": 100, "search_params": { "ef": 512 } } + ], + "upload_params": { "parallel": 32 } + }, + { + "name": "redis-m-64-ef-256", + "engine": "redis", + "connection_params": {}, + "collection_params": { + "hnsw_config": { "M": 64, "EF_CONSTRUCTION": 256, "EF_RUNTIME": 64, "RERANK": "TRUE" } + }, + "search_params": [ + { "parallel": 1, "search_params": { "ef": 64 } }, { "parallel": 1, "search_params": { "ef": 128 } }, { "parallel": 1, "search_params": { "ef": 256 } }, { "parallel": 1, "search_params": { "ef": 512 } }, + { "parallel": 100, "search_params": { "ef": 64 } }, { "parallel": 100, "search_params": { "ef": 128 } }, { "parallel": 100, "search_params": { "ef": 256 } }, { "parallel": 100, "search_params": { "ef": 512 } } + ], + "upload_params": { "parallel": 32 } + }, + { + "name": "redis-m-64-ef-512", + "engine": "redis", + "connection_params": {}, + "collection_params": { + "hnsw_config": { "M": 64, "EF_CONSTRUCTION": 512, "EF_RUNTIME": 64, "RERANK": "TRUE" } + }, + "search_params": [ + { "parallel": 1, "search_params": { "ef": 64 } }, { "parallel": 1, "search_params": { "ef": 128 } }, { "parallel": 1, "search_params": { "ef": 256 } }, { "parallel": 1, "search_params": { "ef": 512 } }, + { "parallel": 100, "search_params": { "ef": 64 } }, { "parallel": 100, "search_params": { "ef": 128 } }, { "parallel": 100, "search_params": { "ef": 256 } }, { "parallel": 100, "search_params": { "ef": 512 } } + ], + "upload_params": { "parallel": 32 } + } +] diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/monitoring/gpu_wrapper.py b/02-semantic-search-rag/fcenedes/vecdb_bench/monitoring/gpu_wrapper.py new file mode 100644 index 0000000..347ac3b --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/monitoring/gpu_wrapper.py @@ -0,0 +1,41 @@ +from http.server import BaseHTTPRequestHandler, HTTPServer +import subprocess + + +class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): + def do_GET(self): + # Parse the query string + command = "gpustat --json --no-header" + + try: + # Execute the command + result = subprocess.run( + command, + shell=True, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + # Send response status code + self.send_response(200) + # Send headers + self.send_header("Content-type", "text/plain") + self.end_headers() + # Write the output to the response + self.wfile.write(result.stdout.encode()) + except subprocess.CalledProcessError as e: + self.send_response(400) + self.end_headers() + self.wfile.write(e.stderr.encode()) + + +def run(server_class=HTTPServer, handler_class=SimpleHTTPRequestHandler, port=8000): + server_address = ("", port) + httpd = server_class(server_address, handler_class) + print(f"Server running on port {port}...") + httpd.serve_forever() + + +if __name__ == "__main__": + run() diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/monitoring/monitor_docker.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/monitoring/monitor_docker.sh new file mode 100644 index 0000000..eb8cc48 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/monitoring/monitor_docker.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -e + + +# write docker stats into file each second +function write_stats { + while true; do + docker stats --no-stream --format "{{ json . }}" >> $1 + sleep 10 + done +} + + +# Ensure that only one instance of this script is running at a time +( + flock -n 200 + write_stats docker.stats.jsonl +) 200>monitor.lock diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/monitoring/results/.keep b/02-semantic-search-rag/fcenedes/vecdb_bench/monitoring/results/.keep new file mode 100644 index 0000000..e69de29 diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/poetry.lock b/02-semantic-search-rag/fcenedes/vecdb_bench/poetry.lock new file mode 100644 index 0000000..3030db7 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/poetry.lock @@ -0,0 +1,3223 @@ +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.11.0" +description = "High-level concurrency and networking framework on top of asyncio or Trio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}, + {file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +trio = ["trio (>=0.31.0)"] + +[[package]] +name = "asttokens" +version = "3.0.0" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, + {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"}, +] + +[package.extras] +astroid = ["astroid (>=2,<4)"] +test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"] + +[[package]] +name = "async-timeout" +version = "5.0.1" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_full_version < \"3.11.3\"" +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + +[[package]] +name = "authlib" +version = "1.6.4" +description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "authlib-1.6.4-py2.py3-none-any.whl", hash = "sha256:39313d2a2caac3ecf6d8f95fbebdfd30ae6ea6ae6a6db794d976405fdd9aa796"}, + {file = "authlib-1.6.4.tar.gz", hash = "sha256:104b0442a43061dc8bc23b133d1d06a2b0a9c2e3e33f34c4338929e816287649"}, +] + +[package.dependencies] +cryptography = "*" + +[[package]] +name = "backoff" +version = "2.2.1" +description = "Function decoration for backoff and retry" +optional = false +python-versions = ">=3.7,<4.0" +groups = ["main"] +files = [ + {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, + {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, +] + +[[package]] +name = "blinker" +version = "1.9.0" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, + {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, +] + +[[package]] +name = "boto3" +version = "1.40.38" +description = "The AWS SDK for Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "boto3-1.40.38-py3-none-any.whl", hash = "sha256:fac337b4f0615e4d6ceee44686e662f51d8e57916ed2bc763468e3e8c611a658"}, + {file = "boto3-1.40.38.tar.gz", hash = "sha256:932ebdd8dbf8ab5694d233df86d5d0950291e0b146c27cb46da8adb4f00f6ca4"}, +] + +[package.dependencies] +botocore = ">=1.40.38,<1.41.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.14.0,<0.15.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.40.38" +description = "Low-level, data-driven core of boto 3." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "botocore-1.40.38-py3-none-any.whl", hash = "sha256:7d60a7557db3a58f9394e7ecec1f6b87495ce947eb713f29d53aee83a6e9dc71"}, + {file = "botocore-1.40.38.tar.gz", hash = "sha256:18039009e1eca2bff12e576e8dd3c80cd9b312294f1469c831de03169582ad59"}, +] + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = [ + {version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""}, + {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""}, +] + +[package.extras] +crt = ["awscrt (==0.27.6)"] + +[[package]] +name = "certifi" +version = "2025.8.3" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, + {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, +] + +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca"}, + {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, + {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, +] + +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version == \"3.9\"" +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "click" +version = "8.3.0" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"}, + {file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {main = "sys_platform == \"win32\" or platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} + +[[package]] +name = "cryptography" +version = "43.0.3" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version == \"3.9\"" +files = [ + {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, + {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, + {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, + {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, + {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, + {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, + {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "cryptography" +version = "46.0.1" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.8" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "cryptography-46.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:1cd6d50c1a8b79af1a6f703709d8973845f677c8e97b1268f5ff323d38ce8475"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ff483716be32690c14636e54a1f6e2e1b7bf8e22ca50b989f88fa1b2d287080"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9873bf7c1f2a6330bdfe8621e7ce64b725784f9f0c3a6a55c3047af5849f920e"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb7c88d4462a0cfdd0d87a3c245a7bc3feb59de101f6ff88194f740f72eda6"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e22801b61613ebdebf7deb18b507919e107547a1d39a3b57f5f855032dd7cfb8"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:757af4f6341ce7a1e47c326ca2a81f41d236070217e5fbbad61bbfe299d55d28"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f7a24ea78de345cfa7f6a8d3bde8b242c7fac27f2bd78fa23474ca38dfaeeab9"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e8776dac9e660c22241b6587fae51a67b4b0147daa4d176b172c3ff768ad736"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9f40642a140c0c8649987027867242b801486865277cbabc8c6059ddef16dc8b"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:449ef2b321bec7d97ef2c944173275ebdab78f3abdd005400cc409e27cd159ab"}, + {file = "cryptography-46.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2dd339ba3345b908fa3141ddba4025568fa6fd398eabce3ef72a29ac2d73ad75"}, + {file = "cryptography-46.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7411c910fb2a412053cf33cfad0153ee20d27e256c6c3f14d7d7d1d9fec59fd5"}, + {file = "cryptography-46.0.1-cp311-abi3-win32.whl", hash = "sha256:cbb8e769d4cac884bb28e3ff620ef1001b75588a5c83c9c9f1fdc9afbe7f29b0"}, + {file = "cryptography-46.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:92e8cfe8bd7dd86eac0a677499894862cd5cc2fd74de917daa881d00871ac8e7"}, + {file = "cryptography-46.0.1-cp311-abi3-win_arm64.whl", hash = "sha256:db5597a4c7353b2e5fb05a8e6cb74b56a4658a2b7bf3cb6b1821ae7e7fd6eaa0"}, + {file = "cryptography-46.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:4c49eda9a23019e11d32a0eb51a27b3e7ddedde91e099c0ac6373e3aacc0d2ee"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9babb7818fdd71394e576cf26c5452df77a355eac1a27ddfa24096665a27f8fd"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9f2c4cc63be3ef43c0221861177cee5d14b505cd4d4599a89e2cd273c4d3542a"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:41c281a74df173876da1dc9a9b6953d387f06e3d3ed9284e3baae3ab3f40883a"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0a17377fa52563d730248ba1f68185461fff36e8bc75d8787a7dd2e20a802b7a"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:0d1922d9280e08cde90b518a10cd66831f632960a8d08cb3418922d83fce6f12"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:af84e8e99f1a82cea149e253014ea9dc89f75b82c87bb6c7242203186f465129"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ef648d2c690703501714588b2ba640facd50fd16548133b11b2859e8655a69da"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:e94eb5fa32a8a9f9bf991f424f002913e3dd7c699ef552db9b14ba6a76a6313b"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:534b96c0831855e29fc3b069b085fd185aa5353033631a585d5cd4dd5d40d657"}, + {file = "cryptography-46.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9b55038b5c6c47559aa33626d8ecd092f354e23de3c6975e4bb205df128a2a0"}, + {file = "cryptography-46.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ec13b7105117dbc9afd023300fb9954d72ca855c274fe563e72428ece10191c0"}, + {file = "cryptography-46.0.1-cp314-cp314t-win32.whl", hash = "sha256:504e464944f2c003a0785b81668fe23c06f3b037e9cb9f68a7c672246319f277"}, + {file = "cryptography-46.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c52fded6383f7e20eaf70a60aeddd796b3677c3ad2922c801be330db62778e05"}, + {file = "cryptography-46.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9495d78f52c804b5ec8878b5b8c7873aa8e63db9cd9ee387ff2db3fffe4df784"}, + {file = "cryptography-46.0.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d84c40bdb8674c29fa192373498b6cb1e84f882889d21a471b45d1f868d8d44b"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9ed64e5083fa806709e74fc5ea067dfef9090e5b7a2320a49be3c9df3583a2d8"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:341fb7a26bc9d6093c1b124b9f13acc283d2d51da440b98b55ab3f79f2522ead"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6ef1488967e729948d424d09c94753d0167ce59afba8d0f6c07a22b629c557b2"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7823bc7cdf0b747ecfb096d004cc41573c2f5c7e3a29861603a2871b43d3ef32"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:f736ab8036796f5a119ff8211deda416f8c15ce03776db704a7a4e17381cb2ef"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e46710a240a41d594953012213ea8ca398cd2448fbc5d0f1be8160b5511104a0"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:84ef1f145de5aee82ea2447224dc23f065ff4cc5791bb3b506615957a6ba8128"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9394c7d5a7565ac5f7d9ba38b2617448eba384d7b107b262d63890079fad77ca"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ed957044e368ed295257ae3d212b95456bd9756df490e1ac4538857f67531fcc"}, + {file = "cryptography-46.0.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f7de12fa0eee6234de9a9ce0ffcfa6ce97361db7a50b09b65c63ac58e5f22fc7"}, + {file = "cryptography-46.0.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7fab1187b6c6b2f11a326f33b036f7168f5b996aedd0c059f9738915e4e8f53a"}, + {file = "cryptography-46.0.1-cp38-abi3-win32.whl", hash = "sha256:45f790934ac1018adeba46a0f7289b2b8fe76ba774a88c7f1922213a56c98bc1"}, + {file = "cryptography-46.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:7176a5ab56fac98d706921f6416a05e5aff7df0e4b91516f450f8627cda22af3"}, + {file = "cryptography-46.0.1-cp38-abi3-win_arm64.whl", hash = "sha256:efc9e51c3e595267ff84adf56e9b357db89ab2279d7e375ffcaf8f678606f3d9"}, + {file = "cryptography-46.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd4b5e2ee4e60425711ec65c33add4e7a626adef79d66f62ba0acfd493af282d"}, + {file = "cryptography-46.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:48948940d0ae00483e85e9154bb42997d0b77c21e43a77b7773c8c80de532ac5"}, + {file = "cryptography-46.0.1-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b9c79af2c3058430d911ff1a5b2b96bbfe8da47d5ed961639ce4681886614e70"}, + {file = "cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0ca4be2af48c24df689a150d9cd37404f689e2968e247b6b8ff09bff5bcd786f"}, + {file = "cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:13e67c4d3fb8b6bc4ef778a7ccdd8df4cd15b4bcc18f4239c8440891a11245cc"}, + {file = "cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:15b5fd9358803b0d1cc42505a18d8bca81dabb35b5cfbfea1505092e13a9d96d"}, + {file = "cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e34da95e29daf8a71cb2841fd55df0511539a6cdf33e6f77c1e95e44006b9b46"}, + {file = "cryptography-46.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:34f04b7311174469ab3ac2647469743720f8b6c8b046f238e5cb27905695eb2a"}, + {file = "cryptography-46.0.1.tar.gz", hash = "sha256:ed570874e88f213437f5cf758f9ef26cbfc3f336d889b1e592ee11283bb8d1c7"}, +] + +[package.dependencies] +cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} +typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox[uv] (>=2024.4.15)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==46.0.1)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "dash" +version = "3.3.0" +description = "A Python framework for building reactive web-apps. Developed by Plotly." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "dash-3.3.0-py3-none-any.whl", hash = "sha256:8f52415977f7490492dd8a3872279160be8ff253ca9f4d49a4e3ba747fa4bd91"}, + {file = "dash-3.3.0.tar.gz", hash = "sha256:eaaa7a671540b5e1db8066f4966d0277d21edc2c7acdaec2fd6d198366a8b0df"}, +] + +[package.dependencies] +Flask = ">=1.0.4,<3.2" +importlib-metadata = "*" +nest-asyncio = "*" +plotly = ">=5.0.0" +requests = "*" +retrying = "*" +setuptools = "*" +typing_extensions = ">=4.1.1" +Werkzeug = "<3.2" + +[package.extras] +ag-grid = ["dash-ag-grid"] +async = ["flask[async]"] +celery = ["celery[redis] (>=5.1.2,<5.4.0)", "kombu (<5.4.0)", "redis (>=3.5.3,<=5.0.4)"] +ci = ["black (==22.3.0)", "flake8 (==7.0.0)", "flaky (==3.8.1)", "flask-talisman (==1.0.0)", "ipython (<9.0.0)", "jupyterlab (<4.0.0)", "mimesis (<=11.1.0)", "mock (==4.0.3)", "mypy (==1.15.0) ; python_version >= \"3.12\"", "numpy (<=1.26.3)", "openpyxl", "orjson (>=3.10.11)", "pandas (>=1.4.0)", "pyarrow", "pylint (==3.0.3)", "pyright (==1.1.398) ; python_version >= \"3.7\"", "pytest-mock", "pytest-rerunfailures", "pytest-sugar (==0.9.6)", "pyzmq (>=26.0.0)", "xlrd (>=2.0.1)"] +cloud = ["plotly-cloud"] +compress = ["flask-compress"] +dev = ["PyYAML (>=5.4.1)", "coloredlogs (>=15.0.1)", "fire (>=0.4.0)"] +diskcache = ["diskcache (>=5.2.1)", "multiprocess (>=0.70.12)", "psutil (>=5.8.0)"] +testing = ["beautifulsoup4 (>=4.8.2)", "cryptography", "dash_testing_stub (>=0.0.2)", "lxml (>=4.6.2)", "multiprocess (>=0.70.12)", "percy (>=2.0.2)", "psutil (>=5.8.0)", "pytest (>=6.0.2)", "requests[security] (>=2.21.0)", "selenium (>=3.141.0,<=4.2.0)", "waitress (>=1.4.4)"] + +[[package]] +name = "decorator" +version = "5.2.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"}, + {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, +] + +[[package]] +name = "deprecation" +version = "2.1.0" +description = "A library to handle automated deprecations" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a"}, + {file = "deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff"}, +] + +[package.dependencies] +packaging = "*" + +[[package]] +name = "distlib" +version = "0.4.0" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, + {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +description = "DNS toolkit" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version == \"3.9\"" +files = [ + {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"}, + {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"}, +] + +[package.extras] +dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.16.0)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "quart-trio (>=0.11.0)", "sphinx (>=7.2.0)", "sphinx-rtd-theme (>=2.0.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] +dnssec = ["cryptography (>=43)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] +doq = ["aioquic (>=1.0.0)"] +idna = ["idna (>=3.7)"] +trio = ["trio (>=0.23)"] +wmi = ["wmi (>=1.5.1)"] + +[[package]] +name = "dnspython" +version = "2.8.0" +description = "DNS toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af"}, + {file = "dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f"}, +] + +[package.extras] +dev = ["black (>=25.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.17.0)", "mypy (>=1.17)", "pylint (>=3)", "pytest (>=8.4)", "pytest-cov (>=6.2.0)", "quart-trio (>=0.12.0)", "sphinx (>=8.2.0)", "sphinx-rtd-theme (>=3.0.0)", "twine (>=6.1.0)", "wheel (>=0.45.0)"] +dnssec = ["cryptography (>=45)"] +doh = ["h2 (>=4.2.0)", "httpcore (>=1.0.0)", "httpx (>=0.28.0)"] +doq = ["aioquic (>=1.2.0)"] +idna = ["idna (>=3.10)"] +trio = ["trio (>=0.30)"] +wmi = ["wmi (>=1.5.1) ; platform_system == \"Windows\""] + +[[package]] +name = "elastic-transport" +version = "8.17.1" +description = "Transport classes and utilities shared among Python Elastic client libraries" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "elastic_transport-8.17.1-py3-none-any.whl", hash = "sha256:192718f498f1d10c5e9aa8b9cf32aed405e469a7f0e9d6a8923431dbb2c59fb8"}, + {file = "elastic_transport-8.17.1.tar.gz", hash = "sha256:5edef32ac864dca8e2f0a613ef63491ee8d6b8cfb52881fa7313ba9290cac6d2"}, +] + +[package.dependencies] +certifi = "*" +urllib3 = ">=1.26.2,<3" + +[package.extras] +develop = ["aiohttp", "furo", "httpx", "opentelemetry-api", "opentelemetry-sdk", "orjson", "pytest", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "pytest-mock", "requests", "respx", "sphinx (>2)", "sphinx-autodoc-typehints", "trustme"] + +[[package]] +name = "elasticsearch" +version = "8.19.1" +description = "Python client for Elasticsearch" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "elasticsearch-8.19.1-py3-none-any.whl", hash = "sha256:b511dd4cc26aef1fc02c1b5c1eefe9f16d76127d8f133b0f6900fd052c48d2a4"}, + {file = "elasticsearch-8.19.1.tar.gz", hash = "sha256:7192cd6eff2d6ab48f2858265dea717d15ab42f04cfdc4f571a811bcfcbc7a3a"}, +] + +[package.dependencies] +elastic-transport = ">=8.15.1,<9" +python-dateutil = "*" +typing-extensions = "*" + +[package.extras] +async = ["aiohttp (>=3,<4)"] +dev = ["aiohttp", "black", "build", "coverage", "isort", "jinja2", "mapbox-vector-tile", "mypy", "nox", "numpy", "orjson", "pandas", "pyarrow", "pyright", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock", "python-dateutil", "pyyaml (>=5.4)", "requests (>=2,<3)", "simsimd", "tqdm", "twine", "types-python-dateutil", "types-tqdm", "unasync"] +docs = ["sphinx", "sphinx-autodoc-typehints", "sphinx-rtd-theme (>=2.0)"] +orjson = ["orjson (>=3)"] +pyarrow = ["pyarrow (>=1)"] +requests = ["requests (>=2.4.0,!=2.32.2,<3.0.0)"] +vectorstore-mmr = ["numpy (>=1)", "simsimd (>=3)"] + +[[package]] +name = "events" +version = "0.5" +description = "Bringing the elegance of C# EventHandler to Python" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "Events-0.5-py3-none-any.whl", hash = "sha256:a7286af378ba3e46640ac9825156c93bdba7502174dd696090fdfcd4d80a1abd"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "executing" +version = "2.2.1" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017"}, + {file = "executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4"}, +] + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich ; python_version >= \"3.11\""] + +[[package]] +name = "filelock" +version = "3.19.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d"}, + {file = "filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58"}, +] + +[[package]] +name = "flask" +version = "3.1.2" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c"}, + {file = "flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87"}, +] + +[package.dependencies] +blinker = ">=1.9.0" +click = ">=8.1.3" +importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} +itsdangerous = ">=2.2.0" +jinja2 = ">=3.1.2" +markupsafe = ">=2.1.1" +werkzeug = ">=3.1.0" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "grpcio" +version = "1.75.0" +description = "HTTP/2-based RPC framework" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "grpcio-1.75.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:1ec9cbaec18d9597c718b1ed452e61748ac0b36ba350d558f9ded1a94cc15ec7"}, + {file = "grpcio-1.75.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:7ee5ee42bfae8238b66a275f9ebcf6f295724375f2fa6f3b52188008b6380faf"}, + {file = "grpcio-1.75.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9146e40378f551eed66c887332afc807fcce593c43c698e21266a4227d4e20d2"}, + {file = "grpcio-1.75.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0c40f368541945bb664857ecd7400acb901053a1abbcf9f7896361b2cfa66798"}, + {file = "grpcio-1.75.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:50a6e43a9adc6938e2a16c9d9f8a2da9dd557ddd9284b73b07bd03d0e098d1e9"}, + {file = "grpcio-1.75.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dce15597ca11913b78e1203c042d5723e3ea7f59e7095a1abd0621be0e05b895"}, + {file = "grpcio-1.75.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:851194eec47755101962da423f575ea223c9dd7f487828fe5693920e8745227e"}, + {file = "grpcio-1.75.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ca123db0813eef80625a4242a0c37563cb30a3edddebe5ee65373854cf187215"}, + {file = "grpcio-1.75.0-cp310-cp310-win32.whl", hash = "sha256:222b0851e20c04900c63f60153503e918b08a5a0fad8198401c0b1be13c6815b"}, + {file = "grpcio-1.75.0-cp310-cp310-win_amd64.whl", hash = "sha256:bb58e38a50baed9b21492c4b3f3263462e4e37270b7ea152fc10124b4bd1c318"}, + {file = "grpcio-1.75.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:7f89d6d0cd43170a80ebb4605cad54c7d462d21dc054f47688912e8bf08164af"}, + {file = "grpcio-1.75.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:cb6c5b075c2d092f81138646a755f0dad94e4622300ebef089f94e6308155d82"}, + {file = "grpcio-1.75.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:494dcbade5606128cb9f530ce00331a90ecf5e7c5b243d373aebdb18e503c346"}, + {file = "grpcio-1.75.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:050760fd29c8508844a720f06c5827bb00de8f5e02f58587eb21a4444ad706e5"}, + {file = "grpcio-1.75.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:266fa6209b68a537b2728bb2552f970e7e78c77fe43c6e9cbbe1f476e9e5c35f"}, + {file = "grpcio-1.75.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:06d22e1d8645e37bc110f4c589cb22c283fd3de76523065f821d6e81de33f5d4"}, + {file = "grpcio-1.75.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9880c323595d851292785966cadb6c708100b34b163cab114e3933f5773cba2d"}, + {file = "grpcio-1.75.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:55a2d5ae79cd0f68783fb6ec95509be23746e3c239290b2ee69c69a38daa961a"}, + {file = "grpcio-1.75.0-cp311-cp311-win32.whl", hash = "sha256:352dbdf25495eef584c8de809db280582093bc3961d95a9d78f0dfb7274023a2"}, + {file = "grpcio-1.75.0-cp311-cp311-win_amd64.whl", hash = "sha256:678b649171f229fb16bda1a2473e820330aa3002500c4f9fd3a74b786578e90f"}, + {file = "grpcio-1.75.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:fa35ccd9501ffdd82b861809cbfc4b5b13f4b4c5dc3434d2d9170b9ed38a9054"}, + {file = "grpcio-1.75.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:0fcb77f2d718c1e58cc04ef6d3b51e0fa3b26cf926446e86c7eba105727b6cd4"}, + {file = "grpcio-1.75.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36764a4ad9dc1eb891042fab51e8cdf7cc014ad82cee807c10796fb708455041"}, + {file = "grpcio-1.75.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:725e67c010f63ef17fc052b261004942763c0b18dcd84841e6578ddacf1f9d10"}, + {file = "grpcio-1.75.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91fbfc43f605c5ee015c9056d580a70dd35df78a7bad97e05426795ceacdb59f"}, + {file = "grpcio-1.75.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a9337ac4ce61c388e02019d27fa837496c4b7837cbbcec71b05934337e51531"}, + {file = "grpcio-1.75.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ee16e232e3d0974750ab5f4da0ab92b59d6473872690b5e40dcec9a22927f22e"}, + {file = "grpcio-1.75.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55dfb9122973cc69520b23d39867726722cafb32e541435707dc10249a1bdbc6"}, + {file = "grpcio-1.75.0-cp312-cp312-win32.whl", hash = "sha256:fb64dd62face3d687a7b56cd881e2ea39417af80f75e8b36f0f81dfd93071651"}, + {file = "grpcio-1.75.0-cp312-cp312-win_amd64.whl", hash = "sha256:6b365f37a9c9543a9e91c6b4103d68d38d5bcb9965b11d5092b3c157bd6a5ee7"}, + {file = "grpcio-1.75.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:1bb78d052948d8272c820bb928753f16a614bb2c42fbf56ad56636991b427518"}, + {file = "grpcio-1.75.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:9dc4a02796394dd04de0b9673cb79a78901b90bb16bf99ed8cb528c61ed9372e"}, + {file = "grpcio-1.75.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:437eeb16091d31498585d73b133b825dc80a8db43311e332c08facf820d36894"}, + {file = "grpcio-1.75.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:c2c39984e846bd5da45c5f7bcea8fafbe47c98e1ff2b6f40e57921b0c23a52d0"}, + {file = "grpcio-1.75.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38d665f44b980acdbb2f0e1abf67605ba1899f4d2443908df9ec8a6f26d2ed88"}, + {file = "grpcio-1.75.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e8e752ab5cc0a9c5b949808c000ca7586223be4f877b729f034b912364c3964"}, + {file = "grpcio-1.75.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3a6788b30aa8e6f207c417874effe3f79c2aa154e91e78e477c4825e8b431ce0"}, + {file = "grpcio-1.75.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc33e67cab6141c54e75d85acd5dec616c5095a957ff997b4330a6395aa9b51"}, + {file = "grpcio-1.75.0-cp313-cp313-win32.whl", hash = "sha256:c8cfc780b7a15e06253aae5f228e1e84c0d3c4daa90faf5bc26b751174da4bf9"}, + {file = "grpcio-1.75.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c91d5b16eff3cbbe76b7a1eaaf3d91e7a954501e9d4f915554f87c470475c3d"}, + {file = "grpcio-1.75.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:0b85f4ebe6b56d2a512201bb0e5f192c273850d349b0a74ac889ab5d38959d16"}, + {file = "grpcio-1.75.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:68c95b1c1e3bf96ceadf98226e9dfe2bc92155ce352fa0ee32a1603040e61856"}, + {file = "grpcio-1.75.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:153c5a7655022c3626ad70be3d4c2974cb0967f3670ee49ece8b45b7a139665f"}, + {file = "grpcio-1.75.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:53067c590ac3638ad0c04272f2a5e7e32a99fec8824c31b73bc3ef93160511fa"}, + {file = "grpcio-1.75.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:78dcc025a144319b66df6d088bd0eda69e1719eb6ac6127884a36188f336df19"}, + {file = "grpcio-1.75.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ec2937fd92b5b4598cbe65f7e57d66039f82b9e2b7f7a5f9149374057dde77d"}, + {file = "grpcio-1.75.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:597340a41ad4b619aaa5c9b94f7e6ba4067885386342ab0af039eda945c255cd"}, + {file = "grpcio-1.75.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0aa795198b28807d28570c0a5f07bb04d5facca7d3f27affa6ae247bbd7f312a"}, + {file = "grpcio-1.75.0-cp39-cp39-win32.whl", hash = "sha256:585147859ff4603798e92605db28f4a97c821c69908e7754c44771c27b239bbd"}, + {file = "grpcio-1.75.0-cp39-cp39-win_amd64.whl", hash = "sha256:eafbe3563f9cb378370a3fa87ef4870539cf158124721f3abee9f11cd8162460"}, + {file = "grpcio-1.75.0.tar.gz", hash = "sha256:b989e8b09489478c2d19fecc744a298930f40d8b27c3638afbfe84d22f36ce4e"}, +] + +[package.dependencies] +typing-extensions = ">=4.12,<5.0" + +[package.extras] +protobuf = ["grpcio-tools (>=1.75.0)"] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "h2" +version = "4.3.0" +description = "Pure-Python HTTP/2 protocol implementation" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd"}, + {file = "h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1"}, +] + +[package.dependencies] +hpack = ">=4.1,<5" +hyperframe = ">=6.1,<7" + +[[package]] +name = "h5py" +version = "3.14.0" +description = "Read and write HDF5 files from Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "h5py-3.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:24df6b2622f426857bda88683b16630014588a0e4155cba44e872eb011c4eaed"}, + {file = "h5py-3.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ff2389961ee5872de697054dd5a033b04284afc3fb52dc51d94561ece2c10c6"}, + {file = "h5py-3.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:016e89d3be4c44f8d5e115fab60548e518ecd9efe9fa5c5324505a90773e6f03"}, + {file = "h5py-3.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1223b902ef0b5d90bcc8a4778218d6d6cd0f5561861611eda59fa6c52b922f4d"}, + {file = "h5py-3.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:852b81f71df4bb9e27d407b43071d1da330d6a7094a588efa50ef02553fa7ce4"}, + {file = "h5py-3.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f30dbc58f2a0efeec6c8836c97f6c94afd769023f44e2bb0ed7b17a16ec46088"}, + {file = "h5py-3.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:543877d7f3d8f8a9828ed5df6a0b78ca3d8846244b9702e99ed0d53610b583a8"}, + {file = "h5py-3.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c497600c0496548810047257e36360ff551df8b59156d3a4181072eed47d8ad"}, + {file = "h5py-3.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:723a40ee6505bd354bfd26385f2dae7bbfa87655f4e61bab175a49d72ebfc06b"}, + {file = "h5py-3.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:d2744b520440a996f2dae97f901caa8a953afc055db4673a993f2d87d7f38713"}, + {file = "h5py-3.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e0045115d83272090b0717c555a31398c2c089b87d212ceba800d3dc5d952e23"}, + {file = "h5py-3.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6da62509b7e1d71a7d110478aa25d245dd32c8d9a1daee9d2a42dba8717b047a"}, + {file = "h5py-3.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554ef0ced3571366d4d383427c00c966c360e178b5fb5ee5bb31a435c424db0c"}, + {file = "h5py-3.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cbd41f4e3761f150aa5b662df991868ca533872c95467216f2bec5fcad84882"}, + {file = "h5py-3.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:bf4897d67e613ecf5bdfbdab39a1158a64df105827da70ea1d90243d796d367f"}, + {file = "h5py-3.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aa4b7bbce683379b7bf80aaba68e17e23396100336a8d500206520052be2f812"}, + {file = "h5py-3.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9603a501a04fcd0ba28dd8f0995303d26a77a980a1f9474b3417543d4c6174"}, + {file = "h5py-3.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8cbaf6910fa3983c46172666b0b8da7b7bd90d764399ca983236f2400436eeb"}, + {file = "h5py-3.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d90e6445ab7c146d7f7981b11895d70bc1dd91278a4f9f9028bc0c95e4a53f13"}, + {file = "h5py-3.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:ae18e3de237a7a830adb76aaa68ad438d85fe6e19e0d99944a3ce46b772c69b3"}, + {file = "h5py-3.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5cc1601e78027cedfec6dd50efb4802f018551754191aeb58d948bd3ec3bd7a"}, + {file = "h5py-3.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e59d2136a8b302afd25acdf7a89b634e0eb7c66b1a211ef2d0457853768a2ef"}, + {file = "h5py-3.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:573c33ad056ac7c1ab6d567b6db9df3ffc401045e3f605736218f96c1e0490c6"}, + {file = "h5py-3.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccbe17dc187c0c64178f1a10aa274ed3a57d055117588942b8a08793cc448216"}, + {file = "h5py-3.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f025cf30ae738c4c4e38c7439a761a71ccfcce04c2b87b2a2ac64e8c5171d43"}, + {file = "h5py-3.14.0.tar.gz", hash = "sha256:2372116b2e0d5d3e5e705b7f663f7c8d96fa79a4052d250484ef91d24d6a08f4"}, +] + +[package.dependencies] +numpy = ">=1.19.3" + +[[package]] +name = "hpack" +version = "4.1.0" +description = "Pure-Python HPACK header encoding" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"}, + {file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"}, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +h2 = {version = ">=3,<5", optional = true, markers = "extra == \"http2\""} +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "hyperframe" +version = "6.1.0" +description = "Pure-Python HTTP/2 framing" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"}, + {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, +] + +[[package]] +name = "identify" +version = "2.6.14" +description = "File identification library for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "identify-2.6.14-py2.py3-none-any.whl", hash = "sha256:11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e"}, + {file = "identify-2.6.14.tar.gz", hash = "sha256:663494103b4f717cb26921c52f8751363dc89db64364cd836a9bf1535f53cd6a"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, + {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +perf = ["ipython"] +test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] + +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "ipdb" +version = "0.13.13" +description = "IPython-enabled pdb" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +files = [ + {file = "ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4"}, + {file = "ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726"}, +] + +[package.dependencies] +decorator = {version = "*", markers = "python_version > \"3.6\" and python_version < \"3.11\""} +ipython = {version = ">=7.31.1", markers = "python_version > \"3.6\" and python_version < \"3.11\""} +tomli = {version = "*", markers = "python_version > \"3.6\" and python_version < \"3.11\""} + +[[package]] +name = "ipython" +version = "8.18.1" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version == \"3.9\"" +files = [ + {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, + {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +prompt-toolkit = ">=3.0.41,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} + +[package.extras] +all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] + +[[package]] +name = "ipython" +version = "8.37.0" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version == \"3.10\"" +files = [ + {file = "ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2"}, + {file = "ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} +prompt_toolkit = ">=3.0.41,<3.1.0" +pygments = ">=2.4.0" +stack_data = "*" +traitlets = ">=5.13.0" +typing_extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} + +[package.extras] +all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli ; python_version < \"3.11\"", "typing_extensions"] +kernel = ["ipykernel"] +matplotlib = ["matplotlib"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "ipython[test]", "jupyter_ai", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] + +[[package]] +name = "ipython" +version = "9.5.0" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.11" +groups = ["main"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "ipython-9.5.0-py3-none-any.whl", hash = "sha256:88369ffa1d5817d609120daa523a6da06d02518e582347c29f8451732a9c5e72"}, + {file = "ipython-9.5.0.tar.gz", hash = "sha256:129c44b941fe6d9b82d36fc7a7c18127ddb1d6f02f78f867f402e2e3adde3113"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +ipython-pygments-lexers = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} +prompt_toolkit = ">=3.0.41,<3.1.0" +pygments = ">=2.4.0" +stack_data = "*" +traitlets = ">=5.13.0" +typing_extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} + +[package.extras] +all = ["ipython[doc,matplotlib,test,test-extra]"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinx_toml (==0.0.4)", "typing_extensions"] +matplotlib = ["matplotlib"] +test = ["packaging", "pytest", "pytest-asyncio", "testpath"] +test-extra = ["curio", "ipykernel", "ipython[test]", "jupyter_ai", "matplotlib (!=3.2.0)", "nbclient", "nbformat", "numpy (>=1.23)", "pandas", "trio"] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +description = "Defines a variety of Pygments lexers for highlighting IPython code." +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c"}, + {file = "ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81"}, +] + +[package.dependencies] +pygments = "*" + +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[[package]] +name = "jedi" +version = "0.19.2" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, + {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, +] + +[package.dependencies] +parso = ">=0.8.4,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] + +[[package]] +name = "jsons" +version = "1.6.3" +description = "For serializing Python objects to JSON (dicts) and back" +optional = false +python-versions = ">=3.5" +groups = ["main"] +files = [ + {file = "jsons-1.6.3-py3-none-any.whl", hash = "sha256:f07f8919316f72a3843c7ca6cc6c900513089f10092626934d1bfe4b5cf15401"}, + {file = "jsons-1.6.3.tar.gz", hash = "sha256:cd5815c7c6790ae11c70ad9978e0aa850d0d08a643a5105cc604eac8b29a30d7"}, +] + +[package.dependencies] +typish = ">=1.9.2" + +[package.extras] +test = ["attrs", "codecov", "coverage", "dataclasses ; python_version == \"3.6\"", "pytest", "scons", "tzdata ; python_version >= \"3.9\""] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version == \"3.9\"" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] + +[[package]] +name = "markupsafe" +version = "3.0.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, + {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, +] + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "ml-dtypes" +version = "0.4.1" +description = "" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "ml_dtypes-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1fe8b5b5e70cd67211db94b05cfd58dace592f24489b038dc6f9fe347d2e07d5"}, + {file = "ml_dtypes-0.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c09a6d11d8475c2a9fd2bc0695628aec105f97cab3b3a3fb7c9660348ff7d24"}, + {file = "ml_dtypes-0.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5e8f75fa371020dd30f9196e7d73babae2abd51cf59bdd56cb4f8de7e13354"}, + {file = "ml_dtypes-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:15fdd922fea57e493844e5abb930b9c0bd0af217d9edd3724479fc3d7ce70e3f"}, + {file = "ml_dtypes-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2d55b588116a7085d6e074cf0cdb1d6fa3875c059dddc4d2c94a4cc81c23e975"}, + {file = "ml_dtypes-0.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e138a9b7a48079c900ea969341a5754019a1ad17ae27ee330f7ebf43f23877f9"}, + {file = "ml_dtypes-0.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74c6cfb5cf78535b103fde9ea3ded8e9f16f75bc07789054edc7776abfb3d752"}, + {file = "ml_dtypes-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:274cc7193dd73b35fb26bef6c5d40ae3eb258359ee71cd82f6e96a8c948bdaa6"}, + {file = "ml_dtypes-0.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:827d3ca2097085cf0355f8fdf092b888890bb1b1455f52801a2d7756f056f54b"}, + {file = "ml_dtypes-0.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:772426b08a6172a891274d581ce58ea2789cc8abc1c002a27223f314aaf894e7"}, + {file = "ml_dtypes-0.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126e7d679b8676d1a958f2651949fbfa182832c3cd08020d8facd94e4114f3e9"}, + {file = "ml_dtypes-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:df0fb650d5c582a9e72bb5bd96cfebb2cdb889d89daff621c8fbc60295eba66c"}, + {file = "ml_dtypes-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e35e486e97aee577d0890bc3bd9e9f9eece50c08c163304008587ec8cfe7575b"}, + {file = "ml_dtypes-0.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:560be16dc1e3bdf7c087eb727e2cf9c0e6a3d87e9f415079d2491cc419b3ebf5"}, + {file = "ml_dtypes-0.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad0b757d445a20df39035c4cdeed457ec8b60d236020d2560dbc25887533cf50"}, + {file = "ml_dtypes-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:ef0d7e3fece227b49b544fa69e50e607ac20948f0043e9f76b44f35f229ea450"}, + {file = "ml_dtypes-0.4.1.tar.gz", hash = "sha256:fad5f2de464fd09127e49b7fd1252b9006fb43d2edc1ff112d390c324af5ca7a"}, +] + +[package.dependencies] +numpy = [ + {version = ">1.20"}, + {version = ">=1.23.3", markers = "python_version >= \"3.11\""}, + {version = ">=1.21.2", markers = "python_version == \"3.10\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, +] + +[package.extras] +dev = ["absl-py", "pyink", "pylint (>=2.6.0)", "pytest", "pytest-xdist"] + +[[package]] +name = "narwhals" +version = "2.15.0" +description = "Extremely lightweight compatibility layer between dataframe libraries" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "narwhals-2.15.0-py3-none-any.whl", hash = "sha256:cbfe21ca19d260d9fd67f995ec75c44592d1f106933b03ddd375df7ac841f9d6"}, + {file = "narwhals-2.15.0.tar.gz", hash = "sha256:a9585975b99d95084268445a1fdd881311fa26ef1caa18020d959d5b2ff9a965"}, +] + +[package.extras] +cudf = ["cudf (>=24.10.0)"] +dask = ["dask[dataframe] (>=2024.8)"] +duckdb = ["duckdb (>=1.1)"] +ibis = ["ibis-framework (>=6.0.0)", "packaging", "pyarrow-hotfix", "rich"] +modin = ["modin"] +pandas = ["pandas (>=1.1.3)"] +polars = ["polars (>=0.20.4)"] +pyarrow = ["pyarrow (>=13.0.0)"] +pyspark = ["pyspark (>=3.5.0)"] +pyspark-connect = ["pyspark[connect] (>=3.5.0)"] +sqlframe = ["sqlframe (>=3.22.0,!=3.39.3)"] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +groups = ["main"] +files = [ + {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, + {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "numpy" +version = "2.0.2" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version == \"3.9\"" +files = [ + {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b"}, + {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd"}, + {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318"}, + {file = "numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8"}, + {file = "numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326"}, + {file = "numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97"}, + {file = "numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a"}, + {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669"}, + {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951"}, + {file = "numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9"}, + {file = "numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15"}, + {file = "numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4"}, + {file = "numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c"}, + {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692"}, + {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a"}, + {file = "numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c"}, + {file = "numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded"}, + {file = "numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5"}, + {file = "numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729"}, + {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1"}, + {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd"}, + {file = "numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d"}, + {file = "numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d"}, + {file = "numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa"}, + {file = "numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"}, + {file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"}, +] + +[[package]] +name = "numpy" +version = "2.2.6" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version == \"3.10\"" +files = [ + {file = "numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb"}, + {file = "numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90"}, + {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163"}, + {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf"}, + {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83"}, + {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915"}, + {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680"}, + {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289"}, + {file = "numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d"}, + {file = "numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3"}, + {file = "numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae"}, + {file = "numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a"}, + {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42"}, + {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491"}, + {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a"}, + {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf"}, + {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1"}, + {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab"}, + {file = "numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47"}, + {file = "numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303"}, + {file = "numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff"}, + {file = "numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c"}, + {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3"}, + {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282"}, + {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87"}, + {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249"}, + {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49"}, + {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de"}, + {file = "numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4"}, + {file = "numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2"}, + {file = "numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84"}, + {file = "numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b"}, + {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d"}, + {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566"}, + {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f"}, + {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f"}, + {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868"}, + {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d"}, + {file = "numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd"}, + {file = "numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c"}, + {file = "numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6"}, + {file = "numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda"}, + {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40"}, + {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8"}, + {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f"}, + {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa"}, + {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571"}, + {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1"}, + {file = "numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff"}, + {file = "numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06"}, + {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d"}, + {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db"}, + {file = "numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543"}, + {file = "numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00"}, + {file = "numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd"}, +] + +[[package]] +name = "numpy" +version = "2.3.3" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.11" +groups = ["main"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "numpy-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d"}, + {file = "numpy-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569"}, + {file = "numpy-2.3.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f"}, + {file = "numpy-2.3.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125"}, + {file = "numpy-2.3.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48"}, + {file = "numpy-2.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6"}, + {file = "numpy-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa"}, + {file = "numpy-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30"}, + {file = "numpy-2.3.3-cp311-cp311-win32.whl", hash = "sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57"}, + {file = "numpy-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa"}, + {file = "numpy-2.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7"}, + {file = "numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf"}, + {file = "numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25"}, + {file = "numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe"}, + {file = "numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b"}, + {file = "numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8"}, + {file = "numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20"}, + {file = "numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea"}, + {file = "numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7"}, + {file = "numpy-2.3.3-cp312-cp312-win32.whl", hash = "sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf"}, + {file = "numpy-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb"}, + {file = "numpy-2.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5"}, + {file = "numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf"}, + {file = "numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7"}, + {file = "numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6"}, + {file = "numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7"}, + {file = "numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c"}, + {file = "numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93"}, + {file = "numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae"}, + {file = "numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86"}, + {file = "numpy-2.3.3-cp313-cp313-win32.whl", hash = "sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8"}, + {file = "numpy-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf"}, + {file = "numpy-2.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5"}, + {file = "numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc"}, + {file = "numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc"}, + {file = "numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b"}, + {file = "numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19"}, + {file = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30"}, + {file = "numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e"}, + {file = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3"}, + {file = "numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea"}, + {file = "numpy-2.3.3-cp313-cp313t-win32.whl", hash = "sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd"}, + {file = "numpy-2.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d"}, + {file = "numpy-2.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1"}, + {file = "numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593"}, + {file = "numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652"}, + {file = "numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7"}, + {file = "numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a"}, + {file = "numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe"}, + {file = "numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421"}, + {file = "numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021"}, + {file = "numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf"}, + {file = "numpy-2.3.3-cp314-cp314-win32.whl", hash = "sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0"}, + {file = "numpy-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8"}, + {file = "numpy-2.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe"}, + {file = "numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00"}, + {file = "numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a"}, + {file = "numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d"}, + {file = "numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a"}, + {file = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54"}, + {file = "numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e"}, + {file = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097"}, + {file = "numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970"}, + {file = "numpy-2.3.3-cp314-cp314t-win32.whl", hash = "sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5"}, + {file = "numpy-2.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f"}, + {file = "numpy-2.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db"}, + {file = "numpy-2.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc"}, + {file = "numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029"}, +] + +[[package]] +name = "opensearch-py" +version = "2.8.0" +description = "Python client for OpenSearch" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "opensearch_py-2.8.0-py3-none-any.whl", hash = "sha256:52c60fdb5d4dcf6cce3ee746c13b194529b0161e0f41268b98ab8f1624abe2fa"}, + {file = "opensearch_py-2.8.0.tar.gz", hash = "sha256:6598df0bc7a003294edd0ba88a331e0793acbb8c910c43edf398791e3b2eccda"}, +] + +[package.dependencies] +certifi = ">=2024.07.04" +Events = "*" +python-dateutil = "*" +requests = ">=2.32.0,<3.0.0" +urllib3 = [ + {version = ">=1.26.19,<1.27", markers = "python_version < \"3.10\""}, + {version = ">=1.26.19,<2.2.0 || >2.2.0,<2.2.1 || >2.2.1,<3", markers = "python_version >= \"3.10\""}, +] + +[package.extras] +async = ["aiohttp (>=3.9.4,<4)"] +develop = ["black (>=24.3.0)", "botocore", "coverage (<8.0.0)", "jinja2", "myst_parser", "pytest (>=3.0.0)", "pytest-cov", "pytest-mock (<4.0.0)", "pytz", "pyyaml", "requests (>=2.0.0,<3.0.0)", "sphinx", "sphinx_copybutton", "sphinx_rtd_theme"] +docs = ["aiohttp (>=3.9.4,<4)", "myst_parser", "sphinx", "sphinx_copybutton", "sphinx_rtd_theme"] +kerberos = ["requests_kerberos"] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pandas" +version = "2.3.2" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pandas-2.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52bc29a946304c360561974c6542d1dd628ddafa69134a7131fdfd6a5d7a1a35"}, + {file = "pandas-2.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:220cc5c35ffaa764dd5bb17cf42df283b5cb7fdf49e10a7b053a06c9cb48ee2b"}, + {file = "pandas-2.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c05e15111221384019897df20c6fe893b2f697d03c811ee67ec9e0bb5a3424"}, + {file = "pandas-2.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc03acc273c5515ab69f898df99d9d4f12c4d70dbfc24c3acc6203751d0804cf"}, + {file = "pandas-2.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d25c20a03e8870f6339bcf67281b946bd20b86f1a544ebbebb87e66a8d642cba"}, + {file = "pandas-2.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21bb612d148bb5860b7eb2c10faacf1a810799245afd342cf297d7551513fbb6"}, + {file = "pandas-2.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:b62d586eb25cb8cb70a5746a378fc3194cb7f11ea77170d59f889f5dfe3cec7a"}, + {file = "pandas-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1333e9c299adcbb68ee89a9bb568fc3f20f9cbb419f1dd5225071e6cddb2a743"}, + {file = "pandas-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:76972bcbd7de8e91ad5f0ca884a9f2c477a2125354af624e022c49e5bd0dfff4"}, + {file = "pandas-2.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b98bdd7c456a05eef7cd21fd6b29e3ca243591fe531c62be94a2cc987efb5ac2"}, + {file = "pandas-2.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d81573b3f7db40d020983f78721e9bfc425f411e616ef019a10ebf597aedb2e"}, + {file = "pandas-2.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e190b738675a73b581736cc8ec71ae113d6c3768d0bd18bffa5b9a0927b0b6ea"}, + {file = "pandas-2.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c253828cb08f47488d60f43c5fc95114c771bbfff085da54bfc79cb4f9e3a372"}, + {file = "pandas-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:9467697b8083f9667b212633ad6aa4ab32436dcbaf4cd57325debb0ddef2012f"}, + {file = "pandas-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fbb977f802156e7a3f829e9d1d5398f6192375a3e2d1a9ee0803e35fe70a2b9"}, + {file = "pandas-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b9b52693123dd234b7c985c68b709b0b009f4521000d0525f2b95c22f15944b"}, + {file = "pandas-2.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bd281310d4f412733f319a5bc552f86d62cddc5f51d2e392c8787335c994175"}, + {file = "pandas-2.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96d31a6b4354e3b9b8a2c848af75d31da390657e3ac6f30c05c82068b9ed79b9"}, + {file = "pandas-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:df4df0b9d02bb873a106971bb85d448378ef14b86ba96f035f50bbd3688456b4"}, + {file = "pandas-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:213a5adf93d020b74327cb2c1b842884dbdd37f895f42dcc2f09d451d949f811"}, + {file = "pandas-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c13b81a9347eb8c7548f53fd9a4f08d4dfe996836543f805c987bafa03317ae"}, + {file = "pandas-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c6ecbac99a354a051ef21c5307601093cb9e0f4b1855984a084bfec9302699e"}, + {file = "pandas-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6f048aa0fd080d6a06cc7e7537c09b53be6642d330ac6f54a600c3ace857ee9"}, + {file = "pandas-2.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0064187b80a5be6f2f9c9d6bdde29372468751dfa89f4211a3c5871854cfbf7a"}, + {file = "pandas-2.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac8c320bded4718b298281339c1a50fb00a6ba78cb2a63521c39bec95b0209b"}, + {file = "pandas-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:114c2fe4f4328cf98ce5716d1532f3ab79c5919f95a9cfee81d9140064a2e4d6"}, + {file = "pandas-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:48fa91c4dfb3b2b9bfdb5c24cd3567575f4e13f9636810462ffed8925352be5a"}, + {file = "pandas-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:12d039facec710f7ba305786837d0225a3444af7bbd9c15c32ca2d40d157ed8b"}, + {file = "pandas-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c624b615ce97864eb588779ed4046186f967374185c047070545253a52ab2d57"}, + {file = "pandas-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0cee69d583b9b128823d9514171cabb6861e09409af805b54459bd0c821a35c2"}, + {file = "pandas-2.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2319656ed81124982900b4c37f0e0c58c015af9a7bbc62342ba5ad07ace82ba9"}, + {file = "pandas-2.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b37205ad6f00d52f16b6d09f406434ba928c1a1966e2771006a9033c736d30d2"}, + {file = "pandas-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:837248b4fc3a9b83b9c6214699a13f069dc13510a6a6d7f9ba33145d2841a012"}, + {file = "pandas-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d2c3554bd31b731cd6490d94a28f3abb8dd770634a9e06eb6d2911b9827db370"}, + {file = "pandas-2.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:88080a0ff8a55eac9c84e3ff3c7665b3b5476c6fbc484775ca1910ce1c3e0b87"}, + {file = "pandas-2.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d4a558c7620340a0931828d8065688b3cc5b4c8eb674bcaf33d18ff4a6870b4a"}, + {file = "pandas-2.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45178cf09d1858a1509dc73ec261bf5b25a625a389b65be2e47b559905f0ab6a"}, + {file = "pandas-2.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77cefe00e1b210f9c76c697fedd8fdb8d3dd86563e9c8adc9fa72b90f5e9e4c2"}, + {file = "pandas-2.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:13bd629c653856f00c53dc495191baa59bcafbbf54860a46ecc50d3a88421a96"}, + {file = "pandas-2.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:36d627906fd44b5fd63c943264e11e96e923f8de77d6016dc2f667b9ad193438"}, + {file = "pandas-2.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:a9d7ec92d71a420185dec44909c32e9a362248c4ae2238234b76d5be37f208cc"}, + {file = "pandas-2.3.2.tar.gz", hash = "sha256:ab7b58f8f82706890924ccdfb5f48002b83d2b5a3845976a9fb705d36c34dcdb"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] + +[[package]] +name = "parso" +version = "0.8.5" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887"}, + {file = "parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a"}, +] + +[package.extras] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["docopt", "pytest"] + +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +groups = ["main"] +markers = "python_version == \"3.9\" and sys_platform != \"win32\" or sys_platform != \"win32\" and sys_platform != \"emscripten\"" +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pgvector" +version = "0.2.5" +description = "pgvector support for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pgvector-0.2.5-py2.py3-none-any.whl", hash = "sha256:5e5e93ec4d3c45ab1fa388729d56c602f6966296e19deee8878928c6d567e41b"}, +] + +[package.dependencies] +numpy = "*" + +[[package]] +name = "platformdirs" +version = "4.4.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, + {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + +[[package]] +name = "plotly" +version = "6.5.1" +description = "An open-source interactive data visualization library for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "plotly-6.5.1-py3-none-any.whl", hash = "sha256:5adad4f58c360612b6c5ce11a308cdbc4fd38ceb1d40594a614f0062e227abe1"}, + {file = "plotly-6.5.1.tar.gz", hash = "sha256:b0478c8d5ada0c8756bce15315bcbfec7d3ab8d24614e34af9aff7bfcfea9281"}, +] + +[package.dependencies] +narwhals = ">=1.15.1" +packaging = "*" + +[package.extras] +dev = ["plotly[dev-optional]"] +dev-build = ["build", "jupyter", "plotly[dev-core]"] +dev-core = ["pytest", "requests", "ruff (==0.11.12)"] +dev-optional = ["anywidget", "colorcet", "fiona (<=1.9.6) ; python_version <= \"3.8\"", "geopandas", "inflect", "numpy", "orjson", "pandas", "pdfrw", "pillow", "plotly-geo", "plotly[dev-build]", "plotly[kaleido]", "polars[timezone]", "pyarrow", "pyshp", "pytz", "scikit-image", "scipy", "shapely", "statsmodels", "vaex ; python_version <= \"3.9\"", "xarray"] +express = ["numpy"] +kaleido = ["kaleido (>=1.1.0)"] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "portalocker" +version = "3.2.0" +description = "Wraps the portalocker recipe for easy usage" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968"}, + {file = "portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac"}, +] + +[package.dependencies] +pywin32 = {version = ">=226", markers = "platform_system == \"Windows\""} + +[package.extras] +docs = ["portalocker[tests]"] +redis = ["redis"] +tests = ["coverage-conditional-plugin (>=0.9.0)", "portalocker[redis]", "pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "pytest-rerunfailures (>=15.0)", "pytest-timeout (>=2.1.0)", "sphinx (>=6.0.0)", "types-pywin32 (>=310.0.0.20250429)", "types-redis"] + +[[package]] +name = "pre-commit" +version = "2.21.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955"}, + {file = "prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "protobuf" +version = "6.32.1" +description = "" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "protobuf-6.32.1-cp310-abi3-win32.whl", hash = "sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085"}, + {file = "protobuf-6.32.1-cp310-abi3-win_amd64.whl", hash = "sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1"}, + {file = "protobuf-6.32.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8c7e6eb619ffdf105ee4ab76af5a68b60a9d0f66da3ea12d1640e6d8dab7281"}, + {file = "protobuf-6.32.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:2f5b80a49e1eb7b86d85fcd23fe92df154b9730a725c3b38c4e43b9d77018bf4"}, + {file = "protobuf-6.32.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:b1864818300c297265c83a4982fd3169f97122c299f56a56e2445c3698d34710"}, + {file = "protobuf-6.32.1-cp39-cp39-win32.whl", hash = "sha256:68ff170bac18c8178f130d1ccb94700cf72852298e016a2443bdb9502279e5f1"}, + {file = "protobuf-6.32.1-cp39-cp39-win_amd64.whl", hash = "sha256:d0975d0b2f3e6957111aa3935d08a0eb7e006b1505d825f862a1fffc8348e122"}, + {file = "protobuf-6.32.1-py3-none-any.whl", hash = "sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346"}, + {file = "protobuf-6.32.1.tar.gz", hash = "sha256:ee2469e4a021474ab9baafea6cd070e5bf27c7d29433504ddea1a4ee5850f68d"}, +] + +[[package]] +name = "psycopg" +version = "3.2.10" +description = "PostgreSQL database adapter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "psycopg-3.2.10-py3-none-any.whl", hash = "sha256:ab5caf09a9ec42e314a21f5216dbcceac528e0e05142e42eea83a3b28b320ac3"}, + {file = "psycopg-3.2.10.tar.gz", hash = "sha256:0bce99269d16ed18401683a8569b2c5abd94f72f8364856d56c0389bcd50972a"}, +] + +[package.dependencies] +psycopg-binary = {version = "3.2.10", optional = true, markers = "implementation_name != \"pypy\" and extra == \"binary\""} +typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""} +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +binary = ["psycopg-binary (==3.2.10) ; implementation_name != \"pypy\""] +c = ["psycopg-c (==3.2.10) ; implementation_name != \"pypy\""] +dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "isort-psycopg", "isort[colors] (>=6.0)", "mypy (>=1.14)", "pre-commit (>=4.0.1)", "types-setuptools (>=57.4)", "types-shapely (>=2.0)", "wheel (>=0.37)"] +docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] +pool = ["psycopg-pool"] +test = ["anyio (>=4.0)", "mypy (>=1.14)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] + +[[package]] +name = "psycopg-binary" +version = "3.2.10" +description = "PostgreSQL database adapter for Python -- C optimisation distribution" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "implementation_name != \"pypy\"" +files = [ + {file = "psycopg_binary-3.2.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:037dc92fc7d3f2adae7680e17216934c15b919d6528b908ac2eb52aecc0addcf"}, + {file = "psycopg_binary-3.2.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84f7e8c5e5031db342ae697c2e8fb48cd708ba56990573b33e53ce626445371d"}, + {file = "psycopg_binary-3.2.10-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a5a81104d88780018005fe17c37fa55b4afbb6dd3c205963cc56c025d5f1cc32"}, + {file = "psycopg_binary-3.2.10-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0c23e88e048bbc33f32f5a35981707c9418723d469552dd5ac4e956366e58492"}, + {file = "psycopg_binary-3.2.10-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c9f2728488ac5848acdbf14bb4fde50f8ba783cbf3c19e9abd506741389fa7f"}, + {file = "psycopg_binary-3.2.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab1c6d761c4ee581016823dcc02f29b16ad69177fcbba88a9074c924fc31813e"}, + {file = "psycopg_binary-3.2.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a024b3ee539a475cbc59df877c8ecdd6f8552a1b522b69196935bc26dc6152fb"}, + {file = "psycopg_binary-3.2.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:50130c0d1a2a01ec3d41631df86b6c1646c76718be000600a399dc1aad80b813"}, + {file = "psycopg_binary-3.2.10-cp310-cp310-win_amd64.whl", hash = "sha256:7fa1626225a162924d2da0ff4ef77869f7a8501d320355d2732be5bf2dda6138"}, + {file = "psycopg_binary-3.2.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db0eb06a19e4c64a08db0db80875ede44939af6a2afc281762c338fad5d6e547"}, + {file = "psycopg_binary-3.2.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d922fdd49ed17c558b6b2f9ae2054c3d0cced2a34e079ce5a41c86904d0203f7"}, + {file = "psycopg_binary-3.2.10-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d557a94cd6d2e775b3af6cc0bd0ff0d9d641820b5cc3060ccf1f5ca2bf971217"}, + {file = "psycopg_binary-3.2.10-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:29b6bb87959515bc8b6abef10d8d23a9a681f03e48e9f0c8adb4b9fb7fa73f11"}, + {file = "psycopg_binary-3.2.10-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b29285474e3339d0840e1b5079fdb0481914108f92ec62de0c87ae333c60b24"}, + {file = "psycopg_binary-3.2.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:62590dd113d10cd9c08251cb80b32e2e8aaf01ece04a700322e776b1d216959f"}, + {file = "psycopg_binary-3.2.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:764a5b9b40ad371c55dfdf95374d89e44a82fd62272d4fceebea0adb8930e2fb"}, + {file = "psycopg_binary-3.2.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bd3676a04970cf825d2c771b0c147f91182c5a3653e0dbe958e12383668d0f79"}, + {file = "psycopg_binary-3.2.10-cp311-cp311-win_amd64.whl", hash = "sha256:646048f46192c8d23786cc6ef19f35b7488d4110396391e407eca695fdfe9dcd"}, + {file = "psycopg_binary-3.2.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1dee2f4d2adc9adacbfecf8254bd82f6ac95cff707e1b9b99aa721cd1ef16b47"}, + {file = "psycopg_binary-3.2.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8b45e65383da9c4a42a56f817973e521e893f4faae897fe9f1a971f9fe799742"}, + {file = "psycopg_binary-3.2.10-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:484d2b1659afe0f8f1cef5ea960bb640e96fa864faf917086f9f833f5c7a8034"}, + {file = "psycopg_binary-3.2.10-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3bb4046973264ebc8cb7e20a83882d68577c1f26a6f8ad4fe52e4468cd9a8eee"}, + {file = "psycopg_binary-3.2.10-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:14bcbcac0cab465d88b2581e43ec01af4b01c9833e663f1352e05cb41be19e44"}, + {file = "psycopg_binary-3.2.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70bb7f665587dfd79e69f48b34efe226149454d7aab138ed22d5431d703de2f6"}, + {file = "psycopg_binary-3.2.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d2fe9eaa367f6171ab1a21a7dcb335eb2398be7f8bb7e04a20e2260aedc6f782"}, + {file = "psycopg_binary-3.2.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:299834cce3eec0c48aae5a5207fc8f0c558fd65f2ceab1a36693329847da956b"}, + {file = "psycopg_binary-3.2.10-cp312-cp312-win_amd64.whl", hash = "sha256:e037aac8dc894d147ef33056fc826ee5072977107a3fdf06122224353a057598"}, + {file = "psycopg_binary-3.2.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:55b14f2402be027fe1568bc6c4d75ac34628ff5442a70f74137dadf99f738e3b"}, + {file = "psycopg_binary-3.2.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:43d803fb4e108a67c78ba58f3e6855437ca25d56504cae7ebbfbd8fce9b59247"}, + {file = "psycopg_binary-3.2.10-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:470594d303928ab72a1ffd179c9c7bde9d00f76711d6b0c28f8a46ddf56d9807"}, + {file = "psycopg_binary-3.2.10-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a1d4e4d309049e3cb61269652a3ca56cb598da30ecd7eb8cea561e0d18bc1a43"}, + {file = "psycopg_binary-3.2.10-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a92ff1c2cd79b3966d6a87e26ceb222ecd5581b5ae4b58961f126af806a861ed"}, + {file = "psycopg_binary-3.2.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac0365398947879c9827b319217096be727da16c94422e0eb3cf98c930643162"}, + {file = "psycopg_binary-3.2.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:42ee399c2613b470a87084ed79b06d9d277f19b0457c10e03a4aef7059097abc"}, + {file = "psycopg_binary-3.2.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2028073fc12cd70ba003309d1439c0c4afab4a7eee7653b8c91213064fffe12b"}, + {file = "psycopg_binary-3.2.10-cp313-cp313-win_amd64.whl", hash = "sha256:8390db6d2010ffcaf7f2b42339a2da620a7125d37029c1f9b72dfb04a8e7be6f"}, + {file = "psycopg_binary-3.2.10-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b34c278a58aa79562afe7f45e0455b1f4cad5974fc3d5674cc5f1f9f57e97fc5"}, + {file = "psycopg_binary-3.2.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810f65b9ef1fe9dddb5c05937884ea9563aaf4e1a2c3d138205231ed5f439511"}, + {file = "psycopg_binary-3.2.10-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8923487c3898c65e1450847e15d734bb2e6adbd2e79d2d1dd5ad829a1306bdc0"}, + {file = "psycopg_binary-3.2.10-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7950ff79df7a453ac8a7d7a74694055b6c15905b0a2b6e3c99eb59c51a3f9bf7"}, + {file = "psycopg_binary-3.2.10-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c2b95e83fda70ed2b0b4fadd8538572e4a4d987b721823981862d1ab56cc760"}, + {file = "psycopg_binary-3.2.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20384985fbc650c09a547a13c6d7f91bb42020d38ceafd2b68b7fc4a48a1f160"}, + {file = "psycopg_binary-3.2.10-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1f6982609b8ff8fcd67299b67cd5787da1876f3bb28fedd547262cfa8ddedf94"}, + {file = "psycopg_binary-3.2.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bf30dcf6aaaa8d4779a20d2158bdf81cc8e84ce8eee595d748a7671c70c7b890"}, + {file = "psycopg_binary-3.2.10-cp314-cp314-win_amd64.whl", hash = "sha256:d5c6a66a76022af41970bf19f51bc6bf87bd10165783dd1d40484bfd87d6b382"}, + {file = "psycopg_binary-3.2.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:901729188b3fd5625970650ca1167786847dee0b92930c2858724d1a5e25dee1"}, + {file = "psycopg_binary-3.2.10-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d7d05174276bb403b8a57e01b857d96b0ac2a6879c5ce06a5cac2d1115763081"}, + {file = "psycopg_binary-3.2.10-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:37b42b2f5f58df1f07a5df1b0c2bcc9bd3b9c105e2e988923bfa47aa4ae967da"}, + {file = "psycopg_binary-3.2.10-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6fe450a98a0788b721b1b8302f0ba9be6eca82faf74bf7a86d794cd6484c7e27"}, + {file = "psycopg_binary-3.2.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a28f24a7b68456bd31209b027a5b04304d37eb1d622ef847bf8c47933218a738"}, + {file = "psycopg_binary-3.2.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5369202e0e764193eac311b5a337d8cd58b1e23b822ddb7a559ed9f683d97623"}, + {file = "psycopg_binary-3.2.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8f4ae059c6c9e491cdc3f39f9fc4f09373ef281c6cc381499269dcff21abafc9"}, + {file = "psycopg_binary-3.2.10-cp38-cp38-win_amd64.whl", hash = "sha256:3e115930af2f38f4bbb5f1b61b598ceb802f091c1592c0fe0571c796b714b89a"}, + {file = "psycopg_binary-3.2.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0738320a8d405f98743227ff70ed8fac9670870289435f4861dc640cef4a61d3"}, + {file = "psycopg_binary-3.2.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:89440355d1b163b11dc661ae64a5667578aab1b80bbf71ced90693d88e9863e1"}, + {file = "psycopg_binary-3.2.10-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3234605839e7d7584bd0a20716395eba34d368a5099dafe7896c943facac98fc"}, + {file = "psycopg_binary-3.2.10-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:725843fd444075cc6c9989f5b25ca83ac68d8d70b58e1f476fbb4096975e43cc"}, + {file = "psycopg_binary-3.2.10-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:447afc326cbc95ed67c0cd27606c0f81fa933b830061e096dbd37e08501cb3de"}, + {file = "psycopg_binary-3.2.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5334a61a00ccb722f0b28789e265c7a273cfd10d5a1ed6bf062686fbb71e7032"}, + {file = "psycopg_binary-3.2.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:183a59cbdcd7e156669577fd73a9e917b1ee664e620f1e31ae138d24c7714693"}, + {file = "psycopg_binary-3.2.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8fa2efaf5e2f8c289a185c91c80a624a8f97aa17fbedcbc68f373d089b332afd"}, + {file = "psycopg_binary-3.2.10-cp39-cp39-win_amd64.whl", hash = "sha256:6220d6efd6e2df7b67d70ed60d653106cd3b70c5cb8cbe4e9f0a142a5db14015"}, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +groups = ["main"] +markers = "python_version == \"3.9\" and sys_platform != \"win32\" or sys_platform != \"win32\" and sys_platform != \"emscripten\"" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +description = "Safely evaluate AST nodes without side effects" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, + {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pycparser" +version = "2.23" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, + {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, +] + +[[package]] +name = "pydantic" +version = "2.11.9" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2"}, + {file = "pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pymilvus" +version = "2.6.2" +description = "Python Sdk for Milvus" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pymilvus-2.6.2-py3-none-any.whl", hash = "sha256:933e447e09424d490dcf595053b01a7277dadea7ae3235cd704363bd6792509d"}, + {file = "pymilvus-2.6.2.tar.gz", hash = "sha256:b4802cc954de8f2d47bf8d6230e92196514dcb8a3726ba6098dc27909d4bc8e3"}, +] + +[package.dependencies] +grpcio = ">=1.66.2,<1.68.0 || >1.68.0,<1.68.1 || >1.68.1,<1.69.0 || >1.69.0,<1.70.0 || >1.70.0,<1.70.1 || >1.70.1,<1.71.0 || >1.71.0,<1.72.1 || >1.72.1,<1.73.0 || >1.73.0" +pandas = ">=1.2.4" +protobuf = ">=5.27.2" +python-dotenv = ">=1.0.1,<2.0.0" +setuptools = ">69" +ujson = ">=2.0.0" + +[package.extras] +bulk-writer = ["azure-storage-blob", "minio (>=7.0.0)", "pyarrow (>=12.0.0)", "requests", "urllib3"] +dev = ["azure-storage-blob", "black", "grpcio (==1.66.2)", "grpcio-testing (==1.66.2)", "grpcio-tools (==1.66.2)", "minio (>=7.0.0)", "pyarrow (>=12.0.0)", "pytest (>=5.3.4)", "pytest-asyncio", "pytest-cov (>=5.0.0)", "pytest-timeout (>=1.3.4)", "requests", "ruff (>=0.12.9,<1)", "scipy", "urllib3"] +milvus-lite = ["milvus-lite (>=2.4.0) ; sys_platform != \"win32\""] +model = ["pymilvus.model (>=0.3.0)"] + +[[package]] +name = "pymongo" +version = "4.16.0" +description = "PyMongo - the Official MongoDB Python driver" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pymongo-4.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ed162b2227f98d5b270ecbe1d53be56c8c81db08a1a8f5f02d89c7bb4d19591d"}, + {file = "pymongo-4.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a9390dce61d705a88218f0d7b54d7e1fa1b421da8129fc7c009e029a9a6b81e"}, + {file = "pymongo-4.16.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:92a232af9927710de08a6c16a9710cc1b175fb9179c0d946cd4e213b92b2a69a"}, + {file = "pymongo-4.16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d79aa147ce86aef03079096d83239580006ffb684eead593917186aee407767"}, + {file = "pymongo-4.16.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:19a1c96e7f39c7a59a9cfd4d17920cf9382f6f684faeff4649bf587dc59f8edc"}, + {file = "pymongo-4.16.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efe020c46ce3c3a89af6baec6569635812129df6fb6cf76d4943af3ba6ee2069"}, + {file = "pymongo-4.16.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dc2c00bed568732b89e211b6adca389053d5e6d2d5a8979e80b813c3ec4d1f9"}, + {file = "pymongo-4.16.0-cp310-cp310-win32.whl", hash = "sha256:5b9c6d689bbe5beb156374508133218610e14f8c81e35bc17d7a14e30ab593e6"}, + {file = "pymongo-4.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:2290909275c9b8f637b0a92eb9b89281e18a72922749ebb903403ab6cc7da914"}, + {file = "pymongo-4.16.0-cp310-cp310-win_arm64.whl", hash = "sha256:6af1aaa26f0835175d2200e62205b78e7ec3ffa430682e322cc91aaa1a0dbf28"}, + {file = "pymongo-4.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f2077ec24e2f1248f9cac7b9a2dfb894e50cc7939fcebfb1759f99304caabef"}, + {file = "pymongo-4.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d4f7ba040f72a9f43a44059872af5a8c8c660aa5d7f90d5344f2ed1c3c02721"}, + {file = "pymongo-4.16.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8a0f73af1ea56c422b2dcfc0437459148a799ef4231c6aee189d2d4c59d6728f"}, + {file = "pymongo-4.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa30cd16ddd2f216d07ba01d9635c873e97ddb041c61cf0847254edc37d1c60e"}, + {file = "pymongo-4.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d638b0b1b294d95d0fdc73688a3b61e05cc4188872818cd240d51460ccabcb5"}, + {file = "pymongo-4.16.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:21d02cc10a158daa20cb040985e280e7e439832fc6b7857bff3d53ef6914ad50"}, + {file = "pymongo-4.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fbb8d3552c2ad99d9e236003c0b5f96d5f05e29386ba7abae73949bfebc13dd"}, + {file = "pymongo-4.16.0-cp311-cp311-win32.whl", hash = "sha256:be1099a8295b1a722d03fb7b48be895d30f4301419a583dcf50e9045968a041c"}, + {file = "pymongo-4.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:61567f712bda04c7545a037e3284b4367cad8d29b3dec84b4bf3b2147020a75b"}, + {file = "pymongo-4.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:c53338613043038005bf2e41a2fafa08d29cdbc0ce80891b5366c819456c1ae9"}, + {file = "pymongo-4.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bd4911c40a43a821dfd93038ac824b756b6e703e26e951718522d29f6eb166a8"}, + {file = "pymongo-4.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25a6b03a68f9907ea6ec8bc7cf4c58a1b51a18e23394f962a6402f8e46d41211"}, + {file = "pymongo-4.16.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:91ac0cb0fe2bf17616c2039dac88d7c9a5088f5cb5829b27c9d250e053664d31"}, + {file = "pymongo-4.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf0ec79e8ca7077f455d14d915d629385153b6a11abc0b93283ed73a8013e376"}, + {file = "pymongo-4.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2d0082631a7510318befc2b4fdab140481eb4b9dd62d9245e042157085da2a70"}, + {file = "pymongo-4.16.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85dc2f3444c346ea019a371e321ac868a4fab513b7a55fe368f0cc78de8177cc"}, + {file = "pymongo-4.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dabbf3c14de75a20cc3c30bf0c6527157224a93dfb605838eabb1a2ee3be008d"}, + {file = "pymongo-4.16.0-cp312-cp312-win32.whl", hash = "sha256:60307bb91e0ab44e560fe3a211087748b2b5f3e31f403baf41f5b7b0a70bd104"}, + {file = "pymongo-4.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:f513b2c6c0d5c491f478422f6b5b5c27ac1af06a54c93ef8631806f7231bd92e"}, + {file = "pymongo-4.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:dfc320f08ea9a7ec5b2403dc4e8150636f0d6150f4b9792faaae539c88e7db3b"}, + {file = "pymongo-4.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d15f060bc6d0964a8bb70aba8f0cb6d11ae99715438f640cff11bbcf172eb0e8"}, + {file = "pymongo-4.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a19ea46a0fe71248965305a020bc076a163311aefbaa1d83e47d06fa30ac747"}, + {file = "pymongo-4.16.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:311d4549d6bf1f8c61d025965aebb5ba29d1481dc6471693ab91610aaffbc0eb"}, + {file = "pymongo-4.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46ffb728d92dd5b09fc034ed91acf5595657c7ca17d4cf3751322cd554153c17"}, + {file = "pymongo-4.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:acda193f440dd88c2023cb00aa8bd7b93a9df59978306d14d87a8b12fe426b05"}, + {file = "pymongo-4.16.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d9fdb386cf958e6ef6ff537d6149be7edb76c3268cd6833e6c36aa447e4443f"}, + {file = "pymongo-4.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91899dd7fb9a8c50f09c3c1cf0cb73bfbe2737f511f641f19b9650deb61c00ca"}, + {file = "pymongo-4.16.0-cp313-cp313-win32.whl", hash = "sha256:2cd60cd1e05de7f01927f8e25ca26b3ea2c09de8723241e5d3bcfdc70eaff76b"}, + {file = "pymongo-4.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3ead8a0050c53eaa55935895d6919d393d0328ec24b2b9115bdbe881aa222673"}, + {file = "pymongo-4.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:dbbc5b254c36c37d10abb50e899bc3939bbb7ab1e7c659614409af99bd3e7675"}, + {file = "pymongo-4.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8a254d49a9ffe9d7f888e3c677eed3729b14ce85abb08cd74732cead6ccc3c66"}, + {file = "pymongo-4.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a1bf44e13cf2d44d2ea2e928a8140d5d667304abe1a61c4d55b4906f389fbe64"}, + {file = "pymongo-4.16.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f1c5f1f818b669875d191323a48912d3fcd2e4906410e8297bb09ac50c4d5ccc"}, + {file = "pymongo-4.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77cfd37a43a53b02b7bd930457c7994c924ad8bbe8dff91817904bcbf291b371"}, + {file = "pymongo-4.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:36ef2fee50eee669587d742fb456e349634b4fcf8926208766078b089054b24b"}, + {file = "pymongo-4.16.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55f8d5a6fe2fa0b823674db2293f92d74cd5f970bc0360f409a1fc21003862d3"}, + {file = "pymongo-4.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9caacac0dd105e2555521002e2d17afc08665187017b466b5753e84c016628e6"}, + {file = "pymongo-4.16.0-cp314-cp314-win32.whl", hash = "sha256:c789236366525c3ee3cd6e4e450a9ff629a7d1f4d88b8e18a0aea0615fd7ecf8"}, + {file = "pymongo-4.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:2b0714d7764efb29bf9d3c51c964aed7c4c7237b341f9346f15ceaf8321fdb35"}, + {file = "pymongo-4.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:12762e7cc0f8374a8cae3b9f9ed8dabb5d438c7b33329232dd9b7de783454033"}, + {file = "pymongo-4.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1c01e8a7cd0ea66baf64a118005535ab5bf9f9eb63a1b50ac3935dccf9a54abe"}, + {file = "pymongo-4.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4c4872299ebe315a79f7f922051061634a64fda95b6b17677ba57ef00b2ba2a4"}, + {file = "pymongo-4.16.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:78037d02389745e247fe5ab0bcad5d1ab30726eaac3ad79219c7d6bbb07eec53"}, + {file = "pymongo-4.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c126fb72be2518395cc0465d4bae03125119136462e1945aea19840e45d89cfc"}, + {file = "pymongo-4.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f3867dc225d9423c245a51eaac2cfcd53dde8e0a8d8090bb6aed6e31bd6c2d4f"}, + {file = "pymongo-4.16.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f25001a955073b80510c0c3db0e043dbbc36904fd69e511c74e3d8640b8a5111"}, + {file = "pymongo-4.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d9885aad05f82fd7ea0c9ca505d60939746b39263fa273d0125170da8f59098"}, + {file = "pymongo-4.16.0-cp314-cp314t-win32.whl", hash = "sha256:948152b30eddeae8355495f9943a3bf66b708295c0b9b6f467de1c620f215487"}, + {file = "pymongo-4.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f6e42c1bc985d9beee884780ae6048790eb4cd565c46251932906bdb1630034a"}, + {file = "pymongo-4.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6b2a20edb5452ac8daa395890eeb076c570790dfce6b7a44d788af74c2f8cf96"}, + {file = "pymongo-4.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e2d509786344aa844ae243f68f833ca1ac92ac3e35a92ae038e2ceb44aa355ef"}, + {file = "pymongo-4.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:15bb062c0d6d4b0be650410032152de656a2a9a2aa4e1a7443a22695afacb103"}, + {file = "pymongo-4.16.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4cd047ba6cc83cc24193b9208c93e134a985ead556183077678c59af7aacc725"}, + {file = "pymongo-4.16.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96aa7ab896889bf330209d26459e493d00f8855772a9453bfb4520bb1f495baf"}, + {file = "pymongo-4.16.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:66af44ed23686dd5422307619a6db4b56733c5e36fe8c4adf91326dcf993a043"}, + {file = "pymongo-4.16.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:03f42396c1b2c6f46f5401c5b185adc25f6113716e16d9503977ee5386fca0fb"}, + {file = "pymongo-4.16.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d284bf68daffc57516535f752e290609b3b643f4bd54b28fc13cb16a89a8bda6"}, + {file = "pymongo-4.16.0-cp39-cp39-win32.whl", hash = "sha256:7902882ed0efb7f0e991458ab3b8cf0eb052957264949ece2f09b63c58b04f78"}, + {file = "pymongo-4.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:e37469602473f41221cea93fd3736708f561f0fa08ab6b2873dd962014390d52"}, + {file = "pymongo-4.16.0-cp39-cp39-win_arm64.whl", hash = "sha256:2a3ba6be3d8acf64b77cdcd4e36f0e4a8e87965f14a8b09b90ca86f10a1dd2f2"}, + {file = "pymongo-4.16.0.tar.gz", hash = "sha256:8ba8405065f6e258a6f872fe62d797a28f383a12178c7153c01ed04e845c600c"}, +] + +[package.dependencies] +dnspython = ">=2.6.1,<3.0.0" + +[package.extras] +aws = ["pymongo-auth-aws (>=1.1.0,<2.0.0)"] +docs = ["furo (==2025.12.19)", "readthedocs-sphinx-search (>=0.3,<1.0)", "sphinx (>=5.3,<9)", "sphinx-autobuild (>=2020.9.1)", "sphinx-rtd-theme (>=2,<4)", "sphinxcontrib-shellcheck (>=1,<2)"] +encryption = ["certifi (>=2023.7.22) ; os_name == \"nt\" or sys_platform == \"darwin\"", "pymongo-auth-aws (>=1.1.0,<2.0.0)", "pymongocrypt (>=1.13.0,<2.0.0)"] +gssapi = ["pykerberos (>=1.2.4) ; os_name != \"nt\"", "winkerberos (>=0.5.0) ; os_name == \"nt\""] +ocsp = ["certifi (>=2023.7.22) ; os_name == \"nt\" or sys_platform == \"darwin\"", "cryptography (>=42.0.0)", "pyopenssl (>=23.2.0)", "requests (>=2.23.0,<3.0)", "service-identity (>=23.1.0)"] +snappy = ["python-snappy (>=0.6.0)"] +test = ["importlib-metadata (>=7.0) ; python_version < \"3.13\"", "pytest (>=8.2)", "pytest-asyncio (>=0.24.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +version = "1.1.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"}, + {file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "pytz" +version = "2025.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, + {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, +] + +[[package]] +name = "pywin32" +version = "311" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +groups = ["main"] +markers = "platform_system == \"Windows\"" +files = [ + {file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"}, + {file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"}, + {file = "pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b"}, + {file = "pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151"}, + {file = "pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503"}, + {file = "pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2"}, + {file = "pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31"}, + {file = "pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067"}, + {file = "pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852"}, + {file = "pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d"}, + {file = "pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d"}, + {file = "pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a"}, + {file = "pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee"}, + {file = "pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87"}, + {file = "pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42"}, + {file = "pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c"}, + {file = "pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd"}, + {file = "pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b"}, + {file = "pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91"}, + {file = "pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "qdrant-client" +version = "1.15.1" +description = "Client library for the Qdrant vector search engine" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "qdrant_client-1.15.1-py3-none-any.whl", hash = "sha256:2b975099b378382f6ca1cfb43f0d59e541be6e16a5892f282a4b8de7eff5cb63"}, + {file = "qdrant_client-1.15.1.tar.gz", hash = "sha256:631f1f3caebfad0fd0c1fba98f41be81d9962b7bf3ca653bed3b727c0e0cbe0e"}, +] + +[package.dependencies] +grpcio = ">=1.41.0" +httpx = {version = ">=0.20.0", extras = ["http2"]} +numpy = [ + {version = ">=1.21,<2.1.0", markers = "python_version < \"3.10\""}, + {version = ">=1.21", markers = "python_version >= \"3.10\" and python_version < \"3.12\""}, + {version = ">=1.26", markers = "python_version == \"3.12\""}, + {version = ">=2.1.0", markers = "python_version >= \"3.13\""}, +] +portalocker = ">=2.7.0,<4.0" +protobuf = ">=3.20.0" +pydantic = ">=1.10.8,<2.0.dev0 || >2.2.0" +urllib3 = ">=1.26.14,<3" + +[package.extras] +fastembed = ["fastembed (>=0.7,<0.8)"] +fastembed-gpu = ["fastembed-gpu (>=0.7,<0.8)"] + +[[package]] +name = "redis" +version = "6.4.0" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f"}, + {file = "redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} + +[package.extras] +hiredis = ["hiredis (>=3.2.0)"] +jwt = ["pyjwt (>=2.9.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (>=20.0.1)", "requests (>=2.31.0)"] + +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "retrying" +version = "1.4.2" +description = "Retrying" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "retrying-1.4.2-py3-none-any.whl", hash = "sha256:bbc004aeb542a74f3569aeddf42a2516efefcdaff90df0eb38fbfbf19f179f59"}, + {file = "retrying-1.4.2.tar.gz", hash = "sha256:d102e75d53d8d30b88562d45361d6c6c934da06fab31bd81c0420acb97a8ba39"}, +] + +[[package]] +name = "rich" +version = "14.1.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f"}, + {file = "rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "s3transfer" +version = "0.14.0" +description = "An Amazon S3 Transfer Manager" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456"}, + {file = "s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125"}, +] + +[package.dependencies] +botocore = ">=1.37.4,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] + +[[package]] +name = "setuptools" +version = "80.9.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, + {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] + +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "stopit" +version = "1.1.2" +description = "Timeout control decorator and context managers, raise any exception in another thread" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "stopit-1.1.2.tar.gz", hash = "sha256:f7f39c583fd92027bd9d06127b259aee7a5b7945c1f1fa56263811e1e766996d"}, +] + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] +discord = ["requests"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "traitlets" +version = "5.14.3" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, + {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] + +[[package]] +name = "typer" +version = "0.19.2" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9"}, + {file = "typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca"}, +] + +[package.dependencies] +click = ">=8.0.0" +rich = ">=10.11.0" +shellingham = ">=1.3.0" +typing-extensions = ">=3.7.4.3" + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] +markers = {dev = "python_version < \"3.11\""} + +[[package]] +name = "typing-inspection" +version = "0.4.1" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + +[[package]] +name = "typish" +version = "1.9.3" +description = "Functionality for types" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "typish-1.9.3-py3-none-any.whl", hash = "sha256:03cfee5e6eb856dbf90244e18f4e4c41044c8790d5779f4e775f63f982e2f896"}, +] + +[package.extras] +test = ["codecov", "coverage", "mypy", "nptyping (>=1.3.0)", "numpy", "pycodestyle", "pylint", "pytest"] + +[[package]] +name = "tzdata" +version = "2025.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main"] +files = [ + {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, + {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, +] + +[[package]] +name = "ujson" +version = "5.11.0" +description = "Ultra fast JSON encoder and decoder for Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "ujson-5.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:446e8c11c06048611c9d29ef1237065de0af07cabdd97e6b5b527b957692ec25"}, + {file = "ujson-5.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16ccb973b7ada0455201808ff11d48fe9c3f034a6ab5bd93b944443c88299f89"}, + {file = "ujson-5.11.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3134b783ab314d2298d58cda7e47e7a0f7f71fc6ade6ac86d5dbeaf4b9770fa6"}, + {file = "ujson-5.11.0-cp310-cp310-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:185f93ebccffebc8baf8302c869fac70dd5dd78694f3b875d03a31b03b062cdb"}, + {file = "ujson-5.11.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d06e87eded62ff0e5f5178c916337d2262fdbc03b31688142a3433eabb6511db"}, + {file = "ujson-5.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:181fb5b15703a8b9370b25345d2a1fd1359f0f18776b3643d24e13ed9c036d4c"}, + {file = "ujson-5.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4df61a6df0a4a8eb5b9b1ffd673429811f50b235539dac586bb7e9e91994138"}, + {file = "ujson-5.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6eff24e1abd79e0ec6d7eae651dd675ddbc41f9e43e29ef81e16b421da896915"}, + {file = "ujson-5.11.0-cp310-cp310-win32.whl", hash = "sha256:30f607c70091483550fbd669a0b37471e5165b317d6c16e75dba2aa967608723"}, + {file = "ujson-5.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:3d2720e9785f84312b8e2cb0c2b87f1a0b1c53aaab3b2af3ab817d54409012e0"}, + {file = "ujson-5.11.0-cp310-cp310-win_arm64.whl", hash = "sha256:85e6796631165f719084a9af00c79195d3ebf108151452fefdcb1c8bb50f0105"}, + {file = "ujson-5.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d7c46cb0fe5e7056b9acb748a4c35aa1b428025853032540bb7e41f46767321f"}, + {file = "ujson-5.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8951bb7a505ab2a700e26f691bdfacf395bc7e3111e3416d325b513eea03a58"}, + {file = "ujson-5.11.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952c0be400229940248c0f5356514123d428cba1946af6fa2bbd7503395fef26"}, + {file = "ujson-5.11.0-cp311-cp311-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:94fcae844f1e302f6f8095c5d1c45a2f0bfb928cccf9f1b99e3ace634b980a2a"}, + {file = "ujson-5.11.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e0ec1646db172beb8d3df4c32a9d78015e671d2000af548252769e33079d9a6"}, + {file = "ujson-5.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:da473b23e3a54448b008d33f742bcd6d5fb2a897e42d1fc6e7bf306ea5d18b1b"}, + {file = "ujson-5.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:aa6b3d4f1c0d3f82930f4cbd7fe46d905a4a9205a7c13279789c1263faf06dba"}, + {file = "ujson-5.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4843f3ab4fe1cc596bb7e02228ef4c25d35b4bb0809d6a260852a4bfcab37ba3"}, + {file = "ujson-5.11.0-cp311-cp311-win32.whl", hash = "sha256:e979fbc469a7f77f04ec2f4e853ba00c441bf2b06720aa259f0f720561335e34"}, + {file = "ujson-5.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:683f57f0dd3acdd7d9aff1de0528d603aafcb0e6d126e3dc7ce8b020a28f5d01"}, + {file = "ujson-5.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:7855ccea3f8dad5e66d8445d754fc1cf80265a4272b5f8059ebc7ec29b8d0835"}, + {file = "ujson-5.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7895f0d2d53bd6aea11743bd56e3cb82d729980636cd0ed9b89418bf66591702"}, + {file = "ujson-5.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12b5e7e22a1fe01058000d1b317d3b65cc3daf61bd2ea7a2b76721fe160fa74d"}, + {file = "ujson-5.11.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0180a480a7d099082501cad1fe85252e4d4bf926b40960fb3d9e87a3a6fbbc80"}, + {file = "ujson-5.11.0-cp312-cp312-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:fa79fdb47701942c2132a9dd2297a1a85941d966d8c87bfd9e29b0cf423f26cc"}, + {file = "ujson-5.11.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8254e858437c00f17cb72e7a644fc42dad0ebb21ea981b71df6e84b1072aaa7c"}, + {file = "ujson-5.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1aa8a2ab482f09f6c10fba37112af5f957689a79ea598399c85009f2f29898b5"}, + {file = "ujson-5.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a638425d3c6eed0318df663df44480f4a40dc87cc7c6da44d221418312f6413b"}, + {file = "ujson-5.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e3cff632c1d78023b15f7e3a81c3745cd3f94c044d1e8fa8efbd6b161997bbc"}, + {file = "ujson-5.11.0-cp312-cp312-win32.whl", hash = "sha256:be6b0eaf92cae8cdee4d4c9e074bde43ef1c590ed5ba037ea26c9632fb479c88"}, + {file = "ujson-5.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:b7b136cc6abc7619124fd897ef75f8e63105298b5ca9bdf43ebd0e1fa0ee105f"}, + {file = "ujson-5.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:6cd2df62f24c506a0ba322d5e4fe4466d47a9467b57e881ee15a31f7ecf68ff6"}, + {file = "ujson-5.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:109f59885041b14ee9569bf0bb3f98579c3fa0652317b355669939e5fc5ede53"}, + {file = "ujson-5.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a31c6b8004438e8c20fc55ac1c0e07dad42941db24176fe9acf2815971f8e752"}, + {file = "ujson-5.11.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78c684fb21255b9b90320ba7e199780f653e03f6c2528663768965f4126a5b50"}, + {file = "ujson-5.11.0-cp313-cp313-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:4c9f5d6a27d035dd90a146f7761c2272cf7103de5127c9ab9c4cd39ea61e878a"}, + {file = "ujson-5.11.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:837da4d27fed5fdc1b630bd18f519744b23a0b5ada1bbde1a36ba463f2900c03"}, + {file = "ujson-5.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:787aff4a84da301b7f3bac09bc696e2e5670df829c6f8ecf39916b4e7e24e701"}, + {file = "ujson-5.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6dd703c3e86dc6f7044c5ac0b3ae079ed96bf297974598116aa5fb7f655c3a60"}, + {file = "ujson-5.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3772e4fe6b0c1e025ba3c50841a0ca4786825a4894c8411bf8d3afe3a8061328"}, + {file = "ujson-5.11.0-cp313-cp313-win32.whl", hash = "sha256:8fa2af7c1459204b7a42e98263b069bd535ea0cd978b4d6982f35af5a04a4241"}, + {file = "ujson-5.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:34032aeca4510a7c7102bd5933f59a37f63891f30a0706fb46487ab6f0edf8f0"}, + {file = "ujson-5.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:ce076f2df2e1aa62b685086fbad67f2b1d3048369664b4cdccc50707325401f9"}, + {file = "ujson-5.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:65724738c73645db88f70ba1f2e6fb678f913281804d5da2fd02c8c5839af302"}, + {file = "ujson-5.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29113c003ca33ab71b1b480bde952fbab2a0b6b03a4ee4c3d71687cdcbd1a29d"}, + {file = "ujson-5.11.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c44c703842024d796b4c78542a6fcd5c3cb948b9fc2a73ee65b9c86a22ee3638"}, + {file = "ujson-5.11.0-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:e750c436fb90edf85585f5c62a35b35082502383840962c6983403d1bd96a02c"}, + {file = "ujson-5.11.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f278b31a7c52eb0947b2db55a5133fbc46b6f0ef49972cd1a80843b72e135aba"}, + {file = "ujson-5.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ab2cb8351d976e788669c8281465d44d4e94413718af497b4e7342d7b2f78018"}, + {file = "ujson-5.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:090b4d11b380ae25453100b722d0609d5051ffe98f80ec52853ccf8249dfd840"}, + {file = "ujson-5.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:80017e870d882d5517d28995b62e4e518a894f932f1e242cbc802a2fd64d365c"}, + {file = "ujson-5.11.0-cp314-cp314-win32.whl", hash = "sha256:1d663b96eb34c93392e9caae19c099ec4133ba21654b081956613327f0e973ac"}, + {file = "ujson-5.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:849e65b696f0d242833f1df4182096cedc50d414215d1371fca85c541fbff629"}, + {file = "ujson-5.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:e73df8648c9470af2b6a6bf5250d4744ad2cf3d774dcf8c6e31f018bdd04d764"}, + {file = "ujson-5.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:de6e88f62796372fba1de973c11138f197d3e0e1d80bcb2b8aae1e826096d433"}, + {file = "ujson-5.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e56ef8066f11b80d620985ae36869a3ff7e4b74c3b6129182ec5d1df0255f3"}, + {file = "ujson-5.11.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a325fd2c3a056cf6c8e023f74a0c478dd282a93141356ae7f16d5309f5ff823"}, + {file = "ujson-5.11.0-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:a0af6574fc1d9d53f4ff371f58c96673e6d988ed2b5bf666a6143c782fa007e9"}, + {file = "ujson-5.11.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10f29e71ecf4ecd93a6610bd8efa8e7b6467454a363c3d6416db65de883eb076"}, + {file = "ujson-5.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a0a9b76a89827a592656fe12e000cf4f12da9692f51a841a4a07aa4c7ecc41c"}, + {file = "ujson-5.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b16930f6a0753cdc7d637b33b4e8f10d5e351e1fb83872ba6375f1e87be39746"}, + {file = "ujson-5.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:04c41afc195fd477a59db3a84d5b83a871bd648ef371cf8c6f43072d89144eef"}, + {file = "ujson-5.11.0-cp314-cp314t-win32.whl", hash = "sha256:aa6d7a5e09217ff93234e050e3e380da62b084e26b9f2e277d2606406a2fc2e5"}, + {file = "ujson-5.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:48055e1061c1bb1f79e75b4ac39e821f3f35a9b82de17fce92c3140149009bec"}, + {file = "ujson-5.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1194b943e951092db611011cb8dbdb6cf94a3b816ed07906e14d3bc6ce0e90ab"}, + {file = "ujson-5.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65f3c279f4ed4bf9131b11972040200c66ae040368abdbb21596bf1564899694"}, + {file = "ujson-5.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:99c49400572cd77050894e16864a335225191fd72a818ea6423ae1a06467beac"}, + {file = "ujson-5.11.0-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0654a2691fc252c3c525e3d034bb27b8a7546c9d3eb33cd29ce6c9feda361a6a"}, + {file = "ujson-5.11.0-cp39-cp39-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:6b6ec7e7321d7fc19abdda3ad809baef935f49673951a8bab486aea975007e02"}, + {file = "ujson-5.11.0-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f62b9976fabbcde3ab6e413f4ec2ff017749819a0786d84d7510171109f2d53c"}, + {file = "ujson-5.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f1a27ab91083b4770e160d17f61b407f587548f2c2b5fbf19f94794c495594a"}, + {file = "ujson-5.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ecd6ff8a3b5a90c292c2396c2d63c687fd0ecdf17de390d852524393cd9ed052"}, + {file = "ujson-5.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9aacbeb23fdbc4b256a7d12e0beb9063a1ba5d9e0dbb2cfe16357c98b4334596"}, + {file = "ujson-5.11.0-cp39-cp39-win32.whl", hash = "sha256:674f306e3e6089f92b126eb2fe41bcb65e42a15432c143365c729fdb50518547"}, + {file = "ujson-5.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:c6618f480f7c9ded05e78a1938873fde68baf96cdd74e6d23c7e0a8441175c4b"}, + {file = "ujson-5.11.0-cp39-cp39-win_arm64.whl", hash = "sha256:5600202a731af24a25e2d7b6eb3f648e4ecd4bb67c4d5cf12f8fab31677469c9"}, + {file = "ujson-5.11.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:abae0fb58cc820092a0e9e8ba0051ac4583958495bfa5262a12f628249e3b362"}, + {file = "ujson-5.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fac6c0649d6b7c3682a0a6e18d3de6857977378dce8d419f57a0b20e3d775b39"}, + {file = "ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b42c115c7c6012506e8168315150d1e3f76e7ba0f4f95616f4ee599a1372bbc"}, + {file = "ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:86baf341d90b566d61a394869ce77188cc8668f76d7bb2c311d77a00f4bdf844"}, + {file = "ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4598bf3965fc1a936bd84034312bcbe00ba87880ef1ee33e33c1e88f2c398b49"}, + {file = "ujson-5.11.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:416389ec19ef5f2013592f791486bef712ebce0cd59299bf9df1ba40bb2f6e04"}, + {file = "ujson-5.11.0.tar.gz", hash = "sha256:e204ae6f909f099ba6b6b942131cee359ddda2b6e4ea39c12eb8b991fe2010e0"}, +] + +[[package]] +name = "urllib3" +version = "1.26.20" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["main"] +markers = "python_version == \"3.9\"" +files = [ + {file = "urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"}, + {file = "urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"}, +] + +[package.extras] +brotli = ["brotli (==1.0.9) ; os_name != \"nt\" and python_version < \"3\" and platform_python_implementation == \"CPython\"", "brotli (>=1.0.9) ; python_version >= \"3\" and platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation != \"CPython\"", "brotlipy (>=0.6.0) ; os_name == \"nt\" and python_version < \"3\""] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress ; python_version == \"2.7\"", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "urllib3" +version = "2.5.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "validators" +version = "0.35.0" +description = "Python Data Validation for Humans™" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "validators-0.35.0-py3-none-any.whl", hash = "sha256:e8c947097eae7892cb3d26868d637f79f47b4a0554bc6b80065dfe5aac3705dd"}, + {file = "validators-0.35.0.tar.gz", hash = "sha256:992d6c48a4e77c81f1b4daba10d16c3a9bb0dbb79b3a19ea847ff0928e70497a"}, +] + +[package.extras] +crypto-eth-addresses = ["eth-hash[pycryptodome] (>=0.7.0)"] + +[[package]] +name = "virtualenv" +version = "20.34.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026"}, + {file = "virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" +typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] + +[[package]] +name = "wcwidth" +version = "0.2.14" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1"}, + {file = "wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605"}, +] + +[[package]] +name = "weaviate-client" +version = "4.16.10" +description = "A python native Weaviate client" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "weaviate_client-4.16.10-py3-none-any.whl", hash = "sha256:a2009530951ca08dc071dd74e13052de9ccc3830e9e57a3f292656efa386d6de"}, + {file = "weaviate_client-4.16.10.tar.gz", hash = "sha256:c041e6ae5416b8db8ed53d961722f122b52516175da1d249601a87db327e9288"}, +] + +[package.dependencies] +authlib = ">=1.2.1,<2.0.0" +deprecation = ">=2.1.0,<3.0.0" +grpcio = ">=1.59.5,<1.80.0" +httpx = ">=0.26.0,<0.29.0" +protobuf = ">=4.21.6,<7.0.0" +pydantic = ">=2.8.0,<3.0.0" +validators = ">=0.34.0,<1.0.0" + +[package.extras] +agents = ["weaviate-agents (>=1.0.0,<2.0.0)"] + +[[package]] +name = "werkzeug" +version = "3.1.4" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "werkzeug-3.1.4-py3-none-any.whl", hash = "sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905"}, + {file = "werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e"}, +] + +[package.dependencies] +markupsafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[[package]] +name = "zipp" +version = "3.23.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, + {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.9,<3.14" +content-hash = "6071fd05bdd2a4e9ad0bae90737bd87df68df77a8d13a97b51885383202911ca" diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/pyproject.toml b/02-semantic-search-rag/fcenedes/vecdb_bench/pyproject.toml new file mode 100644 index 0000000..d869c06 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/pyproject.toml @@ -0,0 +1,36 @@ +[tool.poetry] +name = "vector-db-benchmark" +version = "0.1.0" +description = "" +authors = ["Kacper Łukawski "] + +[tool.poetry.dependencies] +python = ">=3.9,<3.14" +qdrant-client = "^1.8.0" +typer = "^0.19.0" +jsons = "^1.6.3" +h5py = "^3.7.0" +weaviate-client = "^4.5.0" +elasticsearch = "^8.10.0" +pymilvus = "^2.3.1" +redis = "^6.4.0" +ipdb = "^0.13.9" +stopit = "^1.1.2" +opensearch-py = "^2.3.2" +tqdm = "^4.66.1" +backoff = "^2.2.1" +psycopg = {extras = ["binary"], version = "^3.1.17"} +pgvector = "^0.2.4" +ml-dtypes = "^0.4.0" +boto3 = "^1.39.4" +dash = "^3.3.0" +pymongo = "^4.16.0" + + +[tool.poetry.group.dev.dependencies] +pre-commit = "^2.20.0" +pytest = "^7.1" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-0-81171-2026-07-09-11-07-42.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-0-81171-2026-07-09-11-07-42.json new file mode 100644 index 0000000..5224795 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-0-81171-2026-07-09-11-07-42.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 1, + "search_params": { + "ef": 64 + } + }, + "results": { + "total_time": 25.31859429203905, + "mean_time": 0.02514896241633687, + "mean_precisions": 0.8333499999999999, + "std_time": 0.013847095853037318, + "min_time": 0.020153958001174033, + "max_time": 0.29766124999150634, + "rps": 39.49666353769217, + "p50_time": 0.02383745857514441, + "p95_time": 0.028642733272863548, + "p99_time": 0.03381865177885627 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-0-81171-2026-07-09-11-08-05.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-0-81171-2026-07-09-11-08-05.json new file mode 100644 index 0000000..f8d6460 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-0-81171-2026-07-09-11-08-05.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 1, + "search_params": { + "ef": 64 + } + }, + "results": { + "total_time": 23.224931417033076, + "mean_time": 0.023041163327754476, + "mean_precisions": 0.8333499999999999, + "std_time": 0.004262143341244367, + "min_time": 0.018964666058309376, + "max_time": 0.12181749998126179, + "rps": 43.057177739031076, + "p50_time": 0.02243920846376568, + "p95_time": 0.02729069379274733, + "p99_time": 0.031769504190888254 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-0-81171-2026-07-09-11-08-30.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-0-81171-2026-07-09-11-08-30.json new file mode 100644 index 0000000..0dca521 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-0-81171-2026-07-09-11-08-30.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 1, + "search_params": { + "ef": 64 + } + }, + "results": { + "total_time": 24.344367333105765, + "mean_time": 0.024176758312736638, + "mean_precisions": 0.8333499999999999, + "std_time": 0.006388772535546146, + "min_time": 0.019134540925733745, + "max_time": 0.1750043329084292, + "rps": 41.07726384164873, + "p50_time": 0.023455499962437898, + "p95_time": 0.02809429031331092, + "p99_time": 0.03210202972055413 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-1-81171-2026-07-09-11-08-55.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-1-81171-2026-07-09-11-08-55.json new file mode 100644 index 0000000..efac107 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-1-81171-2026-07-09-11-08-55.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 1, + "search_params": { + "ef": 128 + } + }, + "results": { + "total_time": 24.949703624937683, + "mean_time": 0.024813730941270477, + "mean_precisions": 0.86452, + "std_time": 0.009280621095941858, + "min_time": 0.01949583296664059, + "max_time": 0.1802616670029238, + "rps": 40.08063642890258, + "p50_time": 0.02377485448960215, + "p95_time": 0.02894813955063, + "p99_time": 0.032980024995049456 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-1-81171-2026-07-09-11-09-19.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-1-81171-2026-07-09-11-09-19.json new file mode 100644 index 0000000..57369d4 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-1-81171-2026-07-09-11-09-19.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 1, + "search_params": { + "ef": 128 + } + }, + "results": { + "total_time": 23.808236874989234, + "mean_time": 0.023664478288032115, + "mean_precisions": 0.86452, + "std_time": 0.004997433787358491, + "min_time": 0.018294167006388307, + "max_time": 0.1643884580116719, + "rps": 42.00227027523021, + "p50_time": 0.023103667015675455, + "p95_time": 0.027716637548292053, + "p99_time": 0.031785301668569445 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-1-81171-2026-07-09-11-09-43.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-1-81171-2026-07-09-11-09-43.json new file mode 100644 index 0000000..d141e5d --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-1-81171-2026-07-09-11-09-43.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 1, + "search_params": { + "ef": 128 + } + }, + "results": { + "total_time": 24.595808167010546, + "mean_time": 0.024443589540198447, + "mean_precisions": 0.86452, + "std_time": 0.007920150824748739, + "min_time": 0.01974641694687307, + "max_time": 0.18279158405493945, + "rps": 40.65733450227764, + "p50_time": 0.023500187031459063, + "p95_time": 0.028547383355908094, + "p99_time": 0.03136266422807238 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-2-81171-2026-07-09-11-10-12.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-2-81171-2026-07-09-11-10-12.json new file mode 100644 index 0000000..cf5ac0b --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-2-81171-2026-07-09-11-10-12.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 1, + "search_params": { + "ef": 256 + } + }, + "results": { + "total_time": 28.283384208916686, + "mean_time": 0.028108989555388687, + "mean_precisions": 0.9289400000000001, + "std_time": 0.01433052163287492, + "min_time": 0.022548374952748418, + "max_time": 0.31922787497751415, + "rps": 35.35644789228361, + "p50_time": 0.02676049998262897, + "p95_time": 0.031891161022940644, + "p99_time": 0.03774004540056921 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-2-81171-2026-07-09-11-10-41.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-2-81171-2026-07-09-11-10-41.json new file mode 100644 index 0000000..fb87086 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-2-81171-2026-07-09-11-10-41.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 1, + "search_params": { + "ef": 256 + } + }, + "results": { + "total_time": 29.400523791089654, + "mean_time": 0.029215093450271526, + "mean_precisions": 0.9289400000000001, + "std_time": 0.017539508210409036, + "min_time": 0.02306300005875528, + "max_time": 0.30121116689406335, + "rps": 34.012999465780524, + "p50_time": 0.02755577105563134, + "p95_time": 0.03292017025523819, + "p99_time": 0.03746635368326677 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-2-81171-2026-07-09-11-11-10.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-2-81171-2026-07-09-11-11-10.json new file mode 100644 index 0000000..2873e9e --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-2-81171-2026-07-09-11-11-10.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 1, + "search_params": { + "ef": 256 + } + }, + "results": { + "total_time": 28.367832083022222, + "mean_time": 0.028184803569572977, + "mean_precisions": 0.9289400000000001, + "std_time": 0.015170650201043303, + "min_time": 0.022162250010296702, + "max_time": 0.3476167500484735, + "rps": 35.25119568789633, + "p50_time": 0.026781812484841794, + "p95_time": 0.032183054107008495, + "p99_time": 0.03674309722380713 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-3-81171-2026-07-09-11-11-47.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-3-81171-2026-07-09-11-11-47.json new file mode 100644 index 0000000..8df2f29 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-3-81171-2026-07-09-11-11-47.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 1, + "search_params": { + "ef": 512 + } + }, + "results": { + "total_time": 36.98827637499198, + "mean_time": 0.03679261278838385, + "mean_precisions": 0.96394, + "std_time": 0.01236326842512667, + "min_time": 0.030016375007107854, + "max_time": 0.34757862507831305, + "rps": 27.035593382667233, + "p50_time": 0.03553052095230669, + "p95_time": 0.04189690242055803, + "p99_time": 0.048114440385252234 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-3-81171-2026-07-09-11-12-23.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-3-81171-2026-07-09-11-12-23.json new file mode 100644 index 0000000..279ddcf --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-3-81171-2026-07-09-11-12-23.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 1, + "search_params": { + "ef": 512 + } + }, + "results": { + "total_time": 36.8265745829558, + "mean_time": 0.03660352200979833, + "mean_precisions": 0.96394, + "std_time": 0.005411315856846923, + "min_time": 0.031047416967339814, + "max_time": 0.1633058749139309, + "rps": 27.154303959153, + "p50_time": 0.035773979499936104, + "p95_time": 0.0423198003671132, + "p99_time": 0.05019083790015429 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-3-81171-2026-07-09-11-13-00.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-3-81171-2026-07-09-11-13-00.json new file mode 100644 index 0000000..b8471f7 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-3-81171-2026-07-09-11-13-00.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 1, + "search_params": { + "ef": 512 + } + }, + "results": { + "total_time": 36.98376929201186, + "mean_time": 0.036753177164588126, + "mean_precisions": 0.96394, + "std_time": 0.011226751500491405, + "min_time": 0.029513000044971704, + "max_time": 0.30539008299820125, + "rps": 27.03888811614425, + "p50_time": 0.03543033299501985, + "p95_time": 0.04251006047124974, + "p99_time": 0.049267584864282975 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-4-81171-2026-07-09-11-13-12.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-4-81171-2026-07-09-11-13-12.json new file mode 100644 index 0000000..ca374ac --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-4-81171-2026-07-09-11-13-12.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 100, + "search_params": { + "ef": 64 + } + }, + "results": { + "total_time": 9.902913665981032, + "mean_time": 0.2728149756315397, + "mean_precisions": 0.8333499999999999, + "std_time": 0.16810540251225248, + "min_time": 0.019779665977694094, + "max_time": 0.6784368749940768, + "rps": 100.98038150481392, + "p50_time": 0.2730056250584312, + "p95_time": 0.6011622041638475, + "p99_time": 0.6412864829308819 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-4-81171-2026-07-09-11-13-24.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-4-81171-2026-07-09-11-13-24.json new file mode 100644 index 0000000..bdcc819 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-4-81171-2026-07-09-11-13-24.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 100, + "search_params": { + "ef": 64 + } + }, + "results": { + "total_time": 10.703075375058688, + "mean_time": 0.29284180128679144, + "mean_precisions": 0.83335, + "std_time": 0.15256995352567806, + "min_time": 0.01912395900581032, + "max_time": 0.6080645410111174, + "rps": 93.43109012670264, + "p50_time": 0.3151053125038743, + "p95_time": 0.5266138562816195, + "p99_time": 0.5472891136934049 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-4-81171-2026-07-09-11-13-36.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-4-81171-2026-07-09-11-13-36.json new file mode 100644 index 0000000..19a020b --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-4-81171-2026-07-09-11-13-36.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 100, + "search_params": { + "ef": 64 + } + }, + "results": { + "total_time": 9.727176625048742, + "mean_time": 0.3029820872955024, + "mean_precisions": 0.83335, + "std_time": 0.1637958912730104, + "min_time": 0.019652624963782728, + "max_time": 0.6996680839220062, + "rps": 102.80475399458362, + "p50_time": 0.3085765420109965, + "p95_time": 0.5868102164240554, + "p99_time": 0.6176466939214151 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-5-81171-2026-07-09-11-13-48.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-5-81171-2026-07-09-11-13-48.json new file mode 100644 index 0000000..6c76957 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-5-81171-2026-07-09-11-13-48.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 100, + "search_params": { + "ef": 128 + } + }, + "results": { + "total_time": 10.7773267920129, + "mean_time": 0.37398590386461, + "mean_precisions": 0.86452, + "std_time": 0.18930815401097928, + "min_time": 0.019984499900601804, + "max_time": 0.7574138339841738, + "rps": 92.78738775380756, + "p50_time": 0.4069954375154339, + "p95_time": 0.6697018441511318, + "p99_time": 0.6900394867942669 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-5-81171-2026-07-09-11-14-00.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-5-81171-2026-07-09-11-14-00.json new file mode 100644 index 0000000..fea785f --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-5-81171-2026-07-09-11-14-00.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 100, + "search_params": { + "ef": 128 + } + }, + "results": { + "total_time": 10.399604332982562, + "mean_time": 0.3603710829747142, + "mean_precisions": 0.86452, + "std_time": 0.19598959361482224, + "min_time": 0.01994870789349079, + "max_time": 0.7892650420544669, + "rps": 96.15750445701853, + "p50_time": 0.33810218749567866, + "p95_time": 0.7201468207931612, + "p99_time": 0.7371126978576649 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-5-81171-2026-07-09-11-14-12.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-5-81171-2026-07-09-11-14-12.json new file mode 100644 index 0000000..086b8e0 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-5-81171-2026-07-09-11-14-12.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 100, + "search_params": { + "ef": 128 + } + }, + "results": { + "total_time": 10.896291334065609, + "mean_time": 0.4096551546496339, + "mean_precisions": 0.86452, + "std_time": 0.17026448102535463, + "min_time": 0.02086070808582008, + "max_time": 0.7561005830066279, + "rps": 91.77434498962515, + "p50_time": 0.44728377100545913, + "p95_time": 0.6014982374792452, + "p99_time": 0.6459974096820222 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-6-81171-2026-07-09-11-14-28.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-6-81171-2026-07-09-11-14-28.json new file mode 100644 index 0000000..b0984c3 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-6-81171-2026-07-09-11-14-28.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 100, + "search_params": { + "ef": 256 + } + }, + "results": { + "total_time": 14.267457165988162, + "mean_time": 0.6979138209379744, + "mean_precisions": 0.9289400000000001, + "std_time": 0.3025660562872884, + "min_time": 0.02501641702838242, + "max_time": 1.2087163340765983, + "rps": 70.0895743625483, + "p50_time": 0.7533841670374386, + "p95_time": 1.1433846536383498, + "p99_time": 1.184416055593174 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-6-81171-2026-07-09-11-14-43.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-6-81171-2026-07-09-11-14-43.json new file mode 100644 index 0000000..efc10c1 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-6-81171-2026-07-09-11-14-43.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 100, + "search_params": { + "ef": 256 + } + }, + "results": { + "total_time": 13.96395266696345, + "mean_time": 0.6925888263297966, + "mean_precisions": 0.92894, + "std_time": 0.29847777345672905, + "min_time": 0.023525208001956344, + "max_time": 1.2057377500459552, + "rps": 71.6129611614801, + "p50_time": 0.7561755005153827, + "p95_time": 1.1094904288300313, + "p99_time": 1.1264719048212282 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-6-81171-2026-07-09-11-14-59.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-6-81171-2026-07-09-11-14-59.json new file mode 100644 index 0000000..18e4b9d --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-6-81171-2026-07-09-11-14-59.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 100, + "search_params": { + "ef": 256 + } + }, + "results": { + "total_time": 15.025643749977462, + "mean_time": 0.7073493322480936, + "mean_precisions": 0.9289400000000001, + "std_time": 0.2938546690633991, + "min_time": 0.023598000057972968, + "max_time": 1.2203531250124797, + "rps": 66.55288895702056, + "p50_time": 0.7498348334920593, + "p95_time": 1.1584034101339058, + "p99_time": 1.170463448772207 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-7-81171-2026-07-09-11-15-22.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-7-81171-2026-07-09-11-15-22.json new file mode 100644 index 0000000..9768f54 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-7-81171-2026-07-09-11-15-22.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 100, + "search_params": { + "ef": 512 + } + }, + "results": { + "total_time": 21.232301082927734, + "mean_time": 1.2957284598058323, + "mean_precisions": 0.96394, + "std_time": 0.556043547211271, + "min_time": 0.030783999944105744, + "max_time": 2.225304792053066, + "rps": 47.098051035272405, + "p50_time": 1.4311856669955887, + "p95_time": 2.142017939593643, + "p99_time": 2.2074745312391313 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-7-81171-2026-07-09-11-15-46.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-7-81171-2026-07-09-11-15-46.json new file mode 100644 index 0000000..d886850 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-search-7-81171-2026-07-09-11-15-46.json @@ -0,0 +1,23 @@ +{ + "params": { + "dataset": "gist-960-euclidean", + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "parallel": 100, + "search_params": { + "ef": 512 + } + }, + "results": { + "total_time": 22.07763270905707, + "mean_time": 1.2684432509638137, + "mean_precisions": 0.96394, + "std_time": 0.6191006401923105, + "min_time": 0.03108383296057582, + "max_time": 2.564353916910477, + "rps": 45.294711311587434, + "p50_time": 1.27599100000225, + "p95_time": 2.413213141268352, + "p99_time": 2.464927445490612 + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-upload-0--1-2026-07-09-11-07-17.json b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-upload-0--1-2026-07-09-11-07-17.json new file mode 100644 index 0000000..073240f --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/results/redis-m-16-ef-64-gist-960-euclidean-upload-0--1-2026-07-09-11-07-17.json @@ -0,0 +1,162 @@ +{ + "params": { + "experiment": "redis-m-16-ef-64", + "engine": "redis", + "dataset": "gist-960-euclidean", + "start_idx": 0, + "end_idx": -1, + "parallel": 32, + "hnsw_config": { + "M": 16, + "EF_CONSTRUCTION": 64, + "EF_RUNTIME": 64, + "RERANK": "TRUE" + } + }, + "results": { + "post_upload": {}, + "upload_time": 481.30283708299976, + "total_time": 481.4557867500698, + "parallel": 32, + "batch_size": 64, + "memory_usage": { + "used_memory": [ + 19154765070 + ], + "index_info": { + "index_name": "idx", + "index_options": [], + "index_definition": [ + "key_type", + "HASH", + "prefixes", + [ + "" + ], + "default_score", + "1", + "indexes_all", + "false" + ], + "attributes": [ + [ + "identifier", + "vector", + "attribute", + "vector", + "type", + "VECTOR", + "algorithm", + "HNSW", + "data_type", + "FLOAT32", + "dim", + 960, + "distance_metric", + "L2", + "M", + 16, + "ef_construction", + 64, + "ef_runtime", + 64, + "rerank", + "true" + ] + ], + "gc_stats": [ + "bytes_collected", + 0, + "total_ms_run", + 0, + "total_cycles", + 1, + "average_cycle_time_ms", + "0", + "last_run_time_ms", + 0 + ], + "cursor_stats": [ + "global_idle", + 0, + "global_total", + 0, + "index_capacity", + 640, + "index_total", + 0 + ], + "dialect_stats": [ + "dialect_1", + 0, + "dialect_2", + 0, + "dialect_3", + 0, + "dialect_4", + 0 + ], + "num_docs": 1000000, + "max_doc_id": 200083, + "num_terms": 0, + "num_records": 0, + "inverted_sz_mb": "0.009765625", + "total_inverted_index_blocks": 0, + "vector_index_sz_mb": "992.8104705810547", + "offset_vectors_sz_mb": "0", + "doc_table_size_mb": "40.03463935852051", + "sortable_values_size_mb": "0", + "key_table_size_mb": "0", + "tag_overhead_sz_mb": "0", + "text_overhead_sz_mb": "0", + "total_index_memory_sz_mb": "4882.472717285156", + "geoshapes_sz_mb": "0", + "records_per_doc_avg": "0", + "bytes_per_record_avg": "nan", + "offsets_per_term_avg": "nan", + "offset_bits_per_record_avg": "nan", + "indexing": 0, + "percent_indexed": "1", + "hash_indexing_failures": 0, + "number_of_uses": 2, + "cleaning": 0, + "Index Errors": [ + "indexing failures", + 0, + "last indexing error", + "N/A", + "last indexing error key", + "N/A", + "background indexing status", + "OK" + ], + "field statistics": [ + [ + "identifier", + "vector", + "attribute", + "vector", + "Index Errors", + [ + "indexing failures", + 0, + "last indexing error", + "N/A", + "last indexing error key", + "N/A" + ], + "memory", + 1041037232, + "marked_deleted", + 0, + "direct_hnsw_insertions", + 0, + "flat_buffer_size", + 0 + ] + ] + }, + "device_info": {} + } + } +} \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/run.py b/02-semantic-search-rag/fcenedes/vecdb_bench/run.py new file mode 100755 index 0000000..b155a31 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/run.py @@ -0,0 +1,401 @@ +import fnmatch +import json +import os +import traceback +import warnings +from typing import List + +# Suppress the pkg_resources deprecation warning from stopit +warnings.filterwarnings("ignore", message="pkg_resources is deprecated", category=UserWarning) + +import stopit +import typer + +from benchmark.config_read import read_dataset_config, read_engine_configs +from benchmark.dataset import Dataset +from engine.base_client import IncompatibilityError +from engine.clients.client_factory import ClientFactory + +app = typer.Typer() + + +def load_engines(engines: List[str], engines_file: str = None) -> dict: + """Load engine configurations from file or pattern matching.""" + # Check if both engines and engines_file are provided + if engines != ["*"] and engines_file is not None: + typer.echo("Error: Cannot use both --engines and --engines-file at the same time.", err=True) + raise typer.Exit(1) + + # Load engine configurations + if engines_file is not None: + # Load engines from specified file + if not os.path.exists(engines_file): + typer.echo(f"Error: Engines file '{engines_file}' not found.", err=True) + raise typer.Exit(1) + + try: + with open(engines_file, 'r') as f: + engines_from_file = json.load(f) + + # Convert list of engine configs to dictionary with name as key + selected_engines = {} + for config in engines_from_file: + if 'name' not in config: + typer.echo(f"Error: Engine configuration missing 'name' field in {engines_file}", err=True) + raise typer.Exit(1) + selected_engines[config['name']] = config + + except json.JSONDecodeError as e: + typer.echo(f"Error: Invalid JSON in engines file '{engines_file}': {e}", err=True) + raise typer.Exit(1) + except Exception as e: + typer.echo(f"Error reading engines file '{engines_file}': {e}", err=True) + raise typer.Exit(1) + else: + # Load engines using pattern matching (original behavior) + all_engines = read_engine_configs() + selected_engines = { + name: config + for name, config in all_engines.items() + if any(fnmatch.fnmatch(name, engine) for engine in engines) + } + + return selected_engines + + +@app.command() +def run( + engines: List[str] = typer.Option(["*"]), + engines_file: str = typer.Option(None, help="Path to JSON file containing engine configurations to use instead of searching by pattern"), + datasets: List[str] = typer.Option(["*"]), + parallels: List[int] = typer.Option([]), + host: str = "localhost", + skip_upload: bool = False, + skip_search: bool = False, + skip_if_exists: bool = True, + exit_on_error: bool = True, + timeout: float = 86400.0, + upload_start_idx: int = 0, + upload_end_idx: int = -1, + queries: int = typer.Option(-1, help="Number of queries to run. If the available queries are fewer, they will be reused."), + ef_runtime: List[int] = typer.Option([], help="Filter search experiments by ef runtime values. Only experiments with these ef values will be run."), + describe: str = typer.Option(None, help="Describe available options: 'datasets' or 'engines'. When used, shows information and exits."), + verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed information when using --describe"), +): + """ + Examples: + # Use pattern matching to select engines (original behavior) + python3 run.py --engines *-m-16-* --engines qdrant-* --datasets glove-* + + # Use engines from a specific JSON file + python3 run.py --engines-file my_engines.json --datasets glove-* + + # Describe available options + python3 run.py --describe datasets + python3 run.py --describe engines --verbose + """ + # Handle describe option first + if describe: + if describe.lower() == "datasets": + describe_datasets(datasets[0] if datasets != ["*"] else "*", verbose) + return + elif describe.lower() == "engines": + # Load engines using same logic as main function + selected_engines = load_engines(engines, engines_file) + # For describe engines, we'll pass all loaded engines or filter by pattern + if engines_file is not None: + # When using engines_file, show all engines from the file + describe_engines_with_configs(selected_engines, "*", verbose) + else: + # When using pattern matching, use the pattern + describe_engines_with_configs(selected_engines, engines[0] if engines != ["*"] else "*", verbose) + return + else: + typer.echo(f"Error: Unknown describe target '{describe}'. Use 'datasets' or 'engines'.", err=True) + raise typer.Exit(1) + + all_datasets = read_dataset_config() + selected_engines = load_engines(engines, engines_file) + + selected_datasets = { + name: config + for name, config in all_datasets.items() + if any(fnmatch.fnmatch(name, dataset) for dataset in datasets) + } + + for engine_name, engine_config in selected_engines.items(): + for dataset_name, dataset_config in selected_datasets.items(): + print(f"Running experiment: {engine_name} - {dataset_name}") + client = ClientFactory(host).build_client(engine_config) + dataset = Dataset( + dataset_config, + skip_upload, + skip_search, + upload_start_idx, + upload_end_idx, + ) + dataset.download() + try: + with stopit.ThreadingTimeout(timeout) as tt: + client.run_experiment( + dataset, + skip_upload, + skip_search, + skip_if_exists, + parallels, + upload_start_idx, + upload_end_idx, + queries, + ef_runtime, + ) + client.delete_client() + + # If the timeout is reached, the server might be still in the + # middle of some background processing, like creating the index. + # Next experiment should not be launched. It's better to reset + # the server state manually. + if tt.state != stopit.ThreadingTimeout.EXECUTED: + print( + f"Timed out {engine_name} - {dataset_name}, " + f"exceeded {timeout} seconds" + ) + exit(2) + except IncompatibilityError as e: + print(f"Skipping {engine_name} - {dataset_name}, incompatible params") + continue + except KeyboardInterrupt as e: + traceback.print_exc() + exit(1) + except Exception as e: + print(f"Experiment {engine_name} - {dataset_name} interrupted") + traceback.print_exc() + if exit_on_error: + raise e + continue + + +def describe_datasets(filter_pattern: str = "*", verbose: bool = False): + """Display information about available datasets.""" + try: + all_datasets = read_dataset_config() + except Exception as e: + typer.echo(f"Error reading dataset configuration: {e}", err=True) + raise typer.Exit(1) + + # Filter datasets + filtered_datasets = { + name: config + for name, config in all_datasets.items() + if fnmatch.fnmatch(name, filter_pattern) + } + + if not filtered_datasets: + typer.echo(f"No datasets found matching pattern '{filter_pattern}'") + return + + typer.echo(f"\n📊 Available Datasets ({len(filtered_datasets)} found)") + typer.echo("=" * 80) + + # Sort datasets by dimension, then by vector count, then by name + def get_sort_key(item): + name, config = item + # Get dimension (vector_size) + dimension = config.get('vector_size', 0) + if dimension == 'N/A': + dimension = 0 + + # Get vector count from config, fallback to 0 if None or missing + vector_count = config.get('vector_count', 0) + if vector_count is None: + vector_count = 0 + + return (dimension, vector_count, name.lower()) + + sorted_datasets = sorted(filtered_datasets.items(), key=get_sort_key) + + if verbose: + # Detailed view + for name, config in sorted_datasets: + typer.echo(f"\n🔹 {name}") + typer.echo(f" Vector Size: {config.get('vector_size', 'N/A')}") + typer.echo(f" Distance: {config.get('distance', 'N/A')}") + typer.echo(f" Type: {config.get('type', 'N/A')}") + typer.echo(f" Path: {config.get('path', 'N/A')}") + if 'link' in config: + typer.echo(f" Download: {config['link']}") + if 'schema' in config: + typer.echo(f" Schema: {config['schema']}") + else: + # Compact table view with proper columnar formatting + col_widths = [35, 6, 8, 12, 30, 20] # Dataset Name, Dims, Distance, Vector Count, Description, Schema + headers = ["Dataset Name ", "Dims ", "Distance ", "Vector Count ", "Description ", "Schema "] + + # Print headers + header_line = "" + for header, width in zip(headers, col_widths): + header_line += f"{header:<{width}}" + typer.echo(header_line) + typer.echo("-" * sum(col_widths)) + + for name, config in sorted_datasets: + dimensions = str(config.get('vector_size', 'N/A')) + distance = config.get('distance', 'N/A') + + # Get vector count + vector_count = config.get('vector_count') + if vector_count is None: + vector_count_str = 'N/A' + else: + # Format large numbers with appropriate suffixes + if vector_count >= 1000000000: + vector_count_str = f"{vector_count / 1000000000:.1f}B" + elif vector_count >= 1000000: + vector_count_str = f"{vector_count / 1000000:.1f}M" + elif vector_count >= 1000: + vector_count_str = f"{vector_count / 1000:.1f}K" + else: + vector_count_str = str(vector_count) + + # Get description from config + description = config.get('description', 'N/A') + + # Truncate description if too long + if len(description) > col_widths[4] - 1: + description = description[:col_widths[4] - 4] + "..." + + # Get schema information - always show field count first + schema_info = "" + if 'schema' in config: + schema = config['schema'] + if isinstance(schema, dict): + field_count = len(schema) + + # Always start with field count + if field_count == 0: + schema_info = "0 fields" + elif field_count == 1: + schema_info = "1 field" + else: + schema_info = f"{field_count} fields" + + # Try to add details if they fit + if field_count > 0: + if field_count <= 2: + # For small schemas, try to show field names + field_names = ", ".join(schema.keys()) + test_info = f"{schema_info}: {field_names}" + if len(test_info) <= col_widths[4] - 1: + schema_info = test_info + else: + # For larger schemas, try to show types + field_types = sorted(set(schema.values())) + types_str = ", ".join(field_types) + test_info = f"{schema_info} ({types_str})" + if len(test_info) <= col_widths[4] - 1: + schema_info = test_info + else: + schema_info = str(schema) + + # Final truncation if still too long + if len(schema_info) > col_widths[4] - 1: + schema_info = schema_info[:col_widths[4] - 4] + "..." + + # Truncate name if too long + display_name = name + if len(display_name) > col_widths[0] - 1: + display_name = display_name[:col_widths[0] - 4] + "..." + + # Print row with proper column alignment + row_line = "" + values = [display_name, dimensions, distance, vector_count_str, description, schema_info] + for value, width in zip(values, col_widths): + row_line += f"{value:<{width}}" + typer.echo(row_line) + + typer.echo(f"\nTotal: {len(filtered_datasets)} datasets") + if filter_pattern != "*": + typer.echo(f"Filter: '{filter_pattern}'") + typer.echo("\nUse --verbose for detailed information") + + +def describe_engines_with_configs(engines_dict: dict, filter_pattern: str = "*", verbose: bool = False): + """Display information about engines from provided configurations.""" + # Filter engines + filtered_engines = { + name: config + for name, config in engines_dict.items() + if fnmatch.fnmatch(name, filter_pattern) + } + + if not filtered_engines: + typer.echo(f"No engines found matching pattern '{filter_pattern}'") + return + + typer.echo(f"\n🚀 Available Engines ({len(filtered_engines)} found)") + typer.echo("=" * 80) + + if verbose: + # Detailed view + for name, config in sorted(filtered_engines.items()): + typer.echo(f"\n🔹 {name}") + typer.echo(f" Engine: {config.get('engine', 'N/A')}") + typer.echo(f" Module: {config.get('module', 'N/A')}") + if 'docker' in config: + typer.echo(f" Docker: {config['docker']}") + if 'search_params' in config: + search_params = config['search_params'] + typer.echo(f" Search Params:") + if isinstance(search_params, list): + for i, param_config in enumerate(search_params): + typer.echo(f" Config {i+1}:") + for param, value in param_config.items(): + if isinstance(value, dict): + typer.echo(f" {param}:") + for subparam, subvalue in value.items(): + typer.echo(f" {subparam}: {subvalue}") + else: + typer.echo(f" {param}: {value}") + else: + # Legacy format - dict + for param, values in search_params.items(): + if isinstance(values, list): + typer.echo(f" {param}: {values}") + else: + typer.echo(f" {param}: {values}") + if 'upload_params' in config: + upload_params = config['upload_params'] + typer.echo(f" Upload Params:") + for param, value in upload_params.items(): + typer.echo(f" {param}: {value}") + else: + # Compact table view + typer.echo(f"{'Engine Name':<40} {'Engine Type':<15} {'Module':<25}") + typer.echo("-" * 80) + for name, config in sorted(filtered_engines.items()): + engine_type = config.get('engine', 'N/A') + module = config.get('module', 'N/A') + display_name = name[:37] + "..." if len(name) > 40 else name + display_engine = engine_type[:12] + "..." if len(engine_type) > 15 else engine_type + display_module = module[:22] + "..." if len(module) > 25 else module + typer.echo(f"{display_name:<40} {display_engine:<15} {display_module:<25}") + + typer.echo(f"\nTotal: {len(filtered_engines)} engines") + if filter_pattern != "*": + typer.echo(f"Filter: '{filter_pattern}'") + if not verbose: + typer.echo("\nUse --verbose for detailed information") + + +def describe_engines(filter_pattern: str = "*", verbose: bool = False): + """Display information about available engines using default configuration loading.""" + try: + all_engines = read_engine_configs() + except Exception as e: + typer.echo(f"Error reading engine configuration: {e}", err=True) + raise typer.Exit(1) + + describe_engines_with_configs(all_engines, filter_pattern, verbose) + + +if __name__ == "__main__": + app() diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/run_bench.py b/02-semantic-search-rag/fcenedes/vecdb_bench/run_bench.py new file mode 100755 index 0000000..42e6153 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/run_bench.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +""" +Benchmark runner script - Python version of run_bench.sh +Executes Redis and MongoDB benchmarks with various configurations. +""" + +import os +import re +import subprocess +import sys + + +def extract_from_connection_string(connection_string, pattern): + """Extract a component from MongoDB connection string using regex.""" + match = re.search(pattern, connection_string) + return match.group(1) if match else "" + + +def get_poetry_venv_path(): + """Get the poetry virtual environment path.""" + try: + result = subprocess.run( + ["poetry", "env", "info", "--path"], + capture_output=True, + text=True, + check=True + ) + return result.stdout.strip() + except subprocess.CalledProcessError as e: + print(f"Error getting poetry environment: {e}", file=sys.stderr) + sys.exit(1) + + +def run_experiments(engines_file, dataset, host, description=""): + """Run all benchmark experiments defined in a single engines file. + + Using --engines-file makes the given file the single source of truth for + engine configs, bypassing run.py's glob-and-merge over experiments/configurations/*.json + (which merges duplicate engine names across files with last-wins order and would + otherwise ignore edits to benchmark.json).""" + print("-----------------------------------") + if description: + print(f"Running experiments from: {engines_file} - {description}") + else: + print(f"Running experiments from: {engines_file}") + + cmd = ["python", "run.py", "--engines-file", engines_file, "--datasets", dataset, "--host", host] + + try: + subprocess.run(cmd, check=True) + print(f"Completed experiments from: {engines_file}") + except subprocess.CalledProcessError as e: + print(f"Error running experiments from {engines_file}: {e}", file=sys.stderr) + sys.exit(1) + + print("-----------------------------------") + + +def main(): + # ============== Redis ENV VARS ============== + + # ============== General ENV VARS ============== + os.environ["DATASETS"] = os.getenv("BENCH_DATASETS", "gist-960-euclidean") + os.environ["ENGINES"] = os.getenv("BENCH_ENGINES", "redis") + + # Redis Flex (Disk HNSW) does not support DIALECT 4, so default the query + # dialect to 2. Override by exporting REDIS_DIALECT before launching. + os.environ.setdefault("REDIS_DIALECT", "2") + + + # Print configuration (with sensitive values redacted) + print("REDIS_PORT:", os.environ["REDIS_PORT"]) + print("REDIS_HOST:", os.environ["REDIS_HOST"]) + print("REDIS_USER: [REDACTED]") + print("REDIS_AUTH: [REDACTED]") + print("DATASETS:", os.environ["DATASETS"]) + print("ENGINES:", os.environ["ENGINES"]) + print("REDIS_DIALECT:", os.environ["REDIS_DIALECT"]) + print() + + # Activate poetry virtual environment + venv_path = get_poetry_venv_path() + activate_script = os.path.join(venv_path, "bin", "activate") + + # Note: We don't need to source activate in Python, just ensure we're using the right Python + # The subprocess calls will inherit the environment + + # Define experiments + datasets = os.environ["DATASETS"] + redis_host = os.environ["REDIS_HOST"] + + + + # Redis experiments are defined in this file (single source of truth). + engines_file = os.getenv( + "BENCH_ENGINES_FILE", "experiments/configurations/benchmark.json" + ) + + # Run Redis experiments + run_experiments(engines_file, datasets, redis_host, "Redis") + + print("All experiments completed!") + + +if __name__ == "__main__": + main() + diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/run_quick.py b/02-semantic-search-rag/fcenedes/vecdb_bench/run_quick.py new file mode 100755 index 0000000..40663ca --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/run_quick.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +""" +Quick benchmark runner script - Python version of run_quick.sh +Executes quick Redis and MongoDB benchmarks for testing. +""" + +import os +import re +import subprocess +import sys + + +def extract_from_connection_string(connection_string, pattern): + """Extract a component from MongoDB connection string using regex.""" + match = re.search(pattern, connection_string) + return match.group(1) if match else "" + + +def get_poetry_venv_path(): + """Get the poetry virtual environment path.""" + try: + result = subprocess.run( + ["poetry", "env", "info", "--path"], + capture_output=True, + text=True, + check=True + ) + return result.stdout.strip() + except subprocess.CalledProcessError as e: + print(f"Error getting poetry environment: {e}", file=sys.stderr) + sys.exit(1) + + +def run_experiment(experiment_name, dataset, host, description=""): + """Run a single benchmark experiment.""" + print("-----------------------------------") + if description: + print(f"Running experiment: {experiment_name} - {description}") + else: + print(f"Running experiment: {experiment_name}") + + cmd = ["python", "run.py", "--engines", experiment_name, "--datasets", dataset, "--host", host] + + try: + subprocess.run(cmd, check=True) + print(f"Completed experiment: {experiment_name}") + except subprocess.CalledProcessError as e: + print(f"Error running experiment {experiment_name}: {e}", file=sys.stderr) + sys.exit(1) + + print("-----------------------------------") + + +def main(): + # ============== Redis ENV VARS ============== + os.environ["REDIS_PORT"] = os.getenv("REDIS_DB_PORT", "") + os.environ["REDIS_HOST"] = os.getenv("REDIS_HOST", "") + os.environ["REDIS_USER"] = os.getenv("REDIS_RW_USER", "") + os.environ["REDIS_AUTH"] = os.getenv("REDIS_RW_PASS", "") + + # ============== General ENV VARS ============== + os.environ["DATASETS"] = os.getenv("BENCH_DATASETS", "random-100-euclidean") + os.environ["ENGINES"] = os.getenv("BENCH_ENGINES", "redis") + + # ============== MONGO ENV VARS ============== + mongo_connection_string = os.getenv( + "MONGO_CONNECTION_STRING", + "" + ) + os.environ["MONGO_CONNECTION_STRING"] = mongo_connection_string + + # Extract HOST, USER, AUTH from connection string + os.environ["MONGO_HOST"] = extract_from_connection_string( + mongo_connection_string, r'.*@([^:/]+).*' + ) + os.environ["MONGO_USER"] = extract_from_connection_string( + mongo_connection_string, r'.*://([^:]+):.*' + ) + os.environ["MONGO_AUTH"] = extract_from_connection_string( + mongo_connection_string, r'.*://[^:]+:([^@]+)@.*' + ) + + os.environ["MONGO_READ_PREFERENCE"] = os.getenv("MONGO_READ_PREFERENCE", "primary") + os.environ["MONGO_WRITE_CONCERN"] = os.getenv("MONGO_WRITE_CONCERN", "1") + os.environ["EMBEDDING_FIELD_NAME"] = os.getenv("EMBEDDING_FIELD_NAME", "embedding") + os.environ["ATLAS_DB_NAME"] = os.getenv("MONGO_DB", "vector-db") + os.environ["ATLAS_COLLECTION_NAME"] = os.getenv("ATLAS_COLLECTION_NAME", "vector-collection") + os.environ["ATLAS_VECTOR_SEARCH_INDEX_NAME"] = os.getenv("ATLAS_VECTOR_SEARCH_INDEX_NAME", "vector-index") + + # Print configuration (with sensitive values redacted) + print("REDIS_PORT:", os.environ["REDIS_PORT"]) + print("REDIS_HOST:", os.environ["REDIS_HOST"]) + print("REDIS_USER: [REDACTED]") + print("REDIS_AUTH: [REDACTED]") + print("DATASETS:", os.environ["DATASETS"]) + print("ENGINES:", os.environ["ENGINES"]) + print("MONGO_CONNECTION_STRING: [REDACTED]") + print("MONGO_PORT:", os.environ.get("MONGO_PORT", "N/A")) + print("MONGO_HOST:", os.environ["MONGO_HOST"]) + print("MONGO_USER: [REDACTED]") + print("MONGO_AUTH: [REDACTED]") + print() + + # Activate poetry virtual environment + venv_path = get_poetry_venv_path() + activate_script = os.path.join(venv_path, "bin", "activate") + + # Note: We don't need to source activate in Python, just ensure we're using the right Python + # The subprocess calls will inherit the environment + + # Define experiments + datasets = os.environ["DATASETS"] + redis_host = os.environ["REDIS_HOST"] + mongo_host = os.environ["MONGO_HOST"] + + # Quick test experiments + run_experiment("redis-quicktest", datasets, redis_host, "Redis VS MongoDB - Default") + run_experiment("mongodb-quicktest", datasets, mongo_host, "Redis VS MongoDB - Default") + + print("Redis VS MongoDB - Default completed!") + print("All experiments completed!") + + +if __name__ == "__main__": + main() + diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/scripts/process-benchmarks.ipynb b/02-semantic-search-rag/fcenedes/vecdb_bench/scripts/process-benchmarks.ipynb new file mode 100644 index 0000000..0859490 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/scripts/process-benchmarks.ipynb @@ -0,0 +1,960 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Notebook to process benchmar results\n", + "\n", + "Please run this notebook after running all the benchmarks and storing them in the `results` dir. This will export them in the desired format for the single node benchmark plots of [qdrant.tech/benchmarks](https://qdrant.tech/benchmarks)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "ExecuteTime": { + "end_time": "2022-08-05T10:03:50.900734Z", + "start_time": "2022-08-05T10:03:50.892651Z" + }, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import re\n", + "import json\n", + "import pandas as pd\n", + "from datetime import datetime, timezone" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "ExecuteTime": { + "end_time": "2022-08-05T10:03:50.982398Z", + "start_time": "2022-08-05T10:03:50.903447Z" + }, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(PosixPath('/Users/dvir/Code/vector-db-benchmark/results'),\n", + " 'vectorsets-q8-m-64-ef-512-random-100-search-2-2025-03-18-23-31-46.json')" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "DATA_DIR = Path().resolve().parent / \"results\"\n", + "DATA_DIR, list(DATA_DIR.glob(\"*.json\"))[0].name" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "ExecuteTime": { + "end_time": "2022-08-05T10:03:51.482299Z", + "start_time": "2022-08-05T10:03:51.477275Z" + }, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "PATH_REGEX = re.compile(r\"(?P(\"\n", + " r\"?P[a-z\\-]+)\"\n", + " r\"\\-(?P[a-zA-Z0-9\\-]+)\"\n", + " r\"\\-m\\-(?P[0-9]+)\"\n", + " r\"\\-ef\\-(?P[0-9]+)\"\n", + " r\")\"\n", + " r\"\\-(?P[a-zA-Z0-9\\-]+)\"\n", + " r\"\\-(?P(search)|(upload))\"\n", + " r\"(\\-(?P[0-9]{1,2})\\-)?\"\n", + " r\"\\-?(?P.*)\\.json\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2022-08-05T10:03:54.150582Z", + "start_time": "2022-08-05T10:03:51.558766Z" + }, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[['vectorsets',\n", + " '64',\n", + " '512',\n", + " 'q8',\n", + " 'random-100',\n", + " '2',\n", + " '2025-03-18-23-31-46',\n", + " {'dataset': 'random-100',\n", + " 'experiment': 'vectorsets-q8-m-64-ef-512',\n", + " 'engine': 'vectorsets',\n", + " 'parallel': 1,\n", + " 'search_params': {'ef': 256}},\n", + " {'total_time': 0.0025846249773167074,\n", + " 'mean_time': 0.00020889558945782482,\n", + " 'mean_precisions': 1.0,\n", + " 'std_time': 0.00012517310030909733,\n", + " 'min_time': 0.00012079102452844381,\n", + " 'max_time': 0.0005068749887868762,\n", + " 'rps': 3869.033259278392,\n", + " 'p95_time': 0.0004580310720484703,\n", + " 'p99_time': 0.000497106205439195}],\n", + " ['vectorsets',\n", + " '64',\n", + " '512',\n", + " 'q8',\n", + " 'random-100',\n", + " '5',\n", + " '2025-03-18-23-32-25',\n", + " {'dataset': 'random-100',\n", + " 'experiment': 'vectorsets-q8-m-64-ef-512',\n", + " 'engine': 'vectorsets',\n", + " 'parallel': 100,\n", + " 'search_params': {'ef': 128}},\n", + " {'total_time': 4.754267499956768,\n", + " 'mean_time': 0.0019451417960226537,\n", + " 'mean_precisions': 1.0,\n", + " 'std_time': 0.0037093798096747236,\n", + " 'min_time': 0.0004131249734200537,\n", + " 'max_time': 0.013048959022853523,\n", + " 'rps': 2.103373442931205,\n", + " 'p95_time': 0.007698571513174089,\n", + " 'p99_time': 0.011978881520917641}],\n", + " ['vectorsets',\n", + " '64',\n", + " '512',\n", + " 'q8',\n", + " 'random-100',\n", + " '4',\n", + " '2025-03-18-23-32-05',\n", + " {'dataset': 'random-100',\n", + " 'experiment': 'vectorsets-q8-m-64-ef-512',\n", + " 'engine': 'vectorsets',\n", + " 'parallel': 100,\n", + " 'search_params': {'ef': 64}},\n", + " {'total_time': 2.0294713340117596,\n", + " 'mean_time': 0.03196047500241548,\n", + " 'mean_precisions': 1.0,\n", + " 'std_time': 0.08439925765507372,\n", + " 'min_time': 0.00027545803459361196,\n", + " 'max_time': 0.2835138339432888,\n", + " 'rps': 4.927391598201336,\n", + " 'p95_time': 0.1706198027444768,\n", + " 'p99_time': 0.26093502770352645}],\n", + " ['vectorsets',\n", + " '64',\n", + " '512',\n", + " 'q8',\n", + " 'random-100',\n", + " '3',\n", + " '2025-03-18-23-31-46',\n", + " {'dataset': 'random-100',\n", + " 'experiment': 'vectorsets-q8-m-64-ef-512',\n", + " 'engine': 'vectorsets',\n", + " 'parallel': 1,\n", + " 'search_params': {'ef': 512}},\n", + " {'total_time': 0.002255416999105364,\n", + " 'mean_time': 0.0001831832982134074,\n", + " 'mean_precisions': 1.0,\n", + " 'std_time': 0.00013853265792410592,\n", + " 'min_time': 0.00010545901022851467,\n", + " 'max_time': 0.000520500005222857,\n", + " 'rps': 4433.769898855338,\n", + " 'p95_time': 0.0004601064021699129,\n", + " 'p99_time': 0.0005084212846122682}],\n", + " ['vectorsets',\n", + " '64',\n", + " '512',\n", + " 'q8',\n", + " 'random-100',\n", + " '7',\n", + " '2025-03-18-23-33-03',\n", + " {'dataset': 'random-100',\n", + " 'experiment': 'vectorsets-q8-m-64-ef-512',\n", + " 'engine': 'vectorsets',\n", + " 'parallel': 100,\n", + " 'search_params': {'ef': 512}},\n", + " {'total_time': 3.4431491250288673,\n", + " 'mean_time': 0.03187979999929667,\n", + " 'mean_precisions': 1.0,\n", + " 'std_time': 0.06949698412336956,\n", + " 'min_time': 0.0003655419568531215,\n", + " 'max_time': 0.23800891602877527,\n", + " 'rps': 2.9043180056617968,\n", + " 'p95_time': 0.14553500411275289,\n", + " 'p99_time': 0.21951413364557087}],\n", + " ['vectorsets',\n", + " '64',\n", + " '512',\n", + " 'q8',\n", + " 'random-100',\n", + " '6',\n", + " '2025-03-18-23-32-44',\n", + " {'dataset': 'random-100',\n", + " 'experiment': 'vectorsets-q8-m-64-ef-512',\n", + " 'engine': 'vectorsets',\n", + " 'parallel': 100,\n", + " 'search_params': {'ef': 256}},\n", + " {'total_time': 2.7904384169960395,\n", + " 'mean_time': 0.000987829192308709,\n", + " 'mean_precisions': 1.0,\n", + " 'std_time': 0.0018635207020379135,\n", + " 'min_time': 0.0002573750098235905,\n", + " 'max_time': 0.006563208997249603,\n", + " 'rps': 3.583666257994395,\n", + " 'p95_time': 0.003952683851821342,\n", + " 'p99_time': 0.006041103968163953}],\n", + " ['vectorsets',\n", + " '64',\n", + " '512',\n", + " 'q8',\n", + " 'random-100',\n", + " '1',\n", + " '2025-03-18-23-31-46',\n", + " {'dataset': 'random-100',\n", + " 'experiment': 'vectorsets-q8-m-64-ef-512',\n", + " 'engine': 'vectorsets',\n", + " 'parallel': 1,\n", + " 'search_params': {'ef': 128}},\n", + " {'total_time': 0.0023579999688081443,\n", + " 'mean_time': 0.00017647920176386834,\n", + " 'mean_precisions': 1.0,\n", + " 'std_time': 0.00012461744999991972,\n", + " 'min_time': 0.00011404202086851001,\n", + " 'max_time': 0.0005437919753603637,\n", + " 'rps': 4240.882159576329,\n", + " 'p95_time': 0.00038786699296906554,\n", + " 'p99_time': 0.0005126069788821043}],\n", + " ['vectorsets',\n", + " '64',\n", + " '512',\n", + " 'q8',\n", + " 'random-100',\n", + " '0',\n", + " '2025-03-18-23-31-46',\n", + " {'dataset': 'random-100',\n", + " 'experiment': 'vectorsets-q8-m-64-ef-512',\n", + " 'engine': 'vectorsets',\n", + " 'parallel': 1,\n", + " 'search_params': {'ef': 64}},\n", + " {'total_time': 0.003914333006832749,\n", + " 'mean_time': 0.0002679917146451771,\n", + " 'mean_precisions': 1.0,\n", + " 'std_time': 0.00036875994409555664,\n", + " 'min_time': 0.00011029100278392434,\n", + " 'max_time': 0.001371125050354749,\n", + " 'rps': 2554.7136594010485,\n", + " 'p95_time': 0.0008407815796090277,\n", + " 'p99_time': 0.0012650563562056052}]]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "upload_results, search_results = [], []\n", + "\n", + "for path in DATA_DIR.glob(\"*.json\"):\n", + " match = PATH_REGEX.match(path.name)\n", + " if match is None:\n", + " continue\n", + " \n", + " experiment = match.groupdict()\n", + " \n", + " with open(path, \"r\") as fp:\n", + " stats = json.load(fp)\n", + "\n", + " entry = [match[\"engine\"], match[\"m\"], match[\"ef\"], match[\"quant\"],\n", + " match[\"dataset\"], match[\"search_index\"], match[\"date\"], \n", + " stats[\"params\"], stats[\"results\"]]\n", + " if experiment[\"operation\"] == \"search\":\n", + " search_results.append(entry)\n", + " elif experiment[\"operation\"] == \"upload\":\n", + " upload_results.append(entry)\n", + "\n", + "len(upload_results), len(search_results)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "ExecuteTime": { + "end_time": "2022-08-05T10:03:54.157465Z", + "start_time": "2022-08-05T10:03:54.153118Z" + }, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "column_names = [\"engine\", \"m\", \"ef\", \"quant\", \"dataset\", \"search_index\", \"date\", \"params\", \"results\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "ExecuteTime": { + "end_time": "2022-08-05T11:31:17.192306Z", + "start_time": "2022-08-05T11:31:17.125766Z" + }, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
dateparamspost_uploadupload_timetotal_time
enginemefquantdataset
vectorsets64512q8random-1002025-03-18 23:31:46{'experiment': 'vectorsets-q8-m-64-ef-512', 'e...{}3.5902913.590334
\n", + "
" + ], + "text/plain": [ + " date \\\n", + "engine m ef quant dataset \n", + "vectorsets 64 512 q8 random-100 2025-03-18 23:31:46 \n", + "\n", + " params \\\n", + "engine m ef quant dataset \n", + "vectorsets 64 512 q8 random-100 {'experiment': 'vectorsets-q8-m-64-ef-512', 'e... \n", + "\n", + " post_upload upload_time total_time \n", + "engine m ef quant dataset \n", + "vectorsets 64 512 q8 random-100 {} 3.590291 3.590334 " + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "upload_df = pd.DataFrame(upload_results, columns=column_names) \\\n", + " .drop(columns=\"search_index\")\n", + "upload_df[\"date\"] = pd.to_datetime(upload_df[\"date\"], format=\"%Y-%m-%d-%H-%M-%S\")\n", + "upload_df = upload_df.sort_values(\"date\", ascending=False) \\\n", + " .groupby([\"engine\", \"m\", \"ef\", \"quant\", \"dataset\"]) \\\n", + " .last()\n", + "upload_df = pd.concat([upload_df, upload_df[\"results\"].apply(pd.Series)], axis=1)\n", + "upload_df = upload_df.drop(columns=\"results\")\n", + "\n", + "print(len(upload_df))\n", + "\n", + "upload_df.sort_values(\"total_time\", ascending=True).head(n=5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2022-08-05T12:06:58.459530Z", + "start_time": "2022-08-05T12:06:57.908842Z" + }, + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "8\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
datedatasetexperimentengineparallelsearch_paramstotal_timemean_timemean_precisionsstd_timemin_timemax_timerpsp95_timep99_time
enginemefdatasetquantsearch_index
vectorsets64512random-100q832025-03-18 23:31:46random-100vectorsets-q8-m-64-ef-512vectorsets1{'ef': 512}0.0022550.0001831.00.0001390.0001050.0005214433.7698990.0004600.000508
12025-03-18 23:31:46random-100vectorsets-q8-m-64-ef-512vectorsets1{'ef': 128}0.0023580.0001761.00.0001250.0001140.0005444240.8821600.0003880.000513
22025-03-18 23:31:46random-100vectorsets-q8-m-64-ef-512vectorsets1{'ef': 256}0.0025850.0002091.00.0001250.0001210.0005073869.0332590.0004580.000497
02025-03-18 23:31:46random-100vectorsets-q8-m-64-ef-512vectorsets1{'ef': 64}0.0039140.0002681.00.0003690.0001100.0013712554.7136590.0008410.001265
42025-03-18 23:32:05random-100vectorsets-q8-m-64-ef-512vectorsets100{'ef': 64}2.0294710.0319601.00.0843990.0002750.2835144.9273920.1706200.260935
62025-03-18 23:32:44random-100vectorsets-q8-m-64-ef-512vectorsets100{'ef': 256}2.7904380.0009881.00.0018640.0002570.0065633.5836660.0039530.006041
72025-03-18 23:33:03random-100vectorsets-q8-m-64-ef-512vectorsets100{'ef': 512}3.4431490.0318801.00.0694970.0003660.2380092.9043180.1455350.219514
52025-03-18 23:32:25random-100vectorsets-q8-m-64-ef-512vectorsets100{'ef': 128}4.7542670.0019451.00.0037090.0004130.0130492.1033730.0076990.011979
\n", + "
" + ], + "text/plain": [ + " date \\\n", + "engine m ef dataset quant search_index \n", + "vectorsets 64 512 random-100 q8 3 2025-03-18 23:31:46 \n", + " 1 2025-03-18 23:31:46 \n", + " 2 2025-03-18 23:31:46 \n", + " 0 2025-03-18 23:31:46 \n", + " 4 2025-03-18 23:32:05 \n", + " 6 2025-03-18 23:32:44 \n", + " 7 2025-03-18 23:33:03 \n", + " 5 2025-03-18 23:32:25 \n", + "\n", + " dataset \\\n", + "engine m ef dataset quant search_index \n", + "vectorsets 64 512 random-100 q8 3 random-100 \n", + " 1 random-100 \n", + " 2 random-100 \n", + " 0 random-100 \n", + " 4 random-100 \n", + " 6 random-100 \n", + " 7 random-100 \n", + " 5 random-100 \n", + "\n", + " experiment \\\n", + "engine m ef dataset quant search_index \n", + "vectorsets 64 512 random-100 q8 3 vectorsets-q8-m-64-ef-512 \n", + " 1 vectorsets-q8-m-64-ef-512 \n", + " 2 vectorsets-q8-m-64-ef-512 \n", + " 0 vectorsets-q8-m-64-ef-512 \n", + " 4 vectorsets-q8-m-64-ef-512 \n", + " 6 vectorsets-q8-m-64-ef-512 \n", + " 7 vectorsets-q8-m-64-ef-512 \n", + " 5 vectorsets-q8-m-64-ef-512 \n", + "\n", + " engine parallel \\\n", + "engine m ef dataset quant search_index \n", + "vectorsets 64 512 random-100 q8 3 vectorsets 1 \n", + " 1 vectorsets 1 \n", + " 2 vectorsets 1 \n", + " 0 vectorsets 1 \n", + " 4 vectorsets 100 \n", + " 6 vectorsets 100 \n", + " 7 vectorsets 100 \n", + " 5 vectorsets 100 \n", + "\n", + " search_params total_time \\\n", + "engine m ef dataset quant search_index \n", + "vectorsets 64 512 random-100 q8 3 {'ef': 512} 0.002255 \n", + " 1 {'ef': 128} 0.002358 \n", + " 2 {'ef': 256} 0.002585 \n", + " 0 {'ef': 64} 0.003914 \n", + " 4 {'ef': 64} 2.029471 \n", + " 6 {'ef': 256} 2.790438 \n", + " 7 {'ef': 512} 3.443149 \n", + " 5 {'ef': 128} 4.754267 \n", + "\n", + " mean_time mean_precisions \\\n", + "engine m ef dataset quant search_index \n", + "vectorsets 64 512 random-100 q8 3 0.000183 1.0 \n", + " 1 0.000176 1.0 \n", + " 2 0.000209 1.0 \n", + " 0 0.000268 1.0 \n", + " 4 0.031960 1.0 \n", + " 6 0.000988 1.0 \n", + " 7 0.031880 1.0 \n", + " 5 0.001945 1.0 \n", + "\n", + " std_time min_time max_time \\\n", + "engine m ef dataset quant search_index \n", + "vectorsets 64 512 random-100 q8 3 0.000139 0.000105 0.000521 \n", + " 1 0.000125 0.000114 0.000544 \n", + " 2 0.000125 0.000121 0.000507 \n", + " 0 0.000369 0.000110 0.001371 \n", + " 4 0.084399 0.000275 0.283514 \n", + " 6 0.001864 0.000257 0.006563 \n", + " 7 0.069497 0.000366 0.238009 \n", + " 5 0.003709 0.000413 0.013049 \n", + "\n", + " rps p95_time \\\n", + "engine m ef dataset quant search_index \n", + "vectorsets 64 512 random-100 q8 3 4433.769899 0.000460 \n", + " 1 4240.882160 0.000388 \n", + " 2 3869.033259 0.000458 \n", + " 0 2554.713659 0.000841 \n", + " 4 4.927392 0.170620 \n", + " 6 3.583666 0.003953 \n", + " 7 2.904318 0.145535 \n", + " 5 2.103373 0.007699 \n", + "\n", + " p99_time \n", + "engine m ef dataset quant search_index \n", + "vectorsets 64 512 random-100 q8 3 0.000508 \n", + " 1 0.000513 \n", + " 2 0.000497 \n", + " 0 0.001265 \n", + " 4 0.260935 \n", + " 6 0.006041 \n", + " 7 0.219514 \n", + " 5 0.011979 " + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "search_df = pd.DataFrame(search_results, columns=column_names)\n", + "search_df[\"date\"] = pd.to_datetime(search_df[\"date\"], format=\"%Y-%m-%d-%H-%M-%S\")\n", + "search_df = search_df.sort_values(\"date\", ascending=False) \\\n", + " .groupby([\"engine\", \"m\", \"ef\", \"dataset\", \"quant\", \"search_index\"]) \\\n", + " .first()\n", + "\n", + "print(len(search_df))\n", + "\n", + "for column_name in [\"params\", \"results\"]:\n", + " search_df = pd.concat([search_df, search_df[column_name].apply(pd.Series)], axis=1)\n", + " search_df = search_df.drop(columns=column_name)\n", + "search_df.sort_values(\"rps\", ascending=False).head(n=10)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "ename": "ValueError", + "evalue": "cannot insert dataset, already exists", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m/var/folders/4t/wcf5b_sj55lbww_8xxhhq8pr0000gp/T/ipykernel_45869/1649479656.py\u001b[0m in \u001b[0;36m?\u001b[0;34m()\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[0;31m# print(len(joined_df))\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 18\u001b[0m \u001b[0;31m# joined_df\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 19\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 20\u001b[0m \u001b[0;31m# Reset the indices of both dataframes to make columns\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 21\u001b[0;31m \u001b[0msearch_reset\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msearch_df\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mreset_index\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 22\u001b[0m \u001b[0mupload_reset\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mupload_df\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mreset_index\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 23\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 24\u001b[0m \u001b[0;31m# Join on common columns\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/Code/vector-db-benchmark/.venv/lib/python3.11/site-packages/pandas/core/frame.py\u001b[0m in \u001b[0;36m?\u001b[0;34m(self, level, drop, inplace, col_level, col_fill, allow_duplicates, names)\u001b[0m\n\u001b[1;32m 6205\u001b[0m level_values = algorithms.take(\n\u001b[1;32m 6206\u001b[0m \u001b[0mlevel_values\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mlab\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mallow_fill\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfill_value\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mlev\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_na_value\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6207\u001b[0m )\n\u001b[1;32m 6208\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 6209\u001b[0;31m new_obj.insert(\n\u001b[0m\u001b[1;32m 6210\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6211\u001b[0m \u001b[0mname\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6212\u001b[0m \u001b[0mlevel_values\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/Code/vector-db-benchmark/.venv/lib/python3.11/site-packages/pandas/core/frame.py\u001b[0m in \u001b[0;36m?\u001b[0;34m(self, loc, column, value, allow_duplicates)\u001b[0m\n\u001b[1;32m 4768\u001b[0m \u001b[0;34m\"'self.flags.allows_duplicate_labels' is False.\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4769\u001b[0m )\n\u001b[1;32m 4770\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mallow_duplicates\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0mcolumn\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcolumns\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4771\u001b[0m \u001b[0;31m# Should this be a different kind of error??\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 4772\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34mf\"cannot insert {column}, already exists\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 4773\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mloc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mint\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4774\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mTypeError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"loc must be int\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4775\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mValueError\u001b[0m: cannot insert dataset, already exists" + ] + } + ], + "source": [ + "# # Option 1: Check what's in your index and columns\n", + "# print(\"search_df index name:\", search_df.index.name or search_df.index.names)\n", + "# print(\"search_df columns:\", search_df.columns.tolist())\n", + "# print(\"upload_df index name:\", upload_df.index.name or upload_df.index.names)\n", + "# print(\"upload_df columns:\", upload_df.columns.tolist())\n", + "\n", + "# # Option 2: Reset index but specify a different name for the index column\n", + "# _search = search_df.reset_index()\n", + "# _upload = upload_df.reset_index()\n", + "# print(\"search_df index name:\", _search.index.name or _search.index.names)\n", + "# print(\"search_df columns:\", _search.columns.tolist())\n", + "\n", + "# print(\"_upload index name:\", _upload.index.name or _upload.index.names)\n", + "# print(\"_upload columns:\", _upload.columns.tolist())\n", + "\n", + "# joined_df = _search.merge(_upload, how=\"left\", on=[\"engine\", \"m\", \"ef\", \"quant\", \"dataset\"], suffixes=(\"_search\", \"_upload\"))\n", + "# print(len(joined_df))\n", + "# joined_df\n", + "\n", + "# Reset the indices of both dataframes to make columns\n", + "search_reset = search_df.reset_index()\n", + "upload_reset = upload_df.reset_index()\n", + "\n", + "# Join on common columns\n", + "joined_df = search_reset.merge(upload_reset, \n", + " how=\"left\", \n", + " on=[\"engine\", \"m\", \"ef\", \"quant\", \"dataset\"],\n", + " suffixes=(\"\", \"_upload\"))\n", + "\n", + "# Rename any conflicting columns to match what's expected in cell 10\n", + "joined_df = joined_df.rename(columns={\n", + " \"total_time\": \"total_time_search\",\n", + " \"total_time_upload\": \"total_time_upload\"\n", + "})\n", + "\n", + "print(f\"Joined dataframe has {len(joined_df)} rows\")\n", + "joined_df.head(2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "ename": "KeyError", + "evalue": "'upload_time'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", + "File \u001b[0;32m~/Code/vector-db-benchmark/.venv/lib/python3.11/site-packages/pandas/core/indexes/base.py:3653\u001b[0m, in \u001b[0;36mIndex.get_loc\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 3652\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m-> 3653\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_engine\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_loc\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcasted_key\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 3654\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m err:\n", + "File \u001b[0;32m~/Code/vector-db-benchmark/.venv/lib/python3.11/site-packages/pandas/_libs/index.pyx:147\u001b[0m, in \u001b[0;36mpandas._libs.index.IndexEngine.get_loc\u001b[0;34m()\u001b[0m\n", + "File \u001b[0;32m~/Code/vector-db-benchmark/.venv/lib/python3.11/site-packages/pandas/_libs/index.pyx:176\u001b[0m, in \u001b[0;36mpandas._libs.index.IndexEngine.get_loc\u001b[0;34m()\u001b[0m\n", + "File \u001b[0;32mpandas/_libs/hashtable_class_helper.pxi:7080\u001b[0m, in \u001b[0;36mpandas._libs.hashtable.PyObjectHashTable.get_item\u001b[0;34m()\u001b[0m\n", + "File \u001b[0;32mpandas/_libs/hashtable_class_helper.pxi:7088\u001b[0m, in \u001b[0;36mpandas._libs.hashtable.PyObjectHashTable.get_item\u001b[0;34m()\u001b[0m\n", + "\u001b[0;31mKeyError\u001b[0m: 'upload_time'", + "\nThe above exception was the direct cause of the following exception:\n", + "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[12], line 21\u001b[0m\n\u001b[1;32m 13\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m engine_name \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mqdrant-rps\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;129;01mor\u001b[39;00m engine_name \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mqdrant-bq-rps\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;129;01mor\u001b[39;00m engine_name \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mqdrant-sq-rps\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[1;32m 14\u001b[0m engine_name \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mqdrant\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 16\u001b[0m json_object \u001b[38;5;241m=\u001b[39m {\n\u001b[1;32m 17\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mengine_name\u001b[39m\u001b[38;5;124m\"\u001b[39m: engine_name,\n\u001b[1;32m 18\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124msetup_name\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mrow[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mengine\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m-m-\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mrow[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mm\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m-ef-\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mrow[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mef\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m-quant-\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mrow[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mquant\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 19\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mdataset_name\u001b[39m\u001b[38;5;124m\"\u001b[39m: row[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mdataset\u001b[39m\u001b[38;5;124m'\u001b[39m],\n\u001b[1;32m 20\u001b[0m \u001b[38;5;66;03m# \"search_idx\": row['search_index'],\u001b[39;00m\n\u001b[0;32m---> 21\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mupload_time\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[43mrow\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mupload_time\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m]\u001b[49m,\n\u001b[1;32m 22\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtotal_upload_time\u001b[39m\u001b[38;5;124m\"\u001b[39m: row[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mtotal_time_upload\u001b[39m\u001b[38;5;124m'\u001b[39m],\n\u001b[1;32m 23\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mp95_time\u001b[39m\u001b[38;5;124m\"\u001b[39m: row[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mp95_time\u001b[39m\u001b[38;5;124m'\u001b[39m],\n\u001b[1;32m 24\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrps\u001b[39m\u001b[38;5;124m\"\u001b[39m: row[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mrps\u001b[39m\u001b[38;5;124m'\u001b[39m],\n\u001b[1;32m 25\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mparallel\u001b[39m\u001b[38;5;124m\"\u001b[39m: row[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mparallel\u001b[39m\u001b[38;5;124m'\u001b[39m],\n\u001b[1;32m 26\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mp99_time\u001b[39m\u001b[38;5;124m\"\u001b[39m: row[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mp99_time\u001b[39m\u001b[38;5;124m'\u001b[39m],\n\u001b[1;32m 27\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmean_time\u001b[39m\u001b[38;5;124m\"\u001b[39m: row[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mmean_time\u001b[39m\u001b[38;5;124m'\u001b[39m],\n\u001b[1;32m 28\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmean_precisions\u001b[39m\u001b[38;5;124m\"\u001b[39m: row[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mmean_precisions\u001b[39m\u001b[38;5;124m'\u001b[39m],\n\u001b[1;32m 29\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mengine_params\u001b[39m\u001b[38;5;124m\"\u001b[39m: engine_params,\n\u001b[1;32m 30\u001b[0m }\n\u001b[1;32m 31\u001b[0m json_all\u001b[38;5;241m.\u001b[39mappend(json_object)\n\u001b[1;32m 33\u001b[0m parallel \u001b[38;5;241m=\u001b[39m row[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mparallel\u001b[39m\u001b[38;5;124m'\u001b[39m]\n", + "File \u001b[0;32m~/Code/vector-db-benchmark/.venv/lib/python3.11/site-packages/pandas/core/series.py:1007\u001b[0m, in \u001b[0;36mSeries.__getitem__\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 1004\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_values[key]\n\u001b[1;32m 1006\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m key_is_scalar:\n\u001b[0;32m-> 1007\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_get_value\u001b[49m\u001b[43m(\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1009\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m is_hashable(key):\n\u001b[1;32m 1010\u001b[0m \u001b[38;5;66;03m# Otherwise index.get_value will raise InvalidIndexError\u001b[39;00m\n\u001b[1;32m 1011\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 1012\u001b[0m \u001b[38;5;66;03m# For labels that don't resolve as scalars like tuples and frozensets\u001b[39;00m\n", + "File \u001b[0;32m~/Code/vector-db-benchmark/.venv/lib/python3.11/site-packages/pandas/core/series.py:1116\u001b[0m, in \u001b[0;36mSeries._get_value\u001b[0;34m(self, label, takeable)\u001b[0m\n\u001b[1;32m 1113\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_values[label]\n\u001b[1;32m 1115\u001b[0m \u001b[38;5;66;03m# Similar to Index.get_value, but we do not fall back to positional\u001b[39;00m\n\u001b[0;32m-> 1116\u001b[0m loc \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mindex\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_loc\u001b[49m\u001b[43m(\u001b[49m\u001b[43mlabel\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1118\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m is_integer(loc):\n\u001b[1;32m 1119\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_values[loc]\n", + "File \u001b[0;32m~/Code/vector-db-benchmark/.venv/lib/python3.11/site-packages/pandas/core/indexes/base.py:3655\u001b[0m, in \u001b[0;36mIndex.get_loc\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 3653\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_engine\u001b[38;5;241m.\u001b[39mget_loc(casted_key)\n\u001b[1;32m 3654\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m err:\n\u001b[0;32m-> 3655\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m(key) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01merr\u001b[39;00m\n\u001b[1;32m 3656\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m:\n\u001b[1;32m 3657\u001b[0m \u001b[38;5;66;03m# If we have a listlike key, _check_indexing_error will raise\u001b[39;00m\n\u001b[1;32m 3658\u001b[0m \u001b[38;5;66;03m# InvalidIndexError. Otherwise we fall through and re-raise\u001b[39;00m\n\u001b[1;32m 3659\u001b[0m \u001b[38;5;66;03m# the TypeError.\u001b[39;00m\n\u001b[1;32m 3660\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_check_indexing_error(key)\n", + "\u001b[0;31mKeyError\u001b[0m: 'upload_time'" + ] + } + ], + "source": [ + "json_all = []\n", + "json_1_or_100_thread = []\n", + "\n", + "for index, row in joined_df.reset_index().iterrows():\n", + " engine_params = {}\n", + " if isinstance(row['search_params'], dict):\n", + " engine_params.update(row['search_params'])\n", + " if isinstance(row['params'], dict):\n", + " engine_params.update(row['params'])\n", + "\n", + " engine_name = row['engine']\n", + "\n", + " if engine_name == \"qdrant-rps\" or engine_name == \"qdrant-bq-rps\" or engine_name == \"qdrant-sq-rps\":\n", + " engine_name = \"qdrant\"\n", + "\n", + " json_object = {\n", + " \"engine_name\": engine_name,\n", + " \"setup_name\": f\"{row['engine']}-m-{row['m']}-ef-{row['ef']}\",\n", + " \"dataset_name\": row['dataset'],\n", + " # \"search_idx\": row['search_index'],\n", + " \"upload_time\": row['upload_time'],\n", + " \"total_upload_time\": row['total_time_upload'],\n", + " \"p95_time\": row['p95_time'],\n", + " \"rps\": row['rps'],\n", + " \"parallel\": row['parallel'],\n", + " \"p99_time\": row['p99_time'],\n", + " \"mean_time\": row['mean_time'],\n", + " \"mean_precisions\": row['mean_precisions'],\n", + " \"engine_params\": engine_params,\n", + " }\n", + " json_all.append(json_object)\n", + " \n", + " parallel = row['parallel']\n", + "\n", + " if parallel == 1 or parallel == 100:\n", + " json_1_or_100_thread.append(json_object)\n", + "\n", + "format = '%Y-%M-%d' # T%H:%M:%S\n", + "now = datetime.now().replace(tzinfo=timezone.utc).strftime(format)\n", + "\n", + "Path(f\"results-{now}.json\").write_text(json.dumps(json_all, indent=2))\n", + "Path(f\"results-1-100-threads-{now}.json\").write_text(json.dumps(json_1_or_100_thread, indent=2))\n", + "\n", + "json_1_or_100_thread[-1], len(json_all), len(json_1_or_100_thread)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/scripts/update_datasets.py b/02-semantic-search-rag/fcenedes/vecdb_bench/scripts/update_datasets.py new file mode 100644 index 0000000..cbfc4aa --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/scripts/update_datasets.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +""" +Script to add missing vector_count and description fields to datasets.json +""" + +import json +import re + +def estimate_vector_count(name): + """Estimate vector count from dataset name patterns""" + name_lower = name.lower() + + # Direct patterns + if '1b' in name_lower or '1billion' in name_lower or '1g' in name_lower: + return 1000000000 + elif '400m' in name_lower: + return 400000000 + elif '200m' in name_lower: + return 200000000 + elif '100m' in name_lower: + return 100000000 + elif '40m' in name_lower: + return 40000000 + elif '20m' in name_lower: + return 20000000 + elif '10m' in name_lower: + return 10000000 + elif '1m' in name_lower: + return 1000000 + elif '100k' in name_lower: + return 100000 + elif '10k' in name_lower: + return 10000 + elif '1k' in name_lower: + return 1000 + elif 'random-100' in name_lower: + return 100 + + # Special cases + if 'glove' in name_lower: + return 1183514 # Standard GloVe size + elif 'deep-image' in name_lower: + return 9990000 # Standard deep image size + elif 'gist' in name_lower: + return 1000000 # Standard GIST size + elif 'yandex' in name_lower and '100k' in name_lower: + return 100000 + elif 'dbpedia' in name_lower: + return 1000000 + elif 'h-and-m' in name_lower: + return 105542 + elif 'arxiv' in name_lower: + return 2205995 + elif 'laion-small-clip' in name_lower: + return 100000 + elif 'random-match' in name_lower or 'random-range' in name_lower or 'random-geo' in name_lower: + if '2048' in name_lower: + return 100000 # 2048D synthetic datasets + else: + return 1000000 # 100D synthetic datasets + elif 'random-100-match' in name_lower: + return 100 # Small vocab datasets + + # Default for unknown patterns + return None + +def generate_description(name): + """Generate description from dataset name patterns""" + name_lower = name.lower() + + if 'laion' in name_lower: + return 'Image embeddings' + elif 'glove' in name_lower: + return 'Word vectors' + elif 'deep-image' in name_lower: + return 'CNN image features' + elif 'gist' in name_lower: + return 'Image descriptors' + elif 'dbpedia' in name_lower: + return 'Knowledge embeddings' + elif 'yandex' in name_lower: + return 'Text-to-image embeddings' + elif 'arxiv' in name_lower: + return 'Academic paper embeddings' + elif 'h-and-m' in name_lower: + return 'Fashion product embeddings' + elif 'random' in name_lower: + if 'match' in name_lower and 'keyword' in name_lower: + return 'Synthetic keyword matching' + elif 'match' in name_lower and 'int' in name_lower: + return 'Synthetic integer matching' + elif 'range' in name_lower: + return 'Synthetic range queries' + elif 'geo' in name_lower: + return 'Synthetic geo queries' + else: + return 'Synthetic data' + else: + return None + +def main(): + # Read the datasets.json file + with open('datasets/datasets.json', 'r') as f: + datasets = json.load(f) + + updated_count = 0 + + for dataset in datasets: + updated = False + + # Add vector_count if missing + if 'vector_count' not in dataset: + vector_count = estimate_vector_count(dataset['name']) + dataset['vector_count'] = vector_count + updated = True + print(f"Added vector_count {vector_count} to {dataset['name']}") + + # Add description if missing + if 'description' not in dataset: + description = generate_description(dataset['name']) + dataset['description'] = description + updated = True + print(f"Added description '{description}' to {dataset['name']}") + + if updated: + updated_count += 1 + + # Write back the updated datasets.json + with open('datasets/datasets.json', 'w') as f: + json.dump(datasets, f, indent=2) + + print(f"\nUpdated {updated_count} datasets") + print("datasets.json has been updated with missing vector_count and description fields") + +if __name__ == "__main__": + main() diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/scripts/validate_datasets.py b/02-semantic-search-rag/fcenedes/vecdb_bench/scripts/validate_datasets.py new file mode 100644 index 0000000..c252e43 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/scripts/validate_datasets.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +""" +Dataset validation script for vector-db-benchmark + +This script validates the datasets.json file to ensure: +1. All required fields are present +2. Field types are correct +3. Values are reasonable +4. Dataset names are unique +5. The --describe functionality works + +Usage: + python validate_datasets.py + python validate_datasets.py --strict # Exit on warnings too +""" + +import json +import sys +import argparse +from pathlib import Path +from typing import List, Tuple, Any + + +def load_datasets() -> List[dict]: + """Load and parse datasets.json file.""" + datasets_file = Path('datasets/datasets.json') + if not datasets_file.exists(): + print('❌ datasets/datasets.json not found') + sys.exit(1) + + try: + with open(datasets_file, 'r') as f: + datasets = json.load(f) + except json.JSONDecodeError as e: + print(f'❌ Invalid JSON in datasets.json: {e}') + sys.exit(1) + + if not isinstance(datasets, list): + print('❌ datasets.json must contain a list of datasets') + sys.exit(1) + + return datasets + + +def validate_dataset_structure(datasets: List[dict]) -> Tuple[List[str], List[str]]: + """Validate dataset structure and required fields.""" + required_fields = { + 'name': str, + 'vector_size': int, + 'distance': str, + 'type': str, + 'path': (str, dict), # Can be string or dict for h5-multi type + 'vector_count': (int, type(None)), + 'description': (str, type(None)) + } + + valid_distances = ['cosine', 'l2', 'dot', 'euclidean'] + valid_types = ['h5', 'tar', 'jsonl', 'h5-multi'] + + errors = [] + warnings = [] + + for i, dataset in enumerate(datasets): + dataset_name = dataset.get('name', f'Dataset #{i+1}') + + # Check required fields + for field, expected_type in required_fields.items(): + if field not in dataset: + errors.append(f'❌ {dataset_name}: Missing required field "{field}"') + continue + + value = dataset[field] + + # Handle tuple types (multiple allowed types) + if isinstance(expected_type, tuple): + if not isinstance(value, expected_type): + type_names = [t.__name__ if t != type(None) else 'null' for t in expected_type] + errors.append(f'❌ {dataset_name}: Field "{field}" must be {" or ".join(type_names)}, got {type(value).__name__}') + elif not isinstance(value, expected_type): + errors.append(f'❌ {dataset_name}: Field "{field}" must be {expected_type.__name__}, got {type(value).__name__}') + + # Validate specific field values + distance = dataset.get('distance') + if distance and distance not in valid_distances: + errors.append(f'❌ {dataset_name}: Invalid distance "{distance}", must be one of {valid_distances}') + + data_type = dataset.get('type') + if data_type and data_type not in valid_types: + warnings.append(f'⚠️ {dataset_name}: Unusual type "{data_type}", expected one of {valid_types}') + + # Validate numeric values + vector_size = dataset.get('vector_size') + if isinstance(vector_size, int): + if vector_size <= 0: + errors.append(f'❌ {dataset_name}: vector_size must be positive, got {vector_size}') + elif vector_size > 4096: + warnings.append(f'⚠️ {dataset_name}: Very large vector_size ({vector_size}) - verify this is correct') + + vector_count = dataset.get('vector_count') + if isinstance(vector_count, int): + if vector_count <= 0: + errors.append(f'❌ {dataset_name}: vector_count must be positive, got {vector_count}') + elif vector_count >= 1000000 and vector_count % 1000000 == 0: + warnings.append(f'⚠️ {dataset_name}: vector_count {vector_count} looks like an estimate (very round number)') + elif vector_count is None: + warnings.append(f'⚠️ {dataset_name}: vector_count is None - consider adding actual count') + + # Check for missing descriptions + if dataset.get('description') is None: + warnings.append(f'⚠️ {dataset_name}: description is None - consider adding description') + + # Check for missing links on downloadable datasets + if not dataset.get('link') and data_type in ['h5', 'tar', 'h5-multi']: + warnings.append(f'⚠️ {dataset_name}: No download link provided for {data_type} dataset') + + return errors, warnings + + +def validate_unique_names(datasets: List[dict]) -> List[str]: + """Check that all dataset names are unique.""" + names = [d.get('name') for d in datasets if d.get('name')] + duplicates = [name for name in set(names) if names.count(name) > 1] + + errors = [] + if duplicates: + errors.append(f'❌ Found duplicate dataset names: {duplicates}') + + return errors + + +def test_describe_functionality() -> List[str]: + """Test that the --describe functionality works.""" + import subprocess + + errors = [] + + try: + # Test describe datasets + print("🔍 Testing --describe datasets functionality...") + result = subprocess.run( + [sys.executable, 'run.py', '--describe', 'datasets'], + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode != 0: + errors.append(f'❌ --describe datasets failed with exit code {result.returncode}') + if result.stderr: + errors.append(f' Error: {result.stderr.strip()}') + elif not result.stdout or 'Available Datasets' not in result.stdout: + errors.append('❌ --describe datasets output missing expected content') + else: + print("✅ --describe datasets works correctly") + + # Test describe engines + print("🔍 Testing --describe engines functionality...") + result = subprocess.run( + [sys.executable, 'run.py', '--describe', 'engines'], + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode != 0: + errors.append(f'❌ --describe engines failed with exit code {result.returncode}') + if result.stderr: + errors.append(f' Error: {result.stderr.strip()}') + elif not result.stdout or 'Available Engines' not in result.stdout: + errors.append('❌ --describe engines output missing expected content') + else: + print("✅ --describe engines works correctly") + + except subprocess.TimeoutExpired: + errors.append('❌ --describe command timed out') + except Exception as e: + errors.append(f'❌ Error testing --describe functionality: {e}') + + return errors + + +def main(): + parser = argparse.ArgumentParser(description='Validate datasets.json file') + parser.add_argument('--strict', action='store_true', + help='Exit with error code on warnings too') + args = parser.parse_args() + + print("🔍 Validating datasets.json...") + + # Load datasets + datasets = load_datasets() + print(f"✅ Loaded {len(datasets)} datasets from datasets.json") + + # Validate structure + structure_errors, structure_warnings = validate_dataset_structure(datasets) + + # Validate unique names + name_errors = validate_unique_names(datasets) + + # Test describe functionality + describe_errors = test_describe_functionality() + + # Combine all errors and warnings + all_errors = structure_errors + name_errors + describe_errors + all_warnings = structure_warnings + + # Print results + if all_warnings: + print("\n⚠️ Warnings:") + for warning in all_warnings: + print(f" {warning}") + + if all_errors: + print("\n❌ Errors:") + for error in all_errors: + print(f" {error}") + print(f"\n❌ Validation failed with {len(all_errors)} errors") + sys.exit(1) + else: + print(f"\n✅ All validations passed!") + if all_warnings: + print(f"⚠️ Found {len(all_warnings)} warnings (non-blocking)") + if args.strict: + print("❌ Strict mode: treating warnings as errors") + sys.exit(1) + + print(f"📊 Summary: {len(datasets)} datasets validated successfully") + + +if __name__ == "__main__": + main() diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/sync_results.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/sync_results.sh new file mode 100644 index 0000000..7f2bc05 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/sync_results.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + + +rsync -avP \ + $1:./projects/vector-db-benchmark/results/ \ + ./results/ \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/test_multiprocessing.py b/02-semantic-search-rag/fcenedes/vecdb_bench/test_multiprocessing.py new file mode 100644 index 0000000..341289e --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/test_multiprocessing.py @@ -0,0 +1,36 @@ +from engine.base_client.search import BaseSearcher +from dataset_reader.base_reader import Query +import time + +class TestSearcher(BaseSearcher): + @classmethod + def init_client(cls, host, distance, connection_params, search_params): + pass + + @classmethod + def search_one(cls, vector, meta_conditions, top): + return [] + + @classmethod + def _search_one(cls, query, top=None): + # Add a small delay to simulate real work + time.sleep(0.001) + return 1.0, 0.1 + + def setup_search(self): + pass + +# Create a small set of test queries +queries = [Query(vector=[0.1]*10, meta_conditions=None, expected_result=None) for _ in range(10)] + +# Create a searcher with parallel=10 +searcher = TestSearcher('localhost', {}, {'parallel': 10}) + +# Run the search_all method with a large num_queries parameter +start = time.perf_counter() +results = searcher.search_all('cosine', queries, num_queries=1000) +total_time = time.perf_counter() - start + +print(f'Number of queries: {len(results["latencies"])}') +print(f'Total time: {total_time:.6f} seconds') +print(f'Throughput: {results["rps"]:.2f} queries/sec') diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tests/engine/clients/qdrant/test_qdrant_parser.py b/02-semantic-search-rag/fcenedes/vecdb_bench/tests/engine/clients/qdrant/test_qdrant_parser.py new file mode 100644 index 0000000..5501613 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tests/engine/clients/qdrant/test_qdrant_parser.py @@ -0,0 +1,99 @@ +import pytest +from qdrant_client.http import models as rest + +from engine.clients.qdrant.parser import QdrantConditionParser + + +@pytest.fixture +def qdrant_condition_parser(): + return QdrantConditionParser() + + +def test_parse_returns_none_on_none(qdrant_condition_parser): + qdrant_filter = qdrant_condition_parser.parse(None) + assert qdrant_filter is None + + +def test_parse_returns_none_on_empty(qdrant_condition_parser): + conditions = {} + qdrant_filter = qdrant_condition_parser.parse(conditions) + assert qdrant_filter is None + + +def test_parse_converts_exact_match(qdrant_condition_parser): + conditions = {"and": [{"product_group_name": {"match": {"value": "Shoes"}}}]} + qdrant_filter = qdrant_condition_parser.parse(conditions) + + assert qdrant_filter is not None + assert qdrant_filter.should is None + assert qdrant_filter.must is not None + assert qdrant_filter.must_not is None + assert 1 == len(qdrant_filter.must) + condition = qdrant_filter.must[0] + assert isinstance(condition, rest.FieldCondition) + assert ( + rest.FieldCondition( + key="product_group_name", match=rest.MatchValue(value="Shoes") + ) + == condition + ) + + +def test_parse_converts_multiple_or_statements(qdrant_condition_parser): + conditions = { + "or": [{"a": {"match": {"value": 80}}}, {"a": {"match": {"value": 2}}}] + } + qdrant_filter = qdrant_condition_parser.parse(conditions) + + assert qdrant_filter is not None + assert qdrant_filter.should is not None + assert qdrant_filter.must is None + assert qdrant_filter.must_not is None + assert 2 == len(qdrant_filter.should) + first_condition, second_condition = qdrant_filter.should + assert isinstance(first_condition, rest.FieldCondition) + assert ( + rest.FieldCondition(key="a", match=rest.MatchValue(value=80)) == first_condition + ) + assert ( + rest.FieldCondition(key="a", match=rest.MatchValue(value=2)) == second_condition + ) + + +def test_parse_converts_geo_statement(qdrant_condition_parser): + conditions = { + "and": [ + { + "a": { + "geo": { + "lon": 116.0, + "lat": -52.0, + "radius": 326341, + } + } + } + ] + } + qdrant_filter = qdrant_condition_parser.parse(conditions) + + assert qdrant_filter is not None + assert qdrant_filter.should is None + assert qdrant_filter.must is not None + assert qdrant_filter.must_not is None + assert 1 == len(qdrant_filter.must) + assert 1 == len(qdrant_filter.must) + condition = qdrant_filter.must[0] + assert isinstance(condition, rest.FieldCondition) + assert ( + rest.FieldCondition( + key="a", + geo_radius=rest.GeoRadius( + center=rest.GeoPoint( + lon=116.0, + lat=-52.0, + ), + radius=326341, + ), + ) + == condition + ) diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tests/engine/clients/redis/test_redis_parser.py b/02-semantic-search-rag/fcenedes/vecdb_bench/tests/engine/clients/redis/test_redis_parser.py new file mode 100644 index 0000000..1ddc4d5 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tests/engine/clients/redis/test_redis_parser.py @@ -0,0 +1,80 @@ +import pytest + +from engine.clients.redis.parser import RedisConditionParser + + +@pytest.fixture +def redis_condition_parser(): + return RedisConditionParser() + + +def test_parse_returns_none_on_none(redis_condition_parser): + redis_filter = redis_condition_parser.parse(None) + assert redis_filter is None + + +def test_parse_returns_none_on_empty(redis_condition_parser): + conditions = {} + redis_filter = redis_condition_parser.parse(conditions) + assert redis_filter is None + + +def test_parse_converts_exact_match(redis_condition_parser): + conditions = {"and": [{"product_group_name": {"match": {"value": "Shoes"}}}]} + redis_filter, params = redis_condition_parser.parse(conditions) + + assert '(@product_group_name:"$product_group_name_0")' == redis_filter + assert "Shoes" == params.get("product_group_name_0") + + +def test_parse_converts_multiple_or_statements(redis_condition_parser): + conditions = { + "or": [{"a": {"match": {"value": 80}}}, {"a": {"match": {"value": 2}}}] + } + redis_filter, params = redis_condition_parser.parse(conditions) + + assert "(@a:[$a_0 $a_0] | @a:[$a_1 $a_1])" == redis_filter + assert 80 in params.values() + assert 2 in params.values() + + +def test_parse_converts_range_statement(redis_condition_parser): + conditions = { + "and": [ + { + "a": { + "range": { + "lt": 10, + "gt": -5, + } + } + } + ] + } + redis_filter, params = redis_condition_parser.parse(conditions) + + assert "(@a:[-inf ($a_0_lt] @a:[($a_0_gt +inf])" == redis_filter + assert 10 == params.get("a_0_lt") + assert -5 == params.get("a_0_gt") + + +def test_parse_converts_geo_statement(redis_condition_parser): + conditions = { + "and": [ + { + "a": { + "geo": { + "lon": 116.0, + "lat": -52.0, + "radius": 326341, + } + } + } + ] + } + redis_filter, params = redis_condition_parser.parse(conditions) + + assert "(@a:[$a_0_lon $a_0_lat $a_0_radius m])" == redis_filter + assert 116.0 == params.get("a_0_lon") + assert -52.0 == params.get("a_0_lat") + assert 326341 == params.get("a_0_radius") diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/custom/create_and_install.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/custom/create_and_install.sh new file mode 100755 index 0000000..b20bfc9 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/custom/create_and_install.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +set -e + +# path relative to the script + +SCRIPT=$(realpath "$0") +SCRIPTPATH=$(dirname "$SCRIPT") + + +# Create server in custom Cloud + +SERVER_NAME=${SERVER_NAME:-test-server-1} + + +SERVER_IP=$(jq ".[\"${SERVER_NAME}\"].public_ip" -r $SCRIPTPATH/data.json) + +SSH_USER=$(jq ".[\"${SERVER_NAME}\"].user" -r $SCRIPTPATH/data.json) + +echo "Server IP: ${SERVER_IP}" + +ssh-keygen -f "$HOME/.ssh/known_hosts" -R "${SERVER_IP}" || true + +# Wait for server to be ready + +while ! ssh -oStrictHostKeyChecking=no ${SSH_USER}@${SERVER_IP} echo "Server is ready"; do + sleep 1 +done + +# Create and install docker, poetry, etc + +cat "${SCRIPTPATH}/setup_vm.sh" | ssh "${SSH_USER}@${SERVER_IP}" sudo bash diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/custom/example.data.json b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/custom/example.data.json new file mode 100644 index 0000000..e6f8550 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/custom/example.data.json @@ -0,0 +1,12 @@ +{ + "benchmark-client-glove-100": { + "public_ip": "aa.bb.cc.dd", + "private_ip": "xx.y.z.a", + "user": "root" + }, + "benchmark-server-glove-100": { + "public_ip": "aa.bb.cc.de", + "private_ip": "xx.y.z.b", + "user": "root" + } +} diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/custom/get_private_ip.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/custom/get_private_ip.sh new file mode 100755 index 0000000..9ef92c1 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/custom/get_private_ip.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e +# Get ip of the private network interface of custom cloud server + +# Usage: ./get_private_ip.sh + +# Example: ./get_private_ip.sh benchmark-server-1 + + +SCRIPT=$(realpath "$0") +SCRIPTPATH=$(dirname "$SCRIPT") + +SERVER_IP=$(jq ".[\"${1}\"].private_ip" -r $SCRIPTPATH/data.json) + +echo "${SERVER_IP}" diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/custom/get_public_ip.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/custom/get_public_ip.sh new file mode 100755 index 0000000..0e707e1 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/custom/get_public_ip.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e +# Get ip of the private network interface of custom cloud server + +# Usage: ./get_private_ip.sh + +# Example: ./get_private_ip.sh benchmark-server-1 + + +SCRIPT=$(realpath "$0") +SCRIPTPATH=$(dirname "$SCRIPT") + +SERVER_IP=$(jq ".[\"${1}\"].public_ip" -r $SCRIPTPATH/data.json) + +echo "${SERVER_IP}" diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/custom/get_ssh_user.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/custom/get_ssh_user.sh new file mode 100644 index 0000000..a0114df --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/custom/get_ssh_user.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e +# Get ip of the private network interface of custom cloud server + +# Usage: ./get_private_ip.sh + +# Example: ./get_private_ip.sh benchmark-server-1 + + +SCRIPT=$(realpath "$0") +SCRIPTPATH=$(dirname "$SCRIPT") + +SSH_USER=$(jq ".[\"${1}\"].user" -r $SCRIPTPATH/data.json) + +echo "${SSH_USER}" diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/custom/setup_vm.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/custom/setup_vm.sh new file mode 100755 index 0000000..a0a660b --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/custom/setup_vm.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -e + +mkdir -p projects + +# Install docker + +apt-get update +apt-get install -y \ + ca-certificates \ + curl \ + gnupg \ + lsb-release \ + jq + +mkdir -p /etc/apt/keyrings + +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null + +apt-get update + +apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin + +docker run hello-world + +# Install poetry +apt install -y python3-pip +python3 -m pip install poetry +python3 -m poetry --version diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/export_old.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/export_old.sh new file mode 100644 index 0000000..030be6b --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/export_old.sh @@ -0,0 +1,28 @@ +#!/bin/bash + + + +# file name pattern: qdrant-continuous-benchmark-laion-small-clip-upload-2023-08-01-23-09-25.json + + +# read dates from file names, without hours and minutes +ls -1 results/qdrant/qdrant-continuous-benchmark-laion-small-clip-upload-* | sed -e 's/.*upload-\(.*\)\.json/\1/' | sed -e 's/\(.*\)-\(.*\)-\(.*\)-\(.*\)-\(.*\)-\(.*\)/\1-\2-\3/' | sort | uniq > dates.txt + + +# read dates and search for files with the same date + +while read -r date; do + echo "Processing date: $date" + SEARCH_FILE=$(ls -1 results/qdrant/*.json | grep "$date" | grep search | head -n 1) + UPLOAD_FILE=$(ls -1 results/qdrant/*.json | grep "$date" | grep upload | head -n 1) + + echo "Search file: $SEARCH_FILE" + echo "Upload file: $UPLOAD_FILE" + + SEARCH_RESULTS_FILE=$SEARCH_FILE \ + UPLOAD_RESULTS_FILE=$UPLOAD_FILE \ + MEMORY_USAGE_FILE=results/qdrant/mem-usage.txt \ + MEASURE_TIMESTAMP=$date \ + bash -x tools/upload_results_postgres.sh + +done < dates.txt \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/hetzner/check_ssh_connection.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/hetzner/check_ssh_connection.sh new file mode 100644 index 0000000..f7ba579 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/hetzner/check_ssh_connection.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +SERVER_NAME=${SERVER_NAME:-test-server-1} + +SERVER_IP=$(hcloud server ip "${SERVER_NAME}") + +echo "Server IP: ${SERVER_IP}" + +ssh-keygen -f "$HOME/.ssh/known_hosts" -R "${SERVER_IP}" || true + +ssh -oStrictHostKeyChecking=no root@${SERVER_IP} echo "Server is ready" diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/hetzner/create_and_install.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/hetzner/create_and_install.sh new file mode 100644 index 0000000..e27faa6 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/hetzner/create_and_install.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +set -e + +# path relative to the script + +SCRIPT=$(realpath "$0") +SCRIPTPATH=$(dirname "$SCRIPT") + + +# Create server in Hetzner Cloud + +SERVER_NAME=${SERVER_NAME:-test-server-1} + +SERVER_TYPE=${SERVER_TYPE:-cx41} + +SERVER_IMAGE=${SERVER_IMAGE:-ubuntu-22.04} + +SERVER_LOCATION=${SERVER_LOCATION:-nbg1} + +SERVER_SSH_KEY=${SERVER_SSH_KEY:-'benchmark@qdrant.tech'} + +SERVER_NETWORK=${SERVER_NETWORK:-'benchmarks'} + + +hcloud server create \ + --name "${SERVER_NAME}" \ + --type "${SERVER_TYPE}" \ + --image "${SERVER_IMAGE}" \ + --location "${SERVER_LOCATION}" \ + --ssh-key "${SERVER_SSH_KEY}" \ + --network "${SERVER_NETWORK}" + +# Get server IP +SERVER_IP=$(hcloud server ip "${SERVER_NAME}") + +echo "Server IP: ${SERVER_IP}" + +ssh-keygen -f "$HOME/.ssh/known_hosts" -R "${SERVER_IP}" || true + +# Wait for server to be ready + +while ! ssh -oStrictHostKeyChecking=no root@${SERVER_IP} echo "Server is ready"; do + sleep 1 +done + +# Create and install docker + +cat "${SCRIPTPATH}/setup_hetzner.sh" | ssh "root@${SERVER_IP}" bash diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/hetzner/get_private_ip.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/hetzner/get_private_ip.sh new file mode 100644 index 0000000..8e0012e --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/hetzner/get_private_ip.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e +# Get ip of the private network interface of Hetzner server +# Using `hcloud` CLI tool + +# Usage: ./get_private_ip.sh + +# Example: ./get_private_ip.sh benchmark-server-1 + +hcloud server describe "$1" -o json | jq -r '.private_net[0].ip' diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/hetzner/get_public_ip.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/hetzner/get_public_ip.sh new file mode 100644 index 0000000..5ac00b1 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/hetzner/get_public_ip.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +set -e +# Get public server IP by name + +hcloud server ip "$1" diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/hetzner/remove_server.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/hetzner/remove_server.sh new file mode 100644 index 0000000..57056a5 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/hetzner/remove_server.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +SERVER_NAME=${1:-} + +if [ -z "$SERVER_NAME" ]; then + echo "Server name is not provided" + exit 1 +fi + +hcloud server delete "$SERVER_NAME" diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/hetzner/setup_hetzner.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/hetzner/setup_hetzner.sh new file mode 100644 index 0000000..32e4a7c --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/hetzner/setup_hetzner.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +set -e + +mkdir projects + +# Install docker + +apt-get update +apt-get install -y \ + ca-certificates \ + curl \ + gnupg \ + lsb-release + +mkdir -p /etc/apt/keyrings + +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null + +apt-get update + +apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin + +docker run hello-world diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/qdrant_collect_stats.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/qdrant_collect_stats.sh new file mode 100644 index 0000000..495f33e --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/qdrant_collect_stats.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -e + +# Examples: qdrant-single-node, qdrant-single-node-rps +CONTAINER_NAME=$1 + +CLOUD_NAME=${CLOUD_NAME:-"hetzner"} +SERVER_USERNAME=${SERVER_USERNAME:-"root"} + + +SCRIPT=$(realpath "$0") +SCRIPT_PATH=$(dirname "$SCRIPT") + +BENCH_SERVER_NAME=${SERVER_NAME:-"benchmark-server-1"} + +IP_OF_THE_SERVER=$(bash "${SCRIPT_PATH}/${CLOUD_NAME}/get_public_ip.sh" "$BENCH_SERVER_NAME") + +MEMORY_USAGE=$(ssh -t "${SERVER_USERNAME}@${IP_OF_THE_SERVER}" "grep VmRSS /proc/\$(pidof qdrant)/status | awk '{print \$2}'") + +CURRENT_DATE=$(date +%Y-%m-%d-%H-%M-%S) + +echo "$MEMORY_USAGE" > results/memory-usage-"${CURRENT_DATE}".txt + +ROOT_API_RESPONSE=$(ssh -t "${SERVER_USERNAME}@${IP_OF_THE_SERVER}" "curl -s http://localhost:6333/") + +echo "$ROOT_API_RESPONSE" > results/root-api-"${CURRENT_DATE}".json diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/remote/setup_benchmark_client.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/remote/setup_benchmark_client.sh new file mode 100644 index 0000000..41c36f1 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/remote/setup_benchmark_client.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +set -e +set -x + +VECTOR_DB=${VECTOR_DB:-qdrant} +BRANCH=${BRANCH:-master} + +if [ -d "./vector-db-benchmark" ]; then + echo "vector-db-benchmark repo already exists" +else + git clone https://github.com/qdrant/vector-db-benchmark +fi + +cd vector-db-benchmark +git fetch && git checkout $BRANCH && git pull + +python3 -m poetry install + +# if using qdrant vector db +if [ "$VECTOR_DB" == "qdrant" ]; then + QDRANT_CONFIGS=$(cat experiments/configurations/qdrant-single-node{-bq-rps,-sq-rps,-rps,}.json | jq '.[] | .name' | grep -E 'qdrant(-(rps|bq-rps|sq-rps))?-m-.*-ef-.*' | sed 's/"//g') + + for QDRANT_CONFIG in $QDRANT_CONFIGS; do + # upload + python3 -m poetry run python run.py --engines "${QDRANT_CONFIG}" --datasets $DATASET --host $PRIVATE_SERVER_IP --skip-search >> ${VECTOR_DB}.log 2>&1 + + # now run search (retry on errors) + set +e + while true; do + python3 -m poetry run python run.py --engines "${QDRANT_CONFIG}" --datasets $DATASET --host $PRIVATE_SERVER_IP >> ${VECTOR_DB}.log --skip-upload 2>&1 + if [ $? -ne 0 ]; then + echo "retrying" | tee -a ${VECTOR_DB}.log + sleep 1 + else + echo "done" | tee -a ${VECTOR_DB}.log + break + fi + done + set -e + done +else + nohup python3 -m poetry run python run.py --engines "${VECTOR_DB}-m-*-ef-*" --datasets $DATASET --host $PRIVATE_SERVER_IP >> ${VECTOR_DB}.log 2>&1 & +fi + +PID_BENCHMARK=$! +echo $PID_BENCHMARK > benchmark.pid +wait $PID_BENCHMARK diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/remote/setup_benchmark_server.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/remote/setup_benchmark_server.sh new file mode 100644 index 0000000..291fcab --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/remote/setup_benchmark_server.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +set -e +set -x + +VECTOR_DB=${VECTOR_DB:-qdrant} +BRANCH=${BRANCH:-master} + +if [ -d "./vector-db-benchmark" ]; then + echo "vector-db-benchmark repo already exists" +else + git clone https://github.com/qdrant/vector-db-benchmark +fi + +cd vector-db-benchmark +git fetch && git checkout $BRANCH && git pull + +# remove all running containers: +RUNNING_CONTAINERS=$(docker ps -q) +if [ -n "$RUNNING_CONTAINERS" ]; then + docker container rm -f $RUNNING_CONTAINERS +fi + +if [ "$VECTOR_DB" == "qdrant" ] && [ "$QDRANT_VERSION" == "dev" ]; then + docker rmi qdrant/qdrant:dev || true +fi + +cd engine/servers/${VECTOR_DB}-single-node +docker compose up -d + +# if vector DB is milvus or elasticsearch, wait for them to be up +if [ "$VECTOR_DB" == "milvus" ] || [ "$VECTOR_DB" == "elasticsearch" ]; then + sleep 30 # Throws connection reset which isn't handled by --retry-connrefused in curl. So we need to wait +fi + +# Define a map for database types and their health check URLs +declare -A db_health_urls +db_health_urls["milvus"]="http://localhost:19530/v1/vector/collections" +db_health_urls["qdrant"]="http://localhost:6333" +db_health_urls["elasticsearch"]="http://localhost:9200/_cluster/health" + +# Check if the specified database type exists in the map +if [ -n "${db_health_urls[$VECTOR_DB]}" ]; then + url="${db_health_urls[$VECTOR_DB]}" + # Retry logic for the specified URL + curl --max-time 120 --retry-connrefused --retry 10 --retry-delay 10 "$url" +else + echo "Assuming engine $VECTOR_DB is already up" +fi diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_benchmarks.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_benchmarks.sh new file mode 100755 index 0000000..b071391 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_benchmarks.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# Usage: tools/run_benchmarks.sh "deep-image-96-angular" "new-benchmark-server" + +set -e +set -x + +SCRIPT_PATH="$( cd "$(dirname "$0")" &>/dev/null ; pwd -P )" +export CLOUD_NAME=${CLOUD_NAME:-"custom"} + +DATASETS=("glove-100-angular" "deep-image-96" "gist-960-euclidean" "dbpedia-openai-1M-1536-angular") +VECTOR_DBS=("qdrant" "milvus" "elasticsearch" "weaviate" "redis") +BRANCH="master" + +# Note: If you want to run with a different version of Qdrant. Set the QDRANT_VERSION env variable. +# export QDRANT_VERSION=dev + +# Run only while setting up new benchmark server and client: +# Create different servers and clients for each dataset so benchmarking can be done in parallel +# for dataset in "${DATASETS[@]}"; do +# SERVER_NAME=benchmark-client-${dataset} bash -x $SCRIPT_PATH/$CLOUD_NAME/create_and_install.sh +# SERVER_NAME=benchmark-server-${dataset} bash -x $SCRIPT_PATH/$CLOUD_NAME/create_and_install.sh +# done + +DATASET=$1 +SERVER_NAME=$2 + +# replace "server" with "client" if 3rd argument is not passed +CLIENT_NAME=${3:-"${SERVER_NAME/server/client}"} +PRIVATE_SERVER_IP=$(bash $SCRIPT_PATH/$CLOUD_NAME/get_private_ip.sh $SERVER_NAME) + +for VECTOR_DB in "${VECTOR_DBS[@]}"; do + echo Running benchmark for ${VECTOR_DB} on ${DATASET} + + RUN_SCRIPT="${SCRIPT_PATH}/remote/setup_benchmark_server.sh" \ + ENV_CONTEXT="${VECTOR_DB@A} ${BRANCH@A} ${QDRANT_VERSION@A}" \ + SERVER_NAME=${SERVER_NAME} \ + bash -x $SCRIPT_PATH/run_remote.sh + + RUN_SCRIPT="${SCRIPT_PATH}/remote/setup_benchmark_client.sh" \ + ENV_CONTEXT="${VECTOR_DB@A} ${BRANCH@A} ${PRIVATE_SERVER_IP@A} ${DATASET@A}" \ + SERVER_NAME=${CLIENT_NAME} \ + bash -x $SCRIPT_PATH/run_remote.sh +done diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_ci.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_ci.sh new file mode 100644 index 0000000..039a6fb --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_ci.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +set -e + +# Script, that runs benchmark within the GitHub Actions CI environment + +SCRIPT=$(realpath "$0") +SCRIPT_PATH=$(dirname "$SCRIPT") + +# Set up dependencies + +sudo apt update +sudo apt install -y jq + +# Download and install hcloud + +HCVERSION=v1.36.0 + +wget https://github.com/hetznercloud/cli/releases/download/${HCVERSION}/hcloud-linux-amd64.tar.gz + +tar xzf hcloud-linux-amd64.tar.gz + +sudo mv hcloud /usr/local/bin + +# Install mc + +wget https://dl.min.io/client/mc/release/linux-amd64/mc +chmod +x mc +./mc alias set qdrant https://storage.googleapis.com "${GCS_KEY}" "${GCS_SECRET}" + +bash -x "${SCRIPT_PATH}/run_remote_benchmark.sh" + +./mc cp results/* qdrant/vector-search-engines-benchmark/results/ci/qdrant/ + +# Upload to postgres +# -t sorts by modification time +export SEARCH_RESULTS_FILE=$(ls -t results/*-search-*.json | head -n 1) +export UPLOAD_RESULTS_FILE=$(ls -t results/*-upload-*.json | head -n 1) +export MEMORY_USAGE_FILE=$(ls -t results/memory-usage-*.txt | head -n 1) +export ROOT_API_RESPONSE_FILE=$(ls -t results/root-api-*.json | head -n 1) + +bash -x "${SCRIPT_PATH}/upload_results_postgres.sh" diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_client_script.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_client_script.sh new file mode 100644 index 0000000..c48b87d --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_client_script.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +set -e + +CLOUD_NAME=${CLOUD_NAME:-"hetzner"} +SERVER_USERNAME=${SERVER_USERNAME:-"root"} + +SCRIPT=$(realpath "$0") +SCRIPT_PATH=$(dirname "$SCRIPT") + +BENCH_SERVER_NAME=${SERVER_NAME:-"benchmark-server-1"} +BENCH_CLIENT_NAME=${CLIENT_NAME:-"benchmark-client-1"} + +IP_OF_THE_CLIENT=$(bash "${SCRIPT_PATH}/${CLOUD_NAME}/get_public_ip.sh" "$BENCH_CLIENT_NAME") + +scp "${SCRIPT_PATH}/run_experiment.sh" "${SERVER_USERNAME}@${IP_OF_THE_CLIENT}:~/run_experiment.sh" + +ENGINE_NAME=${ENGINE_NAME:-"qdrant-continuous-benchmark"} + +DATASETS=${DATASETS:-"laion-small-clip"} + +PRIVATE_IP_OF_THE_SERVER=$(bash "${SCRIPT_PATH}/${CLOUD_NAME}/get_private_ip.sh" "$BENCH_SERVER_NAME") + +RUN_EXPERIMENT="ENGINE_NAME=${ENGINE_NAME} DATASETS=${DATASETS} PRIVATE_IP_OF_THE_SERVER=${PRIVATE_IP_OF_THE_SERVER} bash ~/run_experiment.sh" + +ssh -tt "${SERVER_USERNAME}@${IP_OF_THE_CLIENT}" "${RUN_EXPERIMENT}" + +SEARCH_RESULT_FILE=$(ssh "${SERVER_USERNAME}@${IP_OF_THE_CLIENT}" "ls -t results/*-search-*.json | head -n 1") +UPLOAD_RESULT_FILE=$(ssh "${SERVER_USERNAME}@${IP_OF_THE_CLIENT}" "ls -t results/*-upload-*.json | head -n 1") +MEMORY_USAGE_FILE=$(ssh "${SERVER_USERNAME}@${IP_OF_THE_CLIENT}" "ls -t results/memory-usage-*.txt | head -n 1") + +mkdir -p results + +for RESULT_FILE in $SEARCH_RESULT_FILE $UPLOAD_RESULT_FILE $MEMORY_USAGE_FILE; do + # -p preseves modification time, access time, and modes (but not change time) + scp -p "${SERVER_USERNAME}@${IP_OF_THE_CLIENT}:~/${RESULT_FILE}" "./results" +done diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_experiment.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_experiment.sh new file mode 100644 index 0000000..a1b23d5 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_experiment.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +ENGINE_NAME=${ENGINE_NAME:-"qdrant-default"} + +DATASETS=${DATASETS:-""} + +PRIVATE_IP_OF_THE_SERVER=${PRIVATE_IP_OF_THE_SERVER:-""} + +if [[ -z "$ENGINE_NAME" ]]; then + echo "ENGINE_NAME is not set" + exit 1 +fi + +if [[ -z "$DATASETS" ]]; then + echo "DATASETS is not set" + exit 1 +fi + +if [[ -z "$PRIVATE_IP_OF_THE_SERVER" ]]; then + echo "PRIVATE_IP_OF_THE_SERVER is not set" + exit 1 +fi + +docker rmi qdrant/vector-db-benchmark:latest || true + +docker run \ + --rm \ + -it \ + -v "$HOME/results:/code/results" \ + qdrant/vector-db-benchmark:latest \ + python run.py --engines "${ENGINE_NAME}" --datasets "${DATASETS}" --host "${PRIVATE_IP_OF_THE_SERVER}" --no-skip-if-exists --skip-search + +docker run \ + --rm \ + -it \ + -v "$HOME/results:/code/results" \ + qdrant/vector-db-benchmark:latest \ + python run.py --engines "${ENGINE_NAME}" --datasets "${DATASETS}" --host "${PRIVATE_IP_OF_THE_SERVER}" --no-skip-if-exists --skip-upload diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_remote.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_remote.sh new file mode 100644 index 0000000..0b3b339 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_remote.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +set -e + +SCRIPT_PATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" +CLOUD_NAME=${CLOUD_NAME:-"hetzner"} + + +RUN_SCRIPT=${RUN_SCRIPT:-""} +SERVER_NAME=${SERVER_NAME:-""} + +DEFAULT_SSH_USER=$(bash $SCRIPT_PATH/$CLOUD_NAME/get_ssh_user.sh $SERVER_NAME) + +SSH_USER=${SSH_USER:-${DEFAULT_SSH_USER}} + +# List of env variables with values to pass to remote script +# Should be constructed as `${VAR_1@A} ${VAR_2@A}` +ENV_CONTEXT=${ENV_CONTEXT:-""} + +if [[ -z "$RUN_SCRIPT" ]] +then + echo "Please specify RUN_SCRIPT env variable" + exit 1 +fi + +if [[ -z "$SERVER_NAME" ]] +then + echo "Please specify SERVER_NAME env variable" + exit 1 +fi + + +# Get server ip + +SERVER_IP=$(bash $SCRIPT_PATH/$CLOUD_NAME/get_public_ip.sh $SERVER_NAME) + + + +echo $ENV_CONTEXT | cat - "$RUN_SCRIPT" | ssh -oStrictHostKeyChecking=no "$SSH_USER@$SERVER_IP" sudo bash -x + diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_remote_benchmark.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_remote_benchmark.sh new file mode 100644 index 0000000..bcba6a2 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_remote_benchmark.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +set -e + +# Setup 2 machines in Hetzner Cloud +# One machine will be used as a server, another one as a client + +cleanup() { + echo "cleaning up..." + # bash -x "${SCRIPT_PATH}/tear_down.sh" +} + +trap 'echo signal received!; kill $(jobs -p); wait; cleanup' SIGINT SIGTERM + +CLOUD_NAME=${CLOUD_NAME:-"hetzner"} + +SCRIPT=$(realpath "$0") +SCRIPT_PATH=$(dirname "$SCRIPT") + + +BENCH_SERVER_NAME=${SERVER_NAME:-"benchmark-server-1"} +BENCH_CLIENT_NAME=${CLIENT_NAME:-"benchmark-client-1"} + +trap 'cleanup' EXIT + +# Uncomment this to dynamically create servers + +#SERVER_NAME=$BENCH_SERVER_NAME SERVER_TYPE='ccx12' bash -x "${SCRIPT_PATH}/${CLOUD_NAME}/create_and_install.sh" & +#SERVER_CREATION_PID=$! +#SERVER_NAME=$BENCH_CLIENT_NAME SERVER_TYPE='cpx11' bash -x "${SCRIPT_PATH}/${CLOUD_NAME}/create_and_install.sh" +#wait $SERVER_CREATION_PID + +SERVER_NAME=$BENCH_SERVER_NAME bash -x "${SCRIPT_PATH}/${CLOUD_NAME}/check_ssh_connection.sh" +SERVER_NAME=$BENCH_CLIENT_NAME bash -x "${SCRIPT_PATH}/${CLOUD_NAME}/check_ssh_connection.sh" + + +SERVER_CONTAINER_NAME=${SERVER_CONTAINER_NAME:-"qdrant-continuous-benchmarks"} + +bash -x "${SCRIPT_PATH}/run_server_container.sh" "$SERVER_CONTAINER_NAME" + +bash -x "${SCRIPT_PATH}/run_client_script.sh" + +bash -x "${SCRIPT_PATH}/qdrant_collect_stats.sh" "$SERVER_CONTAINER_NAME" diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_server_container.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_server_container.sh new file mode 100644 index 0000000..98d6e73 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/run_server_container.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +set -e + +# Examples: qdrant-single-node +CONTAINER_NAME=$1 + +CLOUD_NAME=${CLOUD_NAME:-"hetzner"} +SERVER_USERNAME=${SERVER_USERNAME:-"root"} + + +SCRIPT=$(realpath "$0") +SCRIPT_PATH=$(dirname "$SCRIPT") + +BENCH_SERVER_NAME=${SERVER_NAME:-"benchmark-server-1"} + +QDRANT_VERSION=${QDRANT_VERSION:-"dev"} + +IP_OF_THE_SERVER=$(bash "${SCRIPT_PATH}/${CLOUD_NAME}/get_public_ip.sh" "$BENCH_SERVER_NAME") + +bash -x "${SCRIPT_PATH}/sync_servers.sh" "root@$IP_OF_THE_SERVER" + +# if version is dev or if starts with "docker" or "ghcr", use container +if [[ ${QDRANT_VERSION} == docker/* ]] || [[ ${QDRANT_VERSION} == ghcr/* ]]; then + + if [[ ${QDRANT_VERSION} == docker/* ]]; then + # pull from docker hub + QDRANT_VERSION=${QDRANT_VERSION#docker/} + CONTAINER_REGISTRY='docker.io' + elif [[ ${QDRANT_VERSION} == ghcr/* ]]; then + # pull from github container registry + QDRANT_VERSION=${QDRANT_VERSION#ghcr/} + CONTAINER_REGISTRY='ghcr.io' + fi + + DOCKER_COMPOSE="export QDRANT_VERSION=${QDRANT_VERSION}; export CONTAINER_REGISTRY=${CONTAINER_REGISTRY}; docker compose down ; pkill qdrant ; docker rmi ${CONTAINER_REGISTRY}/qdrant/qdrant:${QDRANT_VERSION} || true ; docker compose up -d" + ssh -t "${SERVER_USERNAME}@${IP_OF_THE_SERVER}" "cd ./projects/vector-db-benchmark/engine/servers/${CONTAINER_NAME} ; $DOCKER_COMPOSE" +else + # else run natively in the server + DOCKER_QDRANT_STOP="docker stop qdrant-continuous || true" + QDRANT_BUILD="source ~/.cargo/env; git fetch --tags; git checkout ${QDRANT_VERSION}; git pull; mold -run cargo run --bin qdrant --release" + ssh -t "${SERVER_USERNAME}@${IP_OF_THE_SERVER}" "cd ./projects/qdrant; ${DOCKER_QDRANT_STOP}; $QDRANT_BUILD" +fi diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/ssh.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/ssh.sh new file mode 100755 index 0000000..8ab6bdb --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/ssh.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Usage: tools/ssh.sh custom|hetzner + +set -e + +SCRIPT_PATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" +CLOUD_NAME=${CLOUD_NAME:-$1} + +SERVER_NAME=${SERVER_NAME:-$2} + +DEFAULT_SSH_USER=$(bash $SCRIPT_PATH/$CLOUD_NAME/get_ssh_user.sh $SERVER_NAME) +SSH_USER=${SSH_USER:-${DEFAULT_SSH_USER}} + +if [[ -z "$CLOUD_NAME" ]] +then + echo "Please pass CLOUD_NAME env variable" + exit 1 +fi + +if [[ -z "$SERVER_NAME" ]] +then + echo "Please specify SERVER_NAME env variable" + exit 1 +fi + +# Get server ip +SERVER_IP=$(bash $SCRIPT_PATH/$CLOUD_NAME/get_public_ip.sh $SERVER_NAME) + +ssh $SSH_USER@$SERVER_IP diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/sync.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/sync.sh new file mode 100755 index 0000000..192fd83 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/sync.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +PROJECT_PATH=$(realpath "$(dirname "$0")/..") + +rsync -avP \ + --exclude="results" \ + --exclude="results-*" \ + --exclude='*.hdf5' \ + --exclude='venv' \ + --exclude='__pycache__' \ + --exclude='frontend' \ + --exclude='.idea' \ + --exclude='.git' \ + --exclude='datasets/*/' \ + "$PROJECT_PATH/" $1:./projects/vector-db-benchmark/ diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/sync_servers.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/sync_servers.sh new file mode 100755 index 0000000..7304cbb --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/sync_servers.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +PROJECT_PATH=$(realpath "$(dirname "$0")/..") + +rsync -avP --mkpath\ + "$PROJECT_PATH/engine/servers/" $1:./projects/vector-db-benchmark/engine/servers/ diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/tear_down.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/tear_down.sh new file mode 100644 index 0000000..4692c65 --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/tear_down.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -e + +CLOUD_NAME=${CLOUD_NAME:-"hetzner"} + +SCRIPT=$(realpath "$0") +SCRIPT_PATH=$(dirname "$SCRIPT") + +BENCH_SERVER_NAME=${SERVER_NAME:-"benchmark-server-1"} +BENCH_CLIENT_NAME=${CLIENT_NAME:-"benchmark-client-1"} + +bash -x "${SCRIPT_PATH}/${CLOUD_NAME}/remove_server.sh" "$BENCH_SERVER_NAME" +bash -x "${SCRIPT_PATH}/${CLOUD_NAME}/remove_server.sh" "$BENCH_CLIENT_NAME" \ No newline at end of file diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/tools/upload_results_postgres.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/upload_results_postgres.sh new file mode 100644 index 0000000..4bda52c --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/tools/upload_results_postgres.sh @@ -0,0 +1,73 @@ +#!/bin/bash + + +# Read search results from json file and upload it to postgres +# +# Assume table: +# create table benchmark ( +# id SERIAL PRIMARY key, +# engine VARCHAR(255), +# branch VARCHAR(255), +# commit CHAR(40), +# dataset VARCHAR(255), +# measure_timestamp TIMESTAMP, +# upload_time real, +# indexing_time real, +# rps real, +# mean_precisions real, +# p95_time real, +# p99_time real, +# memory_usage real +# ); + +SEARCH_RESULTS_FILE=${SEARCH_RESULTS_FILE:-""} +UPLOAD_RESULTS_FILE=${UPLOAD_RESULTS_FILE:-""} +MEMORY_USAGE_FILE=${MEMORY_USAGE_FILE:-""} +ROOT_API_RESPONSE_FILE=${ROOT_API_RESPONSE_FILE:-""} +POSTGRES_TABLE=${POSTGRES_TABLE:-"benchmark"} + +QDRANT_VERSION=${QDRANT_VERSION:-"dev"} +DATASETS=${DATASETS:-"laion-small-clip"} + +if [[ -z "$SEARCH_RESULTS_FILE" ]]; then + echo "SEARCH_RESULTS_FILE is not set" + exit 1 +fi + + +if [[ -z "$UPLOAD_RESULTS_FILE" ]]; then + echo "UPLOAD_RESULTS_FILE is not set" + exit 1 +fi + + +if [[ -z "$MEMORY_USAGE_FILE" ]]; then + echo "MEMORY_USAGE_FILE is not set" + exit 1 +fi + +if [[ -z "$ROOT_API_RESPONSE_FILE" ]]; then + echo "ROOT_API_RESPONSE_FILE is not set" + exit 1 +fi + +RPS=$(jq -r '.results.rps' "$SEARCH_RESULTS_FILE") +MEAN_PRECISIONS=$(jq -r '.results.mean_precisions' "$SEARCH_RESULTS_FILE") +P95_TIME=$(jq -r '.results.p95_time' "$SEARCH_RESULTS_FILE") +P99_TIME=$(jq -r '.results.p99_time' "$SEARCH_RESULTS_FILE") + +UPLOAD_TIME=$(jq -r '.results.upload_time' "$UPLOAD_RESULTS_FILE") +INDEXING_TIME=$(jq -r '.results.total_time' "$UPLOAD_RESULTS_FILE") + +MEMORY_USAGE=$(cat "$MEMORY_USAGE_FILE") + +QDRANT_COMMIT=$(jq -r '.commit' "$ROOT_API_RESPONSE_FILE") + +MEASURE_TIMESTAMP=${MEASURE_TIMESTAMP:-$(date -u +"%Y-%m-%dT%H:%M:%SZ")} + + +docker run --rm jbergknoff/postgresql-client "postgresql://qdrant:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:5432/postgres" -c " +INSERT INTO ${POSTGRES_TABLE} (engine, branch, commit, dataset, measure_timestamp, upload_time, indexing_time, rps, mean_precisions, p95_time, p99_time, memory_usage) +VALUES ('qdrant-ci', '${QDRANT_VERSION}', '${QDRANT_COMMIT}', '${DATASETS}', '${MEASURE_TIMESTAMP}', ${UPLOAD_TIME}, ${INDEXING_TIME}, ${RPS}, ${MEAN_PRECISIONS}, ${P95_TIME}, ${P99_TIME}, ${MEMORY_USAGE}); +" + diff --git a/02-semantic-search-rag/fcenedes/vecdb_bench/vectorsets.sh b/02-semantic-search-rag/fcenedes/vecdb_bench/vectorsets.sh new file mode 100755 index 0000000..4be2cea --- /dev/null +++ b/02-semantic-search-rag/fcenedes/vecdb_bench/vectorsets.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# Check if hostname is provided +if [ $# -lt 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +hostname=$1 + +# Define experiments array +experiments=( + "vectorsets-bin-default" + "vectorsets-bin-m-32-ef-128" + "vectorsets-bin-m-32-ef-256" + "vectorsets-bin-m-32-ef-512" + "vectorsets-bin-m-64-ef-256" + "vectorsets-bin-m-64-ef-512" + + "vectorsets-q8-default" + "vectorsets-q8-m-32-ef-128" + "vectorsets-q8-m-32-ef-256" + "vectorsets-q8-m-32-ef-512" + "vectorsets-q8-m-64-ef-256" + "vectorsets-q8-m-64-ef-512" + + "vectorsets-fp32-default" + "vectorsets-fp32-m-32-ef-128" + "vectorsets-fp32-m-32-ef-256" + "vectorsets-fp32-m-32-ef-512" + "vectorsets-fp32-m-64-ef-256" + "vectorsets-fp32-m-64-ef-512" +) + +# Run command for each experiment +for experiment in "${experiments[@]}"; do + echo "Running experiment: $experiment" + python run.py --engines "$experiment" --datasets dbpedia-openai-1M-1536-angular --host "$hostname" + echo "Completed experiment: $experiment" + echo "-----------------------------------" +done + +echo "All experiments completed!" \ No newline at end of file