{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Redis Vector Search Benchmarking with RedisVL\n", "\n", "## A Practical Guide to Multiprocessing Performance Testing\n", "\n", "This tutorial demonstrates how to benchmark Redis vector search performance using multiprocessing with RedisVL to bypass Python's GIL and achieve true parallelism.\n", "\n", "### What You'll Learn\n", "- Set up efficient Redis connections for multiprocessing\n", "- Implement multi-process data loading with batching\n", "- Build parallel query execution with worker processes\n", "- Measure and analyze key performance metrics\n", "- Understand factors affecting Redis performance\n", "\n", "### Tutorial Structure\n", "1. **Part 1: Setup & Configuration** - We'll define all our classes, functions, and utilities\n", "2. **Part 2: Benchmarking Execution** - We'll run the actual performance tests and analyze results\n", "\n", "---" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Part 1: Setup & Configuration\n", "\n", "First, let's install dependencies and import the libraries we'll need for benchmarking." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "# Install and import dependencies\n", "%pip install redisvl redis numpy matplotlib pandas tqdm" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from multiprocessing import get_context\n", "from typing import List, Dict, Any, Optional, Tuple\n", "from dataclasses import dataclass\n", "from tqdm import tqdm\n", "from contextlib import contextmanager\n", "\n", "# RedisVL imports\n", "import redis\n", "from redisvl.index import SearchIndex\n", "from redisvl.query import VectorQuery\n", "from redisvl.schema import IndexSchema" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Redis instance\n", "Set up a local Redis instance to use for testing" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "!docker run -d --name redis -p 6379:6379 -v redis_data:/data --restart unless-stopped redis:8.0.0 redis-server --search-workers 6" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Configuration Class & Redis Connection\n", "\n", "We'll define our benchmark configuration. Note that for multiprocessing, we don't use connection pooling since each process will create its own Redis client." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "# Benchmark configuration\n", "@dataclass\n", "class BenchmarkConfig:\n", " # Redis settings\n", " redis_host: str = \"localhost\"\n", " redis_port: int = 6379\n", " redis_password: Optional[str] = None\n", " \n", " # Index settings\n", " index_name: str = \"benchmark_index\"\n", " vector_dim: int = 768\n", " distance_metric: str = \"cosine\"\n", " algorithm: str = \"hnsw\" # flat or hnsw\n", " \n", " # Data settings\n", " data_size: int = 500000\n", " batch_size: int = 1000\n", " query_count: int = 10000\n", " num_results: int = 5\n", " \n", " # Multiprocessing settings\n", " workers: int = 10\n", " mp_start_method: str = \"fork\"" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "def create_redis_client(config: BenchmarkConfig) -> redis.Redis:\n", " return redis.Redis(\n", " host=config.redis_host,\n", " port=config.redis_port,\n", " password=config.redis_password,\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Core Utility Classes\n", "\n", "Next, we'll define our core utilities: a vector generator for creating test data and a timing context manager." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "class VectorGenerator:\n", " \"\"\"Generate normalized random vectors for testing\"\"\"\n", " def __init__(self, dimension: int, seed: int = 42):\n", " self.dimension = dimension\n", " np.random.seed(seed)\n", " \n", " def generate_vectors(self, count: int) -> np.ndarray:\n", " \"\"\"Generate normalized random vectors\"\"\"\n", " vectors = np.random.randn(count, self.dimension).astype(np.float32)\n", " return vectors" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "@contextmanager\n", "def timer(name: Optional[str] = None):\n", " \"\"\"Unified context manager for timing operations\n", " \n", " Usage:\n", " # Auto-logging version:\n", " with timer(\"Test data generation\"):\n", " # do work\n", " \n", " # Get elapsed time version:\n", " with timer() as elapsed:\n", " # do work\n", " total_time = elapsed()\n", " \n", " # Both (log + get time):\n", " with timer(\"Loading data\") as elapsed:\n", " # do work\n", " throughput = ops / elapsed()\n", " \"\"\"\n", " start_time = time.perf_counter()\n", " elapsed = lambda: time.perf_counter() - start_time\n", " \n", " try:\n", " yield elapsed\n", " finally:\n", " if name:\n", " print(f\"โฑ๏ธ {name}: {elapsed():.2f}s\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Index Management Functions\n", "\n", "Now we'll define functions to create and manage our Redis vector search index using RedisVL schemas." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "def create_index_schema(config: BenchmarkConfig) -> IndexSchema:\n", " \"\"\"Create RedisVL index schema\"\"\"\n", " schema_dict = {\n", " \"index\": {\n", " \"name\": config.index_name,\n", " \"prefix\": f\"{config.index_name}:\",\n", " \"storage_type\": \"hash\"\n", " },\n", " \"fields\": [\n", " {\n", " \"name\": \"vector\",\n", " \"type\": \"vector\",\n", " \"attrs\": {\n", " \"dims\": config.vector_dim,\n", " \"distance_metric\": config.distance_metric,\n", " \"algorithm\": config.algorithm,\n", " \"datatype\": \"float32\",\n", " \"initial_cap\": config.data_size\n", " }\n", " },\n", " {\"name\": \"id\", \"type\": \"text\"},\n", " {\"name\": \"metadata\", \"type\": \"text\"}\n", " ]\n", " }\n", " return IndexSchema.from_dict(schema_dict)\n", "\n", "def setup_index(config: BenchmarkConfig, redis_client: redis.Redis) -> SearchIndex:\n", " \"\"\"Create and return search index using provided Redis client\"\"\"\n", " schema = create_index_schema(config)\n", " search_index = SearchIndex(schema, redis_client)\n", " search_index.create(overwrite=True)\n", " print(f\"โœ… Created index: {config.index_name} ({config.algorithm}, {config.vector_dim}D)\")\n", " return search_index" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Multiprocessing Worker Functions\n", "\n", "Here we define the worker functions and initialization for multiprocessing. Each process will have its own Redis client." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "# Global variables for multiprocessing workers\n", "_redis_client = None\n", "_search_index = None\n", "_config = None\n", "\n", "def init_worker(config_dict: dict):\n", " \"\"\"Initialize Redis connection and search index in each worker process\n", " \n", " Each process needs its own Redis client - cannot share across processes.\n", " \"\"\"\n", " global _redis_client, _search_index, _config\n", " \n", " # Reconstruct config from dict\n", " _config = BenchmarkConfig(**config_dict)\n", " \n", " # Create Redis client for this process (process-local)\n", " _redis_client = create_redis_client(_config)\n", " \n", " # Create search index with process-local client\n", " _search_index = SearchIndex(create_index_schema(_config), _redis_client)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Data Loading Functions\n", "\n", "Here we define both sequential and multiprocessing data loading functions." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "def load_batch_worker(\n", " batch_data: List[Tuple[int, np.ndarray]], \n", " redis_client: Optional[redis.Redis] = None, \n", " config: Optional[BenchmarkConfig] = None\n", ") -> Tuple[int, float]:\n", " \"\"\"Load a batch of vectors using Redis pipeline (worker function)\n", " \n", " For sequential: uses passed redis_client (shared)\n", " For parallel: uses global _redis_client (process-local)\n", " \"\"\"\n", " # Use passed parameters (sequential) or fall back to globals (multiprocessing)\n", " client = redis_client or _redis_client\n", " cfg = config or _config\n", " \n", " with timer() as elapsed:\n", " with client.pipeline(transaction=False) as pipe:\n", " for doc_id, vector in batch_data:\n", " pipe.hset(f\"{cfg.index_name}:{doc_id}\", mapping={\n", " \"vector\": vector.tobytes(),\n", " \"id\": f\"doc_{doc_id}\",\n", " \"metadata\": f\"document_{doc_id}\"\n", " })\n", " pipe.execute()\n", " elapsed_ms = elapsed() * 1000\n", " return len(batch_data), elapsed_ms\n", "\n", "# Wrapper functions for multiprocessing (needed for pickling)\n", "def load_batch_worker_mp(batch_data: List[Tuple[int, np.ndarray]]) -> Tuple[int, float]:\n", " \"\"\"Multiprocessing wrapper for load_batch_worker\"\"\"\n", " return load_batch_worker(batch_data)" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "def run_loading_benchmark(\n", " config: BenchmarkConfig, \n", " vectors: np.ndarray, \n", " method: str = \"sequential\",\n", " redis_client: Optional[redis.Redis] = None\n", ") -> Dict[str, Any]:\n", " \"\"\"Run loading benchmark using specified method\n", " \n", " Args:\n", " redis_client: For sequential execution, reuse this client\n", " \"\"\"\n", " \n", " if method == \"sequential\":\n", " print(f\"๐Ÿ“ฅ Loading {len(vectors):,} vectors (sequential)...\")\n", " \n", " # Create batches\n", " batches = []\n", " for i in range(0, len(vectors), config.batch_size):\n", " batch_vectors = vectors[i:i + config.batch_size]\n", " batch_data = [(i + j, batch_vectors[j]) for j in range(len(batch_vectors))]\n", " batches.append(batch_data)\n", " \n", " # Execute batches sequentially using shared client\n", " with timer() as elapsed:\n", " for batch_data in tqdm(batches, desc=\"Loading\"):\n", " batch_size, _ = load_batch_worker(batch_data, redis_client, config)\n", " \n", " total_loaded = len(vectors)\n", " total_time = elapsed()\n", " \n", " elif method == \"multiprocess\":\n", " print(f\"๐Ÿ“ฅ Loading {len(vectors):,} vectors ({config.workers} processes)...\")\n", " \n", " # Create batches for parallel processing\n", " batches = []\n", " for i in range(0, len(vectors), config.batch_size):\n", " batch_vectors = vectors[i:i + config.batch_size]\n", " batch_data = [(i + j, batch_vectors[j]) for j in range(len(batch_vectors))]\n", " batches.append(batch_data)\n", " \n", " ctx = get_context(config.mp_start_method)\n", " config_dict = config.__dict__\n", " \n", " with timer() as elapsed:\n", " # Each process will create its own Redis client via init_worker\n", " with ctx.Pool(\n", " processes=config.workers,\n", " initializer=init_worker,\n", " initargs=(config_dict,)\n", " ) as pool:\n", " results = list(tqdm(\n", " pool.imap_unordered(load_batch_worker_mp, batches),\n", " total=len(batches),\n", " desc=\"Loading\"\n", " ))\n", " \n", " total_time = elapsed()\n", " # Process results: (batch_size, latency_ms) tuples\n", " total_loaded = sum(batch_size for batch_size, _ in results)\n", " \n", " return {\n", " \"total_time\": total_time,\n", " \"throughput\": total_loaded / total_time,\n", " \"total_ops\": total_loaded,\n", " \"success_rate\": 100.0\n", " }" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Query Execution Functions\n", "\n", "Now we'll define our query execution functions for both sequential and multiprocessing approaches." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "def query_worker(\n", " query_vector: np.ndarray,\n", " search_index: Optional[SearchIndex] = None,\n", " config: Optional[BenchmarkConfig] = None\n", ") -> Tuple[bool, float]:\n", " \"\"\"Execute a single vector search query (worker function)\n", " \n", " For sequential: uses passed search_index (shared client)\n", " For parallel: uses global _search_index (process-local client)\n", " \"\"\"\n", " # Use passed parameters (sequential) or fall back to globals (multiprocessing)\n", " index = search_index or _search_index\n", " cfg = config or _config\n", " \n", " try:\n", " with timer() as elapsed:\n", " query = VectorQuery(\n", " vector=query_vector,\n", " vector_field_name=\"vector\",\n", " num_results=cfg.num_results,\n", " return_score=True\n", " )\n", " results = index.query(query)\n", " elapsed_ms = elapsed() * 1000\n", " return True, elapsed_ms\n", " except Exception as e:\n", " print(f\"Query failed: {e}\")\n", " return False, 0.0\n", "\n", "# Wrapper functions for multiprocessing (needed for pickling)\n", "def query_worker_mp(query_vector: np.ndarray) -> Tuple[bool, float]:\n", " \"\"\"Multiprocessing wrapper for query_worker\"\"\"\n", " return query_worker(query_vector)" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "def run_query_benchmark(\n", " config: BenchmarkConfig, \n", " query_vectors: np.ndarray,\n", " method: str = \"sequential\",\n", " search_index: Optional[SearchIndex] = None\n", ") -> Dict[str, Any]:\n", " \"\"\"Run query benchmark using specified method\n", " \n", " Args:\n", " search_index: For sequential execution, reuse this index (with shared client)\n", " \"\"\"\n", " \n", " if method == \"sequential\":\n", " print(f\"๐Ÿ” Executing {len(query_vectors):,} queries (sequential)...\")\n", " latencies = []\n", " failed_queries = 0\n", " \n", " with timer() as elapsed:\n", " for query_vector in tqdm(query_vectors, desc=\"Querying\"):\n", " success, latency_ms = query_worker(query_vector, search_index, config)\n", " if success:\n", " latencies.append(latency_ms)\n", " else:\n", " failed_queries += 1\n", " \n", " total_time = elapsed()\n", " \n", " elif method == \"multiprocess\":\n", " print(f\"๐Ÿ” Executing {len(query_vectors):,} queries ({config.workers} processes)...\")\n", " \n", " ctx = get_context(config.mp_start_method)\n", " config_dict = config.__dict__\n", " \n", " with timer() as elapsed:\n", " # Each process will create its own Redis client and search index via init_worker\n", " with ctx.Pool(\n", " processes=config.workers,\n", " initializer=init_worker,\n", " initargs=(config_dict,)\n", " ) as pool:\n", " results = list(tqdm(\n", " pool.imap_unordered(query_worker_mp, query_vectors),\n", " total=len(query_vectors),\n", " desc=\"Querying\"\n", " ))\n", " \n", " total_time = elapsed()\n", " # Process results: (success, latency_ms) tuples\n", " latencies = [latency for success, latency in results if success]\n", " failed_queries = len([r for r in results if not r[0]])\n", " \n", " if latencies:\n", " return {\n", " \"total_time\": total_time,\n", " \"qps\": len(latencies) / total_time,\n", " \"avg_latency\": np.mean(latencies),\n", " \"p95_latency\": np.percentile(latencies, 95),\n", " \"p99_latency\": np.percentile(latencies, 99),\n", " \"successful_queries\": len(latencies),\n", " \"failed_queries\": failed_queries,\n", " \"success_rate\": (len(latencies) / len(query_vectors)) * 100\n", " }\n", " else:\n", " return {\"error\": \"No successful queries\"}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Part 2: Benchmarking Execution\n", "\n", "Now that we have all our functions defined, let's run the actual benchmarks! We'll start by setting up our test environment and data." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Initialize Environment\n", "\n", "Let's test our Redis connection, generate test vectors, and create our search index." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "๐Ÿ“Š Configuration: 500,000 vectors, 768D, 10 processes\n", "โœ… Connected to Redis 8.0.0\n", " Memory: 498.52M used\n", "โœ… Created index: benchmark_index (hnsw, 768D)\n" ] } ], "source": [ "# Initialize configuration (you can modify these values as needed)\n", "config = BenchmarkConfig()\n", "print(f\"๐Ÿ“Š Configuration: {config.data_size:,} vectors, {config.vector_dim}D, {config.workers} processes\")\n", "\n", "# Create Redis client\n", "try:\n", " redis_client = create_redis_client(config)\n", " redis_client.ping()\n", " info = redis_client.info()\n", " print(f\"โœ… Connected to Redis {info['redis_version']}\")\n", " print(f\" Memory: {info['used_memory_human']} used\")\n", " \n", " # Clear any existing data and create fresh index using shared client\n", " redis_client.flushdb()\n", " search_index = setup_index(config, redis_client)\n", "except Exception as e:\n", " print(f\"โŒ Redis connection failed: {e}\")\n", " raise" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Initialize vector generator and create test/query vector data based on config." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "โฑ๏ธ Test data generation: 5.10s\n", "๐Ÿ“Š Generated 500,000 test vectors (1464.8 MB)\n", "๐Ÿ” Generated 10,000 query vectors for testing\n" ] } ], "source": [ "# Generate test data\n", "with timer(\"Test data generation\"):\n", " vector_gen = VectorGenerator(config.vector_dim)\n", " test_vectors = vector_gen.generate_vectors(config.data_size)\n", " query_vectors = vector_gen.generate_vectors(config.query_count)\n", "\n", "print(f\"๐Ÿ“Š Generated {len(test_vectors):,} test vectors ({test_vectors.nbytes / 1024 / 1024:.1f} MB)\")\n", "print(f\"๐Ÿ” Generated {len(query_vectors):,} query vectors for testing\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Data Loading Benchmark\n", "\n", "Now let's compare sequential vs multiprocessing data loading performance." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "๐Ÿ”ฌ Data Loading Performance Comparison\n", "\n", "=== Sequential Loading ===\n", "๐Ÿ“ฅ Loading 500,000 vectors (sequential)...\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "Loading: 100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 500/500 [03:18<00:00, 2.52it/s]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "โฑ๏ธ Sequential loading: 198.16s\n", "Results: 2524.4 ops/sec\n", "โœ… Created index: benchmark_index (hnsw, 768D)\n", "\n", "=== Multi-process Loading ===\n", "๐Ÿ“ฅ Loading 500,000 vectors (10 processes)...\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "Loading: 100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 500/500 [03:19<00:00, 2.51it/s]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "โฑ๏ธ Multi-process loading: 199.70s\n", "Results: 2505.2 ops/sec\n", "\n", "๐Ÿš€ Loading Performance:\n", " 1.0x faster with 10 processes\n", " Time saved: -0.8%\n", "โฑ๏ธ Redis indexing: 3.01s\n", "โœ… Index ready: 500,000 documents indexed\n" ] } ], "source": [ "print(\"๐Ÿ”ฌ Data Loading Performance Comparison\\n\")\n", "\n", "# Sequential loading benchmark\n", "print(\"=== Sequential Loading ===\")\n", "with timer(\"Sequential loading\"):\n", " # Pass shared Redis client to avoid creating new connections\n", " sequential_load_stats = run_loading_benchmark(config, test_vectors, \"sequential\", redis_client)\n", "print(f\"Results: {sequential_load_stats['throughput']:.1f} ops/sec\")\n", "\n", "# Reset for multi-process test (reuse same client and index)\n", "redis_client.flushdb()\n", "search_index = setup_index(config, redis_client)\n", "\n", "print(\"\\n=== Multi-process Loading ===\")\n", "with timer(\"Multi-process loading\"):\n", " # Multiprocessing will create separate clients per process\n", " multiprocess_load_stats = run_loading_benchmark(config, test_vectors, \"multiprocess\")\n", "print(f\"Results: {multiprocess_load_stats['throughput']:.1f} ops/sec\")\n", "\n", "# Calculate loading performance improvement\n", "loading_speedup = multiprocess_load_stats['throughput'] / sequential_load_stats['throughput']\n", "loading_time_reduction = (sequential_load_stats['total_time'] - multiprocess_load_stats['total_time']) / sequential_load_stats['total_time'] * 100\n", "\n", "print(f\"\\n๐Ÿš€ Loading Performance:\")\n", "print(f\" {loading_speedup:.1f}x faster with {config.workers} processes\")\n", "print(f\" Time saved: {loading_time_reduction:.1f}%\")\n", "\n", "# Wait for indexing to complete\n", "with timer(\"Redis indexing\"):\n", " time.sleep(3)\n", " index_info = search_index.info()\n", "print(f\"โœ… Index ready: {index_info.get('num_docs', 0):,} documents indexed\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Even though we used multiple processes to load from the client-side, the limitation here is actually the redis server indicating we would need to shard out the db in a clustered environment (Redis Cloud or Redis Enterprise Software). With additional shards, the write throughput will improve linearly." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Query Performance Benchmark\n", "\n", "Now let's test query performance comparing sequential vs multiprocessing execution." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "๐Ÿ”ฌ Query Performance Comparison\n", "\n", "=== Sequential Queries ===\n", "๐Ÿ” Executing 10,000 queries (sequential)...\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "Querying: 100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 10000/10000 [00:11<00:00, 883.63it/s]" ] }, { "name": "stdout", "output_type": "stream", "text": [ "โฑ๏ธ Sequential queries: 11.33s\n", "Results: 883.4 QPS, 1.11ms avg\n", "\n", "=== Multi-process Queries ===\n", "๐Ÿ” Executing 10,000 queries (10 processes)...\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\n", "Querying: 100%|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ| 10000/10000 [00:01<00:00, 9130.64it/s]" ] }, { "name": "stdout", "output_type": "stream", "text": [ "โฑ๏ธ Multi-process queries: 1.16s\n", "Results: 8663.6 QPS, 1.05ms avg\n", "\n", "๐Ÿš€ Query Performance:\n", " 9.8x faster with 10 processes\n", " Time saved: 89.8%\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "print(\"\\n๐Ÿ”ฌ Query Performance Comparison\")\n", "\n", "print(\"\\n=== Sequential Queries ===\")\n", "with timer(\"Sequential queries\"):\n", " # Pass shared search index to avoid creating new connections\n", " sequential_query_stats = run_query_benchmark(config, query_vectors, \"sequential\", search_index)\n", "if \"error\" not in sequential_query_stats:\n", " print(f\"Results: {sequential_query_stats['qps']:.1f} QPS, {sequential_query_stats['avg_latency']:.2f}ms avg\")\n", "\n", "print(\"\\n=== Multi-process Queries ===\")\n", "with timer(\"Multi-process queries\"):\n", " # Multiprocessing will create separate clients per process\n", " multiprocess_query_stats = run_query_benchmark(config, query_vectors, \"multiprocess\")\n", "if \"error\" not in multiprocess_query_stats:\n", " print(f\"Results: {multiprocess_query_stats['qps']:.1f} QPS, {multiprocess_query_stats['avg_latency']:.2f}ms avg\")\n", " \n", " # Calculate query performance improvement\n", " query_speedup = multiprocess_query_stats['qps'] / sequential_query_stats['qps']\n", " query_time_reduction = (sequential_query_stats['total_time'] - multiprocess_query_stats['total_time']) / sequential_query_stats['total_time'] * 100\n", " \n", " print(f\"\\n๐Ÿš€ Query Performance:\")\n", " print(f\" {query_speedup:.1f}x faster with {config.workers} processes\")\n", " print(f\" Time saved: {query_time_reduction:.1f}%\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The near 10x improvement in search throughput as we scale client-side processes indicates we haven't fully saturated the Redis server! Additional throughput can be achieved on Redis Cloud / Redis Enterprise Software with additional QPF (search threads) and sharding as the data volume grows. The best solution is a healthy balance of horozontal and vertical scale.\n", "\n", "[Read more about benchmarking techniques and Redis query engine architecture.](https://redis.io/blog/benchmarking-results-for-vector-databases/)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Performance Analysis & Visualization\n", "\n", "Let's analyze our results and create visualizations to better understand the performance improvements." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "๐Ÿ“Š Summary:\n", " Loading: 2524 โ†’ 2505 ops/sec (1.0x)\n", " Queries: 883 โ†’ 8664 QPS (9.8x)\n", " Total time: 209.4s โ†’ 200.7s\n", " Peak QPS: 8664 queries/second\n" ] } ], "source": [ "# Performance Summary\n", "if \"error\" not in sequential_query_stats and \"error\" not in multiprocess_query_stats:\n", " print(f\"\\n๐Ÿ“Š Summary:\")\n", " print(f\" Loading: {sequential_load_stats['throughput']:.0f} โ†’ {multiprocess_load_stats['throughput']:.0f} ops/sec ({loading_speedup:.1f}x)\")\n", " print(f\" Queries: {sequential_query_stats['qps']:.0f} โ†’ {multiprocess_query_stats['qps']:.0f} QPS ({query_speedup:.1f}x)\")\n", " print(f\" Total time: {sequential_load_stats['total_time'] + sequential_query_stats['total_time']:.1f}s โ†’ {multiprocess_load_stats['total_time'] + multiprocess_query_stats['total_time']:.1f}s\")\n", " print(f\" Peak QPS: {multiprocess_query_stats['qps']:.0f} queries/second\")" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "๐Ÿ“Š Performance Summary:\n", " Operation Method Throughput Avg_Latency_ms P95_Latency_ms\n", " Data Loading Sequential 2524.440444 0.000000 0.000000\n", " Data Loading Multi-process 2505.225020 0.000000 0.000000\n", "Vector Queries Sequential 883.366387 1.107922 1.701246\n", "Vector Queries Multi-process 8663.586546 1.048395 1.746423\n" ] } ], "source": [ "# Create comprehensive performance comparison\n", "perf_data = {\n", " 'Operation': ['Data Loading', 'Data Loading', 'Vector Queries', 'Vector Queries'],\n", " 'Method': ['Sequential', 'Multi-process', 'Sequential', 'Multi-process'],\n", " 'Throughput': [sequential_load_stats['throughput'], multiprocess_load_stats['throughput'], \n", " sequential_query_stats['qps'], multiprocess_query_stats['qps']],\n", " 'Avg_Latency_ms': [0, 0, # Loading doesn't track individual latencies\n", " sequential_query_stats['avg_latency'], multiprocess_query_stats['avg_latency']],\n", " 'P95_Latency_ms': [0, 0, # Loading doesn't track individual latencies\n", " sequential_query_stats['p95_latency'], multiprocess_query_stats['p95_latency']]\n", "}\n", "\n", "df = pd.DataFrame(perf_data)\n", "print(\"๐Ÿ“Š Performance Summary:\")\n", "print(df.to_string(index=False))" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAABKUAAAMWCAYAAAAgRDUeAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAAvEdJREFUeJzs3Qm8lPP///9X+0J72hcl2vdIqUgbUkr2KK0fFFqIo00pkVaUELIUhYTQimghlUibqBRtlhbtqvnfnu/v/5rfzJzT6XQ65zrLPO6329U5c80117yvmTlzvXpd7/frnSEQCAQMAAAAAAAA8FFGP58MAAAAAAAAEJJSAAAAAAAA8B1JKQAAAAAAAPiOpBQAAAAAAAB8R1IKAAAAAAAAviMpBQAAAAAAAN+RlAIAAAAAAIDvSEoBAAAAAADAdySlAAAAAAAA4DuSUgDiNWXKFMuQIYNt3bo1uO6qq65yS2qm9lWpUsXSqrvvvtvOP/98S0tmzJhh+fPnt4MHD6Z0U1KFyy+/3Pr165fSzQAARKHHH3/cxW9//fWXpeX4c8WKFZZWbN++3bJnz25Lliyx1OTvv/+28847zz799NOUbgoQJ5JSSPe8k5q36GRRrFgxa9GihT377LP277//JnrfS5cudSf9ffv2JWmb03ogkRy81+RMS2pPlqUFhw8fdq/3l19+meDHnDx50gYPHmz3339/qkmm7d271zJnzuySZSnhkUcesQkTJtiuXbtS5PkBIJqtXbvW7rzzTitevLhly5bNxX66vW7dOkuLdE5OSBykBedu4sSJ7v8QZ2Po0KFWt25du+KKK2LdN3v2bLvmmmusQIEC7v8il1xyiT388MP2zz//xHlhMvT9zJ07t1WvXt1Gjx5tx44dC9t28eLFdu2117rPufZbqlQpa9WqlU2bNi24jZ6za9euNnDgwLM6HsAvmX17JiCF6URRpkwZ+++//9x/EnVy79Wrl40ZM8Y++ugjq1atWqKSUkOGDHEnj7x581q0mDdvnu/PeeONN1q5cuWCt9Ub595777W2bdu6+zyFCxf2vW3pMSmlz7UkNMn38ccf28aNG6179+6WWsydO9cFc82bN0+R57/hhhtcIKnAVt8/AAB/zJw5026//XbXe7dLly4u/lOP71deecXee+89mz59uvuOTksqVqxob775Zti6mJgYdyGof//+Kdau9Ern7oIFC7oYPyH+/PNPe/31190S6aGHHnIJJSWWdMFKn8tVq1bZc8895z6LCxcutIsvvjjsMUqkTp482f2ui9/vv/++2893331n77zzjlv/7rvv2q233mo1atSwBx980PLly2dbtmyxr776yl5++WW74447gvu755573MX4zz//3K6++upzfHWApEVSClFDVxHq1KkTdiLXF/P1119vrVu3tvXr11uOHDlStI1pRdasWX1/TiUNQxOH6kWmpJTW6cpnUjp69Kg7xowZ6UyaUK+99pq7MqgrdamFuqmrTSmVMNbn56abbrI33njDJfm4eg0Aye/XX3+1u+66y8qWLev+c37BBRcE79N/3Bs2bOjihh9//NElq/ykiz45c+ZM1GN10S0y3nnqqadc4iSp46BTp07Z8ePHXc8bJMxbb73lemerl1Kot99+2yWklDyaOnWqZcqUKXifEl6NGze2m2++2Q1T1OM9+j30fb3vvvtcLywlsXRBXT3/1Ku9UqVK9s0338SKzffs2RMrqamyFur9RVIKqQ3/40JU05eyurL+9ttv7mTiUaCiE4UCGp2QixQpYp07d3Zjsj06EajbrSio8brYerWX9J907b9QoULuaodOGi+88EKStl9JNQVXGieu/3jrqp+Sa6F0bDqRlS9f3iXd1IVXJ7/QGlGhXd3VZm1XokQJGzZsmAtMIkXWlPK6lGuY1PDhw91j9bo1adLEfvnll1iP15AmvbZ6nssuu8y+/vrrZKtTpW76OuErCFTCZOTIkWH3e23XVacBAwa4bbTtgQMHglehateu7drqBX5//PFHvK+HR5+hCy+8MGydPkMKltWDRu9Zx44d7YcffnBtiKubuJ6rTZs27kqoAmtdJdNQOY/eRz121KhRNnbsWCtdurRr65VXXmk//fTTWbdT+/MCeC+RokWf9/iSeHPmzLGmTZvGuu/EiRP2xBNP2EUXXeT+DvQ8jz32WKzu51qvBLF64emKnz4/+pvR1e5Q6umodumKorbR57lBgwY2f/78sO30uVWbWrZsGVynbbStXne9nvqbUFtCqV0ahqheeWpvyZIlXV2oyPaKvjP0+dXnRVcnGzVqFKsXYbNmzdzf4OrVq0/7+gEAks4zzzzjkj8vvfRSWEJKdB5/8cUXXW9rbRff+Tq0dEBc3/9ebKBeL7fddpurJxRXbcuV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8MILbnjcVVddZUWLFg0u06dPD24zduxYu/76661du3bWqFEjNxRv5syZwfszZcrkhv7pp5JVd955p3Xo0MGGDh0a3EY9sJSAUu+o6tWr2+jRo23y5MluBj4AAAAAAABEWU8pDd87k+zZs9uECRPccjqlS5e2Tz/9NN79KPH1/fffJ6qdAAAAAAAASIeFzgEAAAAAABBdSEoBAAAAAADAdySlAAAAAAAA4DuSUgAAAAAAAPAdSSkAAAAAAAD4jqQUAAAAAAAAfEdSCgAAAAAAAL4jKQUAAAAAAADfkZQCAAAAAACA70hKAQAAAAAAwHckpQAAAAAAAOA7klIAAAAAAADwHUkpAAAAAAAA+I6kFAAAAAAAAHxHUgoAAAAAAAC+y5zQDQ8cOJDgnebOnTux7QEAAAAAAEAUSHBSKm/evJYhQ4YEbXvy5MlzaRMAAAAAAADSuQQnpb744ovg71u3brVHH33U7r77bqtXr55bt2zZMnv99ddtxIgRydNSAAAAAAAARF9S6sorrwz+PnToUBszZozdfvvtwXWtW7e2qlWr2ksvvWQdO3ZM+pYCAAAAAAAgugudq1dUnTp1Yq3XuuXLlydFuwAAAAAAAJCOJSopVbJkSXv55ZdjrZ88ebK7DwAAAAAAAEjypNTYsWPtueeec8P1unbt6pZq1aq5dboPAAAAyeePP/6wO++80woUKGA5cuRwMdmKFSuC9wcCARs0aJAVLVrU3d+0aVPbtGlT2D7++ecfa9++vZs1WRPadOnSxQ4ePBi2zY8//mgNGza07NmzuwuPI0eO9O0YAQBA+peopNR1113nAptWrVq5gEaLfv/555/dfQAAAEgee/futSuuuMKyZMlin332ma1bt85Gjx5t+fLlC26j5NGzzz5rkyZNsm+//dbOO+88a9GihR09ejS4jRJSa9eutfnz59vs2bPtq6++su7duwfvP3DggDVv3txKly5tK1eutGeeecYef/xxVz8UAADA10LnkUqUKGFPPvlkkjQCAAAACfP000+7XkuvvfZacF2ZMmXCekmNGzfOBgwYYDfccINb98Ybb1jhwoVt1qxZdtttt9n69ettzpw59t133wXrhKrHuy4ujho1yooVK2ZTp06148eP26uvvmpZs2a1ypUr2+rVq91kN6HJKwAAAF97Ssm+ffts3rx59tZbb7lAJ3QBAABA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9WqrX7++W7dkyRKbMmWKjR8/PqWbBwAAkPqTUs8995y9/PLL1qZNG3vqqaeC6+vUqWMPPfRQUrYPAAAg3bj33nutSJEibmKYGTNmuHUVK1a06dOnc1EPAABEnUQN39uyZYvVrFkz1vps2bLZoUOHEryfr776ytVV0BDADBky2KxZs8Luv/vuu9360OWaa64J2+aff/6x9u3bW+7cuS1v3rzWpUsXO3jwYNg2KsresGFDy549u+sarwKjAAAAKaFt27a2ePFi+/vvv92i30lIAQCAaJSopFSZMmVct/NIc+bMcVf7EkoJrOrVq9uECRNOu42SUDt37gwub7/9dtj9SkitXbvW5s+fb7Nnz3aJrtAC7AcOHLDmzZtb6dKlbeXKlfbMM8/Y448/bi+99FKC2wkAAJAUvvvuO/v2229jrde6FStWpEibAAAA0tTwvT59+liPHj3s6NGjFggEbPny5S5ZNGLECJs8eXKC93Pttde6JT7qfaVu7nFZv369S4QpwNPQQW9o4XXXXWejRo1yPbCmTp1qx48ft1dffdWyZs1qlStXdgk1Tb3M7IEAAMBPip/69etndevWDVv/xx9/2NNPPx1nwgoAACC9SlRPqa5du7rAacCAAXb48GG74447XPFzFei87bbbkrSBX375pRUqVMjKly/v6jCom7tn2bJlbsiel5CSpk2bWsaMGYNBnbZp1KiRS0h5WrRoYRs3brS9e/fG+ZzHjh1zPaxCFwAAgHO1bt06q1WrVqz1Koug+wAAAKJJopJS3rC5TZs2ufpNu3btst9//93Vc0pKGrr3xhtvuKmTlQRbtGiR61nlzfan51XCKlTmzJktf/787j5vm8KFC4dt4932tomkHl958uQJLkzRDAAAkoJ6gO/evTvWepUoUAwDAAAQTRKVlDpy5IjrISU5c+Z0t8eNG2fz5s1L0sap11Xr1q2tatWqbqY/1YzSUD31nkpOMTExtn///uCyffv2ZH0+AAAQHVTn0oszPPv27bPHHnvMmjVrlqJtAwAASBNJKc0Qox5MXiB12WWXuamNtV7D+JJL2bJlrWDBgvbLL7+426o1tWfPnrBtTpw44Wbk8+pQ6WfkFUnv9ulqVekqpmbzC10AAADOlWpe6mKXJmBp3LixWzSBjHpvK5YCAACIJolKSq1atcoaNmzofn/vvfdccue3335ziapnn33WkouGCKqmVNGiRd3tevXquaSYZtXzfP7553bq1KlgAVFtoxn5/vvvv+A2mqlPNary5cuXbG0FAACIVLx4cfvxxx9t5MiRVqlSJatdu7aryblmzRrKBQAAgKiTqOIFGrqXK1cu97uG7N14442uuPjll1/uklMJpXpUXq8n2bJli5sZTzWhtAwZMsTatWvnkl6//vqrm62mXLlyrlC5VKxY0dWd6tatm02aNMklnnr27OmG/WnmPVERdu1H9a4eeeQR++mnn1zwN3bs2MQcOgAAwDk577zzmAEYAAAgsT2llBiaNWuW634+d+5cVx9BNJTubIa6rVixws02o0X69Onjfh80aJBlypTJXUlUTalLLrnEJZV0NfHrr792w+s8U6dOtQoVKliTJk3suuuuswYNGthLL70UvF+FypU4U8JLj+/bt6/bP8EgAABICW+++aaLV3QBzbuYp4tlH374YUo3DQAAIPX3lFJSRz2Qevfu7ZJBGiInSv54CaaEuOqqqywQCJz2fiW8zkQ9qqZNmxbvNtWqVXPJLAAAgJSk2puKo3r16mXDhg0LziiskgKaNEb1OQEAAKJFonpK3XTTTbZt2zbX02nOnDnB9UpQMSwOAAAgbs8995y9/PLL1r9/f8uc+f9dG6xTp46rKwUAABBNzqqnVKlSpdxwOi1XX311rNnrNAsfAAAA4qZyAnH1KldpgkOHDqVImwAAANJETynVQFDQ1KNHDytYsKDdeuutrqaTZsADAABA/MqUKeMmdYmknueawAUAACCanFVS6sorr7TRo0fbpk2bbMmSJVajRg3XDV09ptRzSrUQNm/enHytBQAASMM0qYsu7k2fPt3V1Vy+fLkNHz7cYmJi3CzDAAAA0SRRhc6lcuXKblEQtXPnTvv444/d8thjj1nZsmXt6aeftpYtWyZtawEAANKwrl27Wo4cOWzAgAF2+PBhN3GMZuEbP3683XbbbSndPAAAgLSRlApVtGhR6969u1tUD0Gz8GmYHwAAAMK1b9/eLUpKHTx40AoVKpTSTQIAAEg7s++tWrUqbIaYDz/80Nq0aeN6SWXJksXatm1rTZs2Tcp2AgAApHlHjhxxySjJmTOnu63yB7qgBwAAEG0SlZT63//+Zz///LP7XTWk1N1cgdW7775LPQQAAIDTuOGGG+yNN95wv2uiGM1crHqdWv/CCy+k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PHgwpZsGAACQ+ntKaXieimyKElMKpMqXL++6gWt2FwAAAMT222+/2TXXXONqcqrWU7NmzVxMpeLnuj1p0qSUbiIAAEDq7imlmV1++OEH93vdunXdrC1LlixxvafKli2b1G0EAABIF1QCoU6dOrZ3717LkSNHcL1qdaosAgAAQDRJVE8pTRl86NAh97sSUddff701bNjQTUU8ffr0pG4jAABAuvD111/b0qVLLWvWrGHrL7zwQvvjjz9SrF0AAABpJikVOhVwuXLlbMOGDfbPP/9Yvnz5gjPwAQAAINypU6finKX4999/D5ZGAAAAiBZnPXzvv//+s8yZM9tPP/0Utj5//vwkpAAAAOLRvHlzGzduXPC2YicVOB88eLBdd911Kdo2AACAVN9TKkuWLFaqVKk4r/IBAADg9EaPHu16nFeqVMmOHj1qd9xxh23atMlNHPP222+ndPMAAABS//C9/v3722OPPWZvvvmm6yEFAACAMytRooSbLOadd96xH3/80fWS6tKli7Vv3z6s8DkAAEA0SNTse88//7x99dVXVqxYMStfvrzVqlUrbEko7aNVq1ZuP+q+PmvWrLD7A4GADRo0yIoWLeoCtaZNm7qriaFUy0qBXO7cuS1v3rwusFOAF0pBnwqxZ8+e3UqWLOlmCwQAAEgJKoNw5513unhk4sSJ1rVrVxJSAAAgKiWqp1SbNm2S5Mk1g1/16tWtc+fOduONN8a6X8Has88+a6+//rqVKVPGBg4c6Lq8r1u3ziWYRAmpnTt32vz58129q06dOln37t1t2rRp7v4DBw64+g1KaE2aNMnWrFnjnk8JLG0HAADglzfeeCPe+zt06OBbWwAAANJkUkrFOJPCtdde65a4qJeUCoEOGDDAbrjhhmAgV7hwYdej6rbbbrP169fbnDlz7LvvvrM6deq4bZ577jlXKHTUqFGuB9bUqVPt+PHj9uqrr7rplytXrmyrV6+2MWPGkJQCAAC+evDBB8Nu64La4cOHXYySM2dOklIAACCqJGr4nuzbt88mT55sMTExbgidrFq1yv74448kadiWLVts165droeTJ0+ePFa3bl1btmyZu62f6vHkJaRE22fMmNG+/fbb4DaNGjVywZ5Hva02btxoe/fuTZK2AgAAJIRij9BFJQcUkzRo0IBC5wAAIOokqqeUajQp+aMk0datW61bt26u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AAAAAADwHUkpAAAAAAAA+I6kFAAAAAAAAHxHUgoAAAAAAAC+IykFAAAAAAAA35GUAgAAAAAAgO9ISgEAAAAAAMB3JKUAAADSkBEjRtill15quXLlskKFClmbNm1s48aNYdscPXrUevToYQUKFLDzzz/f2rVrZ7t37w7bZtu2bdayZUvLmTOn28/DDz9sJ06cCNvmyy+/tFq1alm2bNmsXLlyNmXKFF+OEQAARAeSUgAAAGnIokWLXMLpm2++sfnz59t///1nzZs3t0OHDgW36d27t3388cf27rvvuu137NhhN954Y/D+kydPuoTU8ePHbenSpfb666+7hNOgQYOC22zZssVt07hxY1u9erX16tXLunbtanPnzvX9mAEAQPqUOaUbAAAAgISbM2dO2G0lk9TTaeXKldaoUSPbv3+/vfLKKzZt2jS7+uqr3TavvfaaVaxY0SWyLr/8cps3b56tW7fOFixYYIULF7YaNWrYE088YY888og9/vjjljVrVps0aZKVKVPGRo8e7fahxy9evNjGjh1rLVq0SJFjBwAA6Qs9pQAAANIwJaEkf/787qeSU+o91bRp0+A2FSpUsFKlStmyZcvcbf2sWrWqS0h5lGg6cOCArV27NrhN6D68bbx9AAAAnCt6SgEAAKRRp06dcsPqrrjiCqtSpYpbt2vXLtfTKW/evGHbKgGl+7xtQhNS3v3effFto8TVkSNHLEeOHLHac+zYMbd4tC0AAMDp0FMKAAAgjVJtqZ9++sneeecdSy1F2PPkyRNcSpYsmdJNAgAAqRhJKQAAgDSoZ8+eNnv2bPviiy+sRIkSwfVFihRxBcz37dsXtr1m39N93jaRs/F5t8+0Te7cuePsJSUxMTFuOKG3bN++PYmOFgAApEckpQAAANKQQCDgElIffPCBff75564YeajatWtblixZbOHChcF1GzdutG3btlm9evXcbf1cs2aN7dmzJ7iNZvJTwqlSpUrBbUL34W3j7SMu2bJlc/sIXQAAAE6HmlIAAABpbMieZtb78MMPLVeuXMEaUBoupx5M+tmlSxfr06ePK36uxND999/vkkmaeU+aN2/ukk933XWXjRw50u1jwIABbt9KLMk999xjzz//vPXr1886d+7sEmAzZsywTz75JEWPHwAApB/0lAIAAEhDXnjhBTc07qqrrrKiRYsGl+nTpwe3GTt2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FFIdBSVaNLZZAU1cbr75ZtuzZ4999tlntnLlSvfF3KRJE/vnn3/c/TNmzHBftk8++aStWLHCihYt6r6MzlbPnj1t2bJl7krEjz/+6J5XAdOmTZuC2xw+fNhGjRrlvjy/+uor27ZtmwvCRD9vueWWYJClpX79+rGe59SpU1aiRAl79913bd26dTZo0CB77LHH3HEA6V3nzp3ttddeC95+9dVXrVOnTue0z+XLl7ufCxYscH93oUFbQjzzzDNWvXp1+/77791/xh588EGbP39+2Db6jmnbtq2tWbPGHcMHH3zgtuvbt6/99NNPLnjScXzxxRfBv/Nrr73WDaF566233N+6/uOnq/7y66+/uu8K9XLQ98306dNdkKXvIdF3mYKsoUOH2saNG23OnDnWqFEjd5+OUUGg2rF+/Xr3H7cbb7zRAoHAOb2OANIWYihiKEQP4ifip3QjAKRC7733XiBfvnyB7NmzB+rXrx+IiYkJ/PDDD+6+r7/+OpA7d+7A0aNHwx5z0UUXBV588UX3e7169QL33Xdf2P1169YNVK9ePXj7yiuvDDz44INh29xwww2Bjh07ut9/++23QKZMmQJ//PFH2DZNmjRx7ZHXXntN31iBX375JXj/hAkTAoULFw7e1v6031Bbtmxxj/v+++9P+xr06NEj0K5du3j3A6Rl3md6z549gWzZsgW2bt3qFv3d//nnn2F/j6VLlw6MHTs27PH6ex48eHDwtv6mPvjgg3j/xhLyd6Tnuuaaa8LW3XrrrYFrr7027Ll69eoVto2+q7p16xa27uabbw5cd9117ve5c+cGMmbMGNi4cWOcz9ulS5dA9+7dw9bp+06POXLkSOD99993330HDhyI9diVK1e6Nun1AxDdiKGIoZC+ET+FI35K++gphVRJme4dO3bYRx995DLfylrrSp66equL+cGDB133Su+KoJYtW7a4TLko061u4qHq1at3Vm1Q9l5dwi+55JKw51m0aFHweSRnzpyuu6lHVxR1BfJsqXtr7dq1XXd6Pc9LL73krhgC6Z0+8y1btnR/37rip98LFiyY7M87derUsL9tDXk53feFbut7JVSdOnXCbuv+K664ImydbnuP01ATXc3Xd0pc9N2m1yC0TSpOrCuE+n5r1qyZG4qjGjF33XWXa796GYiuSqqng7qfqzfCyy+/bHv37j3HVwhAWkQMRQyF6ED89H+In9I+Cp0j1dKYX32JaBk4cKCrfzB48GC77777XNASWpfAo9oECZUxY8ZYXTNVbM+joE3dQtW13ese6tGXnSeyOJ/GSp9tl091bVc39dGjR7svb9VwUPfXb7/99qz2A6RV6jbtdbOOrD+QkL/XxFA9g9D/eKn2wdlQLYSz4RUiPR1956jLurqYR1KNCNVZWLVqlfvumzdvnhuioi7w3333nfvuU/f4pUuXuvuee+4569+/v/sO8epDAIgexFDEUIgOxE/ET+kBPaWQZlSqVMkVndPVvl27dlnmzJmtXLlyYYt3dUAFMyODkW+++SbW1QWvmKboip7GMXtU4E/rdMUu8nnOZtYXfRFqP/HRGGnVSVCwqOfVc4ReSQTSO13N12xRCpR0dStS5N/rgQMH3NWv+P7uJL6/Pf3HJfTvOjToify+0G19r8RH90dOua7b+u6SatWq2e+//24///xznI/Xd5vqJER+32jxjkffeyrAOXLkSFc3QQV/P//88+B/5nRlcciQIa6Wgx6jOg0AQAwFpE/ET8RP6QE9pZDqaMpidZ9U5l9fQvriU4E6fYnccMMN7gtFV8I0A4TWqSunuql/8sknrmieuoSqWJ5midDv+pJRN821a9e6bpsezTbTp08f9zh1HR8zZoyb2cGj/bZv3946dOjgrr4p0Pnzzz/dtMpql7rIJoRmotDMMiqsp+7ycU2pqtkg3njjDbedsvIq+KnsPRl6RAtdSfe6aUdeVff+XtU1u1WrVu6qlq5yxbWdp1ChQi5IUjFLdflWr4Gzmc5YwZC+X/Q9oytoKqCr74r4PPzww64or74r9D318ccfuwKhKhYqV155pSusqaE1+r5RsLRhwwYXDCmofOSRR+zyyy93VzzVq0FXEhVk6fmff/55N8vM5s2b3T7y5ctnn376qeuarllq9B9IfTc1b97cHbtu6/vqTIEggPSFGIoYCtGF+In4KV1I6aJWQCQV33z00UcDtWrVCuTJkyeQM2fOQPny5QMDBgwIHD582G2jQnX3339/oFixYoEsWbIESpYsGWjfvn1g27Ztwf0MHz48ULBgwcD555/vivP169cvrEjn8ePHA/fee28gf/78gUKFCgVGjBgRVhjQ22bQoEGBCy+80D1P0aJFA23btg38+OOPwSKdamMoFQoM/dNSEcJmzZq5dmj9F198EauIoI757rvvdvvKmzeva5deg9D2UqQT6c2ZPtOhf4/79+93xTJVqFJ/71OmTIm3UKe8/PLLblsVulRR3oQ8p1eoc8iQIa7Ipr5/ihQpEhg/fnzYNpHP5Zk4cWKgbNmy7vvikksuCbzxxhth9//999+BTp06BQoUKOAKklapUiUwe/bs4P3Lly8Pfl+cd955gWrVqrnvMq9op45DBYxz5Mjh7ps+fbq7b926dYEWLVoELrjgAlf0VM/93HPPxXucANIfYihiKKR/xE/ET+lNBv2T0okxwA8aO6wpklUsDwDiuzLfq1cvtwAAiKEAnBnxExKLmlIAAAAAAADwHUkpAAAAAAAA+I7hewAAAAAAAPAdPaUAAAAAAADgO5JSAAAAAAAA8B1JKQAAAAAAAPiOpBQAAAAAAAB8R1IKAAAAAAAAviMpBQAAAAAAAN+RlAIAAAAAAIDvSEoBAAAAAADAdySlAAAAAAAA4DuSUgAAAAAAAPAdSSkAAAAAAAD4jqQUAAAAAAAAfEdSCgAAAAAAAL4jKQUA8N2MGTMsf/78dvDgQd+e87///rOSJUvaxIkTfXtOAACAkSNHWoUKFezUqVO+Pefff/9t5513nn366ae+PSeQGCSlgGSydu1au/POO6148eKWLVs2K1asmLu9bt06Sy9uueUWy5Ahgz3yyCMp3ZRUZevWre51GTVqVJLsT0mUKVOmWHpx8uRJGzx4sN1///12/vnn+/a8WbJksT59+tjw4cPt6NGjvj0vACBlpNdY7O6773Zxhrfkzp3bqlevbqNHj7Zjx46FbTt//nxr0KCB5cyZ0/Lly2c33XSTi1MiXXjhhWH79JZ77rnnjO358ssv3bbvvfdekhzfk08+abNmzbL04sCBA/b000+7eDljRv/++12gQAHr2rWrDRw40LfnBBKDpBSQDGbOnGm1atWyhQsXWqdOnVxSoUuXLvb555+79R9++KGlhxPsxx9/7IKYt99+2wKBQEo3Kd1Kb0kpfW42btxo3bt39/259ff4119/2bRp03x/bgCAf9J7LKYk25tvvukWJXHU+/ihhx6yjh07BreZPXu2XXPNNS5R9dRTT1nfvn1t0aJFLkn1559/xtpnjRo1gvv0ls6dO/t8ZOkvKfXqq6/aiRMn7Pbbb/f9uZVUXLVqlfvcA6lV5pRuAJDe/Prrr3bXXXdZ2bJl7auvvrILLrggeN+DDz5oDRs2dFfpfvzxRytTpoyvbTt8+LC7UpYU3n//fdfjRSfaq6++2h3rlVdeaX5Sb5esWbP6etUJ5+61116zK664wl259lvevHmtefPmLsmXEoE2ACD5RUMsljlzZncMnvvuu8/q1q1r06dPtzFjxrheYeqZo9dgyZIlLl6SVq1auaScklTqWRVK5+XQfSLp4p7WrVtb9uzZfX/uihUrWpUqVVzco3gdSI34nxyQxJ555hkXcLz00kthQZAULFjQXnzxRVdHR9uFdsNWj6NIjz/+uOsOHemtt96y2rVrW44cOdyVsdtuu822b98ets1VV13lTkIrV660Ro0auQDosccec1fQ1A7V14mk/6yXL18+Qcc5depUa9asmTVu3Nid8HTbs2LFCtfu119/Pdbj5s6d6+7T1TvPH3/84RIEhQsXdlf+Kleu7JJdcXUNf+edd2zAgAEucNIxqcfWP//8464OVq1a1Q0HUzf2a6+91n744YdYz//bb7+5wEBj7AsVKmS9e/cOtknPEerbb791Vxjz5MnjnktJNwV2SRmkKEBQO3TclSpVshdeeCFsG30uNPxAVza9rvR6bz379u2zXr16uVpJ2ke5cuVcF/HQmgWhwwn1ubzooovctpdeeql99913sdq1YcMGNzRTn199xvSZ6N+/v7vviy++cPv64IMPYj1OvY9037Jly+JNJM6ZM8eaNm0a6z49tmfPnvbuu++610LPXa9ePVuzZo27X387Oj4FdXoNIocfbNq0ydq1a2dFihRx25QoUcL9bezfvz9sO31uFy9e7D43AID0J1pisVC6QOfFBzo/6hynYYpt27YNJqREw/wUtymeisvx48ft0KFDlhwUh9SvX98NK9PrptcvcsifXms9v2JIL+7Re5OYmFH1KzVkX/GA4oImTZrYL7/8Eqtdiveuu+46N7xR8WG1atVs/PjxwVhN+/r+++/j7NGVKVMm16bT2bJli0t+RsY9obHZhAkTXPJQnw+9//ocaQTCE0884dqu1+qGG26IFbco3m7RooX7LGkbJVjjuuCmuEe91BnVgFQrACBJFStWLHDhhRfGu43uL1GiRPB2x44dA6VLl4613eDBg3X2CFs3bNiwQIYMGQK33nprYOLEiYEhQ4YEChYs6Pa5d+/e4HZXXnlloEiRIoELLrggcP/99wdefPHFwKxZswLz5893+/z444/D9rtz585ApkyZAkOHDj3jMf7xxx+BjBkzBt588013W4/Jly9f4NixY8FtypYtG7juuutiPbZTp05u2+PHj7vbu3btcq9FyZIl3X5eeOGFQOvWrV0bx44dG3zcF1984dZVqlQpUKNGjcCYMWMCI0aMCBw6dCjw3XffBS666KLAo48+6o5T+ylevHggT548rq2egwcPunblyJHDbTtu3LjAZZddFqhevbrbt57Ds3DhwkDWrFkD9erVC4wePdq1pVq1am7dt99+G+/rs2XLFre/Z555Jt7tLr300sDdd9/t9v3cc88Fmjdv7h73/PPPB7f54IMP3OtToUIF93prmTdvnrtPx642FShQIPDYY48FJk2aFOjQoYP7fDz44IOx2lOzZs1AuXLlAk8//XRg5MiR7nOjfXvvhfzwww+B3Llzu33GxMS417Nfv36BqlWruvtPnTrl3qt27drFOh6933of4rN48WLXlo8++ijWfVqv49H+n3rqKbfoPSxVqpR7TfTe670YMGCAex8aN24cfKw+e2XKlHF/f/obmTx5svvb0Gu8devWONsQ+TcAAEgf0nsspraed955sda3bdvW7XfDhg2BHTt2uN8HDRoUazudG3Wfns+jY1d8pOfXfbqtOCkhvBjt3XffjXc7vd733XefO6crjlMMpsfNnj07uI3inGzZsgUaNmwYjHuWLl2aqJhRcU/t2rXdfY8//nggZ86c7jlDKaZSTKHj1XutfT7wwAOBpk2buvsPHDjgXpe+ffvGOh7FJVdffXW8x/zWW2+5tvz4449h673YTDGt9qPXw4tvLr/8chfX1a9fP/Dss8+69ujzphjas3v3bhdPX3LJJS7efPnllwP9+/cPVKxY8bRtWLNmTbxtBVIKSSkgCe3bt8996d9www3xbuedQHWiO5tASP+5VrAwfPjwsO10ksmcOXPYegVCeqwSFaFOnjzpTugKpELpZKgT3ubNm894nKNGjXInaK/9P//8s3suJVA8SmhkyZIl8M8//4QlDvLmzRvo3LlzcF2XLl0CRYsWDfz1119hz3Hbbbe5hMThw4fDAgwllbx1nqNHj7rjijzZK6gJDeyU0NA+FBB6jhw54hI+oUkpJV4uvvjiQIsWLdzvHj2vEh/NmjVLkqRU5HGInlPHGKpy5cru/Yz0xBNPuKBUr38oJdz0Odm2bVtYe5RoCn0/Pvzww1hBcaNGjQK5cuUK/Pbbb2H7DH0d9N7qtdXn3bNnzx73GdRnNj5KFp0uMNJ67Vft9SiA13oF9d7nzWuD1nvbfv/99wkKiMUL1JWcAwCkL9EQi3lJqT///NMtv/zyS+DJJ590j9XFHe85FHM1adIk7LGKt/RYtWvFihXB9a1atXLnRcVIr7zyiksKaRtdmEqqpFRk3KOLYlWqVImV2FH7dIyRzjZmVIIm9ILp+PHjw2KQEydOuLhO73toMjEy7rn99ttdojM01ly1apXb12uvvRbvMSvRpO3+/fffsPVebKaEZWg85cU3umD633//hbVBCSvFvKKYW9vpwuyZKKmnbadPn37GbYGUwPA9IAn9+++/7meuXLni3c6739v+bIp2aliWhlapWLO3aLjSxRdf7IZWhVK3ZhX3jOze3b59e/voo4/Cnl/D79SlOiG1FbRty5Ytg8eh51YX7NAhfLfeeqvrlq42e+bNm+eGm+k+UR5CtalU30C/hx6TuiNr2JWKM4ZSl3d1UY48Tq+ulOpcaQpcDeNT9/fQx2vYmIb9afieR925u3XrFra/1atXu6Fgd9xxh9uX1yZ1J1fXb9WnSIopfUOPQ8eq59AQwc2bN8cachYXDXNTXQx1Nw997dRFXK+D2hlKr7u29eixoucTFT3VY9T1u1SpUmGPDR260KFDB1c0NbTLvWpYqIjnmWpR6PWU0HaE0usbOnxC9TFEw/JC/6689V7bNcRSNBRTQzbi4z23XisAQPoSLbGYYhINTdSioe0aFqgh797wej3H//73P1foPSYmxsU1GkaodmuInhw5ciS4P7WlX79+bpiY4gCVDVAspvpUv//+uyWF0Lhn7969LtZRLBIZ68UlMTGjXvfQoYuRcY+G5Gl4ncogqOZkfHHPjh07wt5bvVc6HsUnZ4p7VP/rdLMN33zzzcEYJjS+UTylx4Wu1/vmDRX02qtyGHENAw1F3IPUjqQUkIQSGuDofp3sNAb8bCig0IlYQY8XiHjL+vXrbc+ePWHbKwETejIOPbkqEPECF82EpkBFRUHPRM+jk7gKVWtcvreojoFOjKrx5NUsqFChgktWePS7jtkrtKgkiJJUXs2H0MUL4CKPKa5ATcHh2LFj3eui4E/PoX1oDH9ockf1pFRPKbI2hIK5yNfZS4BFtmvy5MkuIZOQpNGZqD6VEkiqX6DgQvtXUCkJ2b/aqURbZBu9ugWRr11koskLUhQYhgZpqn8RH72vqkcVmoTU75dffnms1/J0TlfXILKNXqCmmllxrffars9Fnz593Puj918Bqmo0xPU6es8dV40QAEDaFg2xmHdRbf78+W7RBSXVIVJcodpEnqFDh7oZB0eOHGmXXHKJ1alTxyU6tE5OlygRvTaqu6kLTpE1NxNLcaJiBbVddbj0mqmWZkJinsTEjGeKe1QQPyFxj2oyFS1aNBj3KO7UzNNK4J0p+XkmiY17dBFTCbEhQ4a4z7DaovpXilEjEfcgtWP2PSAJ6YSh2U6UDImP7lfhQi9IOd1JQr1dQukkqG0/++wzV1gxUmRwEdmjyKMi0urZpCKdCor0U23R1bMz0baiQEVLJF3F8oID9cxRgUldmdFJW1fhNB2ud+XH622kq0GhUxiHUrHJMx2TCk0OHDjQXdlTUUgFOrpCqCtfienR5D1GBVA1PXJc4gvkEkKBkHoFKcGjq5AKPvQefPrppy7BlpB2axsFSrqyGRcFoKHi+sxIYgpf6nOjGYx09VQB0DfffGPPP//8GR+n4qZeUKW/gUina2NC2q5ZhFQMVdN8q1feAw88YCNGjHBtC30uL6A72/+IAABSv2iIxUTPHdekIaG0P12sUSz2888/u+Lgig3UE1xx0pkuJHmJkaSYGOTrr792PdVV8H3ixIkuyZMlSxaXSNFEKWeSmJgxqeIe7Uev2csvv+zaruSfek4lZKZCxT1K7CkJGlcCK7Fxjz6D6rGuGEdFzNVTXHGwYiGtC/0cEvcgtSMpBSQxdSvWrC6a3atBgwZxnpQ144Z6dYReudHVn0jq2RNKvXx0MlKvkMiEw9lSAKQ27Ny50wUDGo53uiFVHj23ttWMe5p6OJISQrqKFJqU0hUcJaoUCKkXlWan8ejqlk7QCvjOFFjFRydltemVV14JW6/XNPQEXLp0aTcTjY4jNPiMnIlFr7NoFr9zaVd8FEAomaNEXehVsshu//EFymqnZg9KqjZ6V1d/+umnM26r91GfH10p1JVeBZbesMz4KAkn6i6v2RKTmvapRTM0Ll261PXomzRpkg0bNiy4jZ5bNPsQACD9Sc+xWGIoBtMiirnU80nDwc50gc3rQR05g2FiKBZUDyklT9Sr3aOkVELinqSKGeOK9xT3nGmfeq+U8FH8poSk2qNe2WcT90QmzZKCep5pUeJRnyENC9XMil27dg1uQ9yD1I7he0ASe+ihh9yUrhrH79XP8ehK0z333OOSHT179gw7KarrcuhVPQUoXpduz4033uiunCjRE3mVR7cjny8+6rGkk756uyjoSMjVHl0ZUhCnpNNNN90Ua1FSQkkVXT3yTn5KEGjYnhZdFdMVMo+ORV2PFajElQhRV+2E0H4iXw/VW4qcolfBg9YpEeQ5evSou/IVSlcu9Z5oml4lfRLbrjO1WULbrc9AXMGZhvfFFSjrauqyZctcgBdJ2+vK3NlQgKX3R1Mrb9u2Ley+yNdXyb5rr73WXdlVIvKaa65J0BU4vba6cqtpjJOSEp6Rx6vPnq4ER3Zl1/AIffZVewMAkP6k51jsXCm20XH17ds37DWJ7BGmOkVPPfWUO2frwt+50mumYw19HsWUs2bNSlDck1QxY6hatWq55OK4ceNiPV/ke6uEkhb1PFMbdHEutObT6XixRlLHPer9FNlGr3d/XHGPehBWrlw5SdsAJBV6SgFJTF2h33jjDRdo6D/FGrevE55OvOrJo5OIrmCE1kbSie2RRx6xtm3buiFHKtSsMfa6AhdatFEBk3p8qGCl9temTRt31UhXQBQ0de/e3QViCU1AKJGg5I3qGenq3Jko+aCg4HTbqlt2//793fF5Vx+VqBo0aJC7OqbXwitI7lHAo0SWrtip4Li6sys40nEvWLAgQV3Gr7/+elc3QckyFQhds2aNa2toXQVRcKohZnpvFAB69QHUttArc2qjgg4lXXQC135VE0IJLbVVgayulJ2Jiosq6RVJ71vz5s1doKeruWqXkl9KjhUqVMgFi5GJHH0e9N7r86VtVJfr4Ycfdgk2Hb+GrWk7FT7V8av3mD4jZ9tV+9lnn3VXlRWo6fPkfXY/+eQTVwA+8qqhkpFeL7mE0GutY9d7q/csqXz++efuPxcqGKq/GyWo3nzzzWAQG0r1N9SDyhtKCABIX9JzLHY2dOFICRRdcFKvKJ17Z8yY4XrRhJ4bFUvomHRO12ui2Eu9bpT8UYkEFXFPCD3Xhg0bYq3XcDsdm8oV6Hg1FE71n1T7Ue9V5FBLxTNqq7bXUEy1SXFiUsSMoRTv6T1WLKaEjuI9xYY6hrVr18a66Ke4x3tvE5pAVCyqmlVqn4bXJZXXX3/dDSXU51WfSQ0PVBypGPW6666LFffoGKkphVQrReb8A6KAppu944473FT2GTNmdFOxZs+ePbB27do4t583b56bFlfTvZYvXz7w1ltvxZqG2PP+++8HGjRo4KbM1VKhQoVAjx49Ahs3bgybhrhy5crxtnHGjBlu/927dz/j8Wja3gIFCrgpguOjqXVr1qwZvL1p0yb3HFoWL14c52N2797t2l+yZMlAlixZ3GumKYxfeumlBE03rOlx+/bt66YJzpEjR+CKK64ILFu2zL0GWkJpmuWWLVu67TQNrx6n11P7/uabb8K2/f777wM33nijO+5s2bK5KYNvueWWwMKFC+N9Dbxpfk+3vPnmm267jz76yE3drM/FhRde6KZifvXVV9022odn165drs25cuVy94Uek6YY1vTB5cqVc5+dggULBurXrx8YNWqUe89C2/PMM8/EaqvW63MW6qeffgq0bdvWTSWttunzOHDgwFiP1TTL+fLlc9MwHzlyJJBQM2fOdNNWb9u2LVZb9DmI67WMbHvk50Hva+fOnQMXXXSRa3P+/PkDjRs3DixYsCDscZp2Wa/T5MmTE9xeAEDalN5iMU/Hjh3dc57Jt99+G2jUqJE7V+u4q1evHpg0aVLg1KlTYdutWLEi0KpVq0Dx4sXdsZ9//vnu2NS2hPDOyadbvv76a7fdK6+8Erj44otdTKXX67XXXovz9d2wYYNrt2I13afjTYqY0Ysp9LyhFJ82a9bMxVl6XRWbPffcc7GOc+fOnYFMmTIFLrnkksDZGDNmjHtNDx8+fNbxjUdt1vrvvvvO3V61alXg9ttvD5QqVcq9noUKFQpcf/317r0MtX79eve4yHgISE0y6J+UTowB0UBX7NSbRVdW9HtqoILQusKnWVu8aXKjkbptq2i7inarRxQSRr2RdAVTV98i63nFR133dXVTww8T2sMqKd9rzUKkQvOnKz4LAEifiMVwLjRxj3pSaQSAJthJKA0LVY8pxR/ezId+0aQ/+mx5pQuA1IiaUoBP1OVXM4FpSNFjjz1mqYG6+eokGVcR0PRKRblDaXidiqFqamcSUmdHdSBUw0Gf7bOhIXUauqdu+3HV7Eouqo+hoQAqgk5CCgCiD7EYzsWUKVPchbW77rrrrB6nek6aKVmzOidmVujEUn0zlaPQ0EwSUkjN6CkFRCHVUdD4fQVm48ePd7UTooXqRGm2O9UO0JUr1VtQ3QDVllKNA5zZt99+6z4/6uWkmlWhtTYAAMCZRXMsltaobqVmb1bvKBV9nzlzZko3CUhXSEoBUUhXS1TwUkXIJ02alKDZQ9ILDd/SVSMVJ/WGkenqlV4LJIyGPiiZp8SerhqqgCcAAEi4aI7F0pqrrrrKli5d6iZJUfxDz3ogaZGUAgAAAAAAgO+oKQUAAAAAAADfkZQCAAAAAACA70hKAQAAAAAAwHdU1EsATd25Y8cOy5UrF9NpAgAQZVR+899//7VixYpZxoxczzsbxFAAAESnQALjJ5JSCaBgqmTJkindDAAAkIK2b99uJUqUSOlmpCnEUAAARLftZ4ifSEolgK7ueS9m7ty5U7o5AADARwcOHHCJFS8eQMIRQwEAEJ0OJDB+IimVAF53cwVTBFQAAEQnhp+dPWIoAACiW4YzxE8URgAAAAAAAIDvSEoBAAAAAADAdySlAAAAAAAA4DuSUgAAAAAAAPAdSSkAAAAAAAD4jqQUAAAAAAAAfEdSCgAAAAAAAL4jKQUAAJDGffXVV9aqVSsrVqyYZciQwWbNmhXv9nfffbfbLnKpXLlycJvHH3881v0VKlTw4WgAAEC0ICkFAACQxh06dMiqV69uEyZMSND248ePt507dwaX7du3W/78+e3mm28O205JqtDtFi9enExHAAAAolHmlG4AAAAAzs21117rloTKkyePWzzqWbV3717r1KlT2HaZM2e2IkWKJGlbAQAAPPSUAgAAiHKvvPKKNW3a1EqXLh22ftOmTW5IYNmyZa19+/a2bdu2FGsjAABIf+gpBQAAEMV27Nhhn332mU2bNi1sfd26dW3KlClWvnx5N3RvyJAh1rBhQ/vpp58sV65cce7r2LFjbvEcOHAg2dsPAADSLpJSAAAAUez111+3vHnzWps2bcLWhw4HrFatmktSqSfVjBkzrEuXLnHua8SIES55BQAAkBAkpVKJ/QRwQKLkGTw4pZsAAGlWIBCwV1991e666y7LmjVrvNsqcXXJJZfYL7/8ctptYmJirE+fPmE9pUqWLJmkbQaASDM37kzpJgBp1o3li6bo81NTCgAAIEotWrTIJZlO1/Mp1MGDB+3XX3+1okVPH7xmy5bNcufOHbYAAACcDkkpAACANE4Jo9WrV7tFtmzZ4n73CpOrB1OHDh3iLHCuYXlVqlSJdd9DDz3kklZbt261pUuXWtu2bS1Tpkx2++23+3BEAAAgGjB8DwAAII1bsWKFNW7cOHjbG0LXsWNHV6xchcojZ87bv3+/vf/++zZ+/Pg49/n777+7BNTff/9tF1xwgTVo0MC++eYb9zsAAEBSICkFAACQxl111VWuPtTpKDEVKU+ePHb48OHTPuadd95JsvYBAADEheF7AAAAAAAA8B1JKQAAAAAAAPiOpBQAAAAAAAB8R00pAAAApEn7hwxJ6SYAaVaewYNTugkAQE8pAAAAAAAA+I+kFAAAAAAAAHxHUgoAAAAAAAC+IykFAAAAAAAA35GUAgAAAAAAgO9ISgEAAAAAAMB3JKUAAAAAAADgO5JSAAAAAAAA8F1m/58SAHA6MzfuTOkmAGnSjeWLpnQTAAAAcJboKQUAAAAAAADfkZQCAAAAAACA70hKAQAAAAAAwHckpQAAAAAAAOA7klIAAAAAAADwHUkpAAAAAAAA+I6kFAAAAAAAAHxHUgoAAAAAAAC+S3NJqa+++spatWplxYoVswwZMtisWbPO+Jgvv/zSatWqZdmyZbNy5crZlClTfGkrAAAAAAAA0klS6tChQ1a9enWbMGFCgrbfsmWLtWzZ0ho3bmyrV6+2Xr16WdeuXW3u3LnJ3lYAAAAAAADELbOlMddee61bEmrSpElWpkwZGz16tLtdsWJFW7x4sY0dO9ZatGiRjC0FAAAAAABAuukpdbaWLVtmTZs2DVunZJTWn86xY8fswIEDYQsAAAAAAACSTrpPSu3atcsKFy4ctk63lWg6cuRInI8ZMWKE5cmTJ7iULFnSp9YCAAAAAABEh3SflEqMmJgY279/f3DZvn17SjcJAAAAAAAgXUlzNaXOVpEiRWz37t1h63Q7d+7cliNHjjgfo1n6tAAAAAAAACB5pPueUvXq1bOFCxeGrZs/f75bDwAAAAAAgJSR5pJSBw8etNWrV7tFtmzZ4n7ftm1bcOhdhw4dgtvfc889tnnzZuvXr59t2LDBJk6caDNmzLDevXun2DEAAAAAAABEuzSXlFqxYoXVrFnTLdKnTx/3+6BBg9ztnTt3BhNUUqZMGfvkk09c76jq1avb6NGjbfLkyW4GPgAAAAAAAKSMNFdT6qqrrrJAIHDa+6dMmRLnY77//vtkbhkAAAAAAADSbU8pAAAAAAAApH0kpQAAAAAAAOA7klIAAAAAAADwHUkpAACANO6rr76yVq1aWbFixSxDhgw2a9aseLf/8ssv3XaRy65du8K2mzBhgl144YWWPXt2q1u3ri1fvjyZjwQAAEQTklIAAABp3KFDh9wsw0oinY2NGze6mYu9pVChQsH7pk+f7mY5Hjx4sK1atcrtX7MX79mzJxmOAAAARKM0N/seAAAAwl177bVuOVtKQuXNmzfO+8aMGWPdunWzTp06uduTJk2yTz75xF599VV79NFHz7nNAAAA9JQCAACIUjVq1LCiRYtas2bNbMmSJcH1x48ft5UrV1rTpk2D6zJmzOhuL1u27LT7O3bsmB04cCBsAQAAOB2SUgAAAFFGiSj1fHr//ffdUrJkSbvqqqvcMD3566+/7OTJk1a4cOGwx+l2ZN2pUCNGjLA8efIEF+0XAADgdBi+BwAAEGXKly/vFk/9+vXt119/tbFjx9qbb76Z6P3GxMS4OlQe9ZQiMQUAAE6HpBQAAADssssus8WLF7vfCxYsaJkyZbLdu3eHbaPbRYoUOe0+smXL5hYAAICEYPgeAAAAbPXq1W5Yn2TNmtVq165tCxcuDN5/6tQpd7tevXop2EoAAJCe0FMKAAAgjTt48KD98ssvwdtbtmxxSab8+fNbqVKl3LC6P/74w9544w13/7hx46xMmTJWuXJlO3r0qE2ePNk+//xzmzdvXnAfGobXsWNHq1OnjutFpcccOnQoOBsfAADAuSIpBQAAkMatWLHCGjduHLzt1XVSUmnKlCm2c+dO27ZtW9jsen379nWJqpw5c1q1atVswYIFYfu49dZb7c8//7RBgwa54uaaqW/OnDmxip8DAAAkFkkpAACANE4z5wUCgdPer8RUqH79+rnlTHr27OkWAACA5EBNKQAAAAAAAPiOpBQAAAAAAAB8R1IKAAAAAAAAviMpBQAAAAAAAN+RlAIAAAAAAIDvSEoBAAAAAADAdySlAAAAAAAA4DuSUgAAAAAAAPAdSSkAAAAAAAD4jqQUAAAAAAAAfEdSCgAAAAAAAL4jKQUAAAAAAADfkZQCAAAAAACA70hKAQAAAAAAwHckpQAAAAAAAOA7klIAAAAAAADwHUkpAAAAAAAA+I6kFAAAAAAAAHxHUgoAAAAAAAC+IykFAAAAAAAA35GUAgAAAAAAgO9ISgEAAAAAAMB3JKUAAAAAAADgO5JSAAAAAAAA8B1JKQAAAAAAAPiOpBQAAAAAAAB8R1IKAAAAAAAAviMpBQAAAAAAAN+RlAIAAAAAAIDvSEoBAAAAAADAdySlAAAAAAAA4DuSUgAAAAAAAPBdmk1KTZgwwS688ELLnj271a1b15YvXx7v9uPGjbPy5ctbjhw5rGTJkta7d287evSob+0FAAAAAABAGk9KTZ8+3fr06WODBw+2VatWWfXq1a1Fixa2Z8+eOLefNm2aPfroo2779evX2yuvvOL28dhjj/nedgAAAAAAAKTRpNSYMWOsW7du1qlTJ6tUqZJNmjTJcubMaa+++mqc2y9dutSuuOIKu+OOO1zvqubNm9vtt99+xt5VAAAAacFXX31lrVq1smLFilmGDBls1qxZ8W4/c+ZMa9asmV1wwQWWO3duq1evns2dOzdsm8cff9ztK3SpUKFCMh8JAACIJmkuKXX8+HFbuXKlNW3aNLguY8aM7vayZcvifEz9+vXdY7wk1ObNm+3TTz+16667Ls7tjx07ZgcOHAhbAAAAUqtDhw65nuMqb5DQJJaSUoqHFCM1btzYJbW+//77sO0qV65sO3fuDC6LFy9OpiMAAADRKLOlMX/99ZedPHnSChcuHLZetzds2BDnY9RDSo9r0KCBBQIBO3HihN1zzz2nHb43YsQIGzJkSLK0HwAAIKlde+21bkko1doM9eSTT9qHH35oH3/8sdWsWTO4PnPmzFakSJEkbSsAAECa7SmVGF9++aULtiZOnOhqUKnL+ieffGJPPPFEnNvHxMTY/v37g8v27dt9bzMAAIBfTp06Zf/++6/lz58/bP2mTZvckMCyZcta+/btbdu2bfHuh97mAAAgXfeUKliwoGXKlMl2794dtl63T3clb+DAgXbXXXdZ165d3e2qVau6bu7du3e3/v37u+F/obJly+YWAACAaDBq1Cg7ePCg3XLLLcF1mt14ypQpbvZiDd1TL/KGDRvaTz/9ZLly5YpzP/Q2BwAA6bqnVNasWa127dq2cOHCsKt7uq0inXE5fPhwrMSTElui4XwAAADRSrMUK5E0Y8YMK1SoUHC9hgPefPPNVq1aNTfLsepP7du3z213OvQ2BwAA6bqnlPTp08c6duxoderUscsuu8zVRVDPJ83GJx06dLDixYu7q3Wiwp2asU81EnTV75dffnG9p7TeS04BAABEm3feecf1JH/33XfDJpGJS968ee2SSy5xcdTp0NscAACk+6TUrbfean/++acNGjTIdu3aZTVq1LA5c+YEi5+r3kFoz6gBAwa4aYz1848//nDTHyshNXz48BQ8CgAAgJTz9ttvW+fOnV1iqmXLlmfcXsP7fv31V1cSAQAAIGqTUtKzZ0+3nK6weSjNHDN48GC3AAAApDdKGIX2YNqyZYutXr3aFS4vVaqUG1anC3NvvPFGcMieep2PHz/e9SLXRT7JkSOH5cmTx/3+0EMPuYt4pUuXth07drg4Sj3Mb7/99hQ6SgAAkN6kuZpSAAAACLdixQpXpkCLV+pAv6tXuahQeejMeS+99JKdOHHCevToYUWLFg0uDz74YHCb33//3SWgVOhcBdALFChg33zzjetxDgAAENU9pQAAAPB/rrrqqngnb9EsevH1Ko+LhvUBAAAkJ3pKAQAAAAAAwHckpQAAAAAAAOA7klIAAAAAAADwHUkpAAAAAAAA+I6kFAAAAAAAAHxHUgoAAAAAAAC+IykFAAAAAAAA35GUAgAAAAAAgO9ISgEAAAAAAMB3JKUAAAAAAADgu8zJ/QSnTp2yRYsW2ddff22//fabHT582C644AKrWbOmNW3a1EqWLJncTQAAAEhViI8AAACSsafUkSNHbNiwYS6ouu666+yzzz6zffv2WaZMmeyXX36xwYMHW5kyZdx933zzTXI1AwAAINUgPgIAAPChp9Qll1xi9erVs5dfftmaNWtmWbJkibWNrgxOmzbNbrvtNuvfv79169YtuZoDAACQ4oiPAAAAfEhKzZs3zypWrBjvNqVLl7aYmBh76KGHbNu2bcnVFAAAgFSB+AgAAMCH4XtnCrhC6SrhRRddlFxNAQAASBWIjwAAAHyefW/OnDm2ePHi4O0JEyZYjRo17I477rC9e/f60QQAAIBUhfgIAABEO1+SUg8//LAdOHDA/b5mzRrr27evK+C5ZcsW69Onjx9NAAAASFWIjwAAQLRLtppSoRRcVapUyf3+/vvv2/XXX29PPvmkrVq1ygVfAAAA0Yb4CAAARDtfekplzZrVDh8+7H5fsGCBNW/e3P2eP3/+4BVCAACAaEJ8BAAAop0vPaUaNGjguqFfccUVtnz5cps+fbpb//PPP1uJEiX8aAIAAECqQnwEAACinS89pZ5//nnLnDmzvffee/bCCy9Y8eLF3frPPvvMrrnmGj+aAAAAkKoQHwEAgGjnS0+pUqVK2ezZs2OtHzt2rB9PDwAAkOoQHwEAgGjnS1LKs2fPHrecOnUqbH21atX8bAYAAECqQXwEAACilS9JqZUrV1rHjh1t/fr1FggE3LoMGTK43/Xz5MmTfjQDAAAg1SA+AgAA0c6XpFTnzp3tkksusVdeecUKFy7sAi0AAIBoRnwEAACinS9Jqc2bN9v7779v5cqV8+PpAAAAUj3iIwAAEO18mX2vSZMm9sMPP/jxVAAAAGkC8REAAIh2vvSUmjx5squZ8NNPP1mVKlUsS5YsYfe3bt3aj2YAAACkGsRHAAAg2vmSlFq2bJktWbLEPvvss1j3UcgTAABEI+IjAAAQ7XwZvnf//ffbnXfeaTt37nTTHYcuBFwAACAaER8BAIBo50tS6u+//7bevXu7mWUAAABAfAQAAOBLUurGG2+0L774wo+nAgAASBOIjwAAQLTzpabUJZdcYjExMbZ48WKrWrVqrEKeDzzwgB/NAAAASDWIjwAAQLTzbfa9888/3xYtWuSWyEKeBF0AACDaEB8BAIBo58vwvS1btpx22bx5sx9NAAAASFWSMj766quvrFWrVlasWDGX0Jo1a9YZH/Pll19arVq1LFu2bFauXDmbMmVKrG0mTJhgF154oWXPnt3q1q1ry5cvP6t2AQAApHhSCgAAAMnn0KFDVr16dZdESgglvlq2bGmNGze21atXW69evaxr1642d+7c4DbTp0+3Pn362ODBg23VqlVu/y1atLA9e/Yk45EAAIBokmxJqaeeesqOHDmSoG2//fZb++STT5KrKQAAAKlCcsVH1157rQ0bNszatm2boO0nTZpkZcqUsdGjR1vFihWtZ8+edtNNN9nYsWOD24wZM8a6detmnTp1skqVKrnH5MyZ01599dUEPQcAAECKJaXWrVtnpUqVsvvuu88+++wz+/PPP4P3nThxwn788UebOHGi1a9f32699VbLlStXcjUFAAAgVUgt8dGyZcusadOmYevUC0rr5fjx47Zy5cqwbTJmzOhue9sAAACk2kLnb7zxhv3www/2/PPP2x133GEHDhywTJkyuboFhw8fdtvUrFnTdRW/++67Xa0CAACA9Cy1xEe7du2ywoULh63TbbVHPbn27t1rJ0+ejHObDRs2nHa/x44dc4tH+wMAAEiR2fdUe+Dll1+2F1980V35++2331ygU7BgQatRo4b7CQAAEE3Sc3w0YsQIGzJkSEo3AwAApBHJmpQK7e6tIEsLAAAAUjY+KlKkiO3evTtsnW7nzp3bcuTI4XpvaYlrGz32dGJiYlxx9NCeUiVLlkyGIwAAAOkBs+8BAABEmXr16tnChQvD1s2fP9+tl6xZs1rt2rXDtjl16pS77W0TFw1DVGIrdAEAADgdklIAAABp3MGDB2316tVukS1btrjft23bFuzB1KFDh+D299xzj23evNn69evnakSpuPqMGTOsd+/ewW3U40nDDF9//XVbv3693XvvvXbo0CE3Gx8AAECaGb4HAACA5LNixQpr3Lhx8LY3hK5jx442ZcoU27lzZzBBJWXKlLFPPvnEJaHGjx9vJUqUsMmTJ7sZ+Dya/U+zAw4aNMgVRtcwwzlz5sQqfg4AABB1PaUmTJhgF154oZuVpm7durZ8+fJ4t9+3b5/16NHDihYt6rqWX3LJJfbpp5/61l4AAIDkctVVV1kgEIi1KCEl+vnll1/Gesz333/vZsv79ddf3Wx/kXr27OkKsWubb7/91sVcAAAAaSop9dprrwWnOU4K06dPd1cABw8ebKtWrXKz2OjK3p49e+Lc/vjx49asWTPbunWrvffee7Zx40bXHb148eJJ1iYAAICUjI8AAADSGl+SUo8++qibqaVLly62dOnSc97fmDFjrFu3bq6mQaVKlWzSpEmWM2dOe/XVV+PcXuv/+ecfmzVrll1xxRWuh9WVV17pklkAAAApIanjIwAAgLTGl6TUH3/84Ypk/vXXX66reIUKFezpp5929QnOlno9rVy50po2bRo2pbJuL1u2LM7HfPTRR26mGA3fUx2EKlWq2JNPPmknT56Mc3t1UdcUxqELAABAao2PAAAA0iJfklKZM2e2tm3b2ocffmjbt293vZymTp1qpUqVstatW7v1mmY4IRS4KZkUWWRTt08XxGl2GQ3b0+NUR2rgwIE2evRoGzZsWJzbjxgxwvLkyRNcSpYsmYijBgAA8Cc+AgAASIt8L3Su5FGDBg1czyX1cFqzZo2bGeaiiy6KVYAzqSigK1SokL300ktWu3ZtN5tM//793bC/uGja5P379wcXBYoAAADpKT4CAACImqTU7t27bdSoUVa5cmXXRV1D4mbPnm1btmxx3ddvueUWF3ydScGCBS1Tpkxuf5H7V12GuGjGPc22p8d5Klas6HpWaThgJM3Olzt37rAFAAAgtcZHAAAAaZEvSalWrVq5IXCajlhd0xVkvf3228G6UOedd5717ds3QT2SsmbN6no7LVy4MKwnlG7r6mJcVNz8l19+CesC//PPP7tklfYHAADgt6SMjwAAANKizH48iYbOLVq06LRJI7ngggvcVcGE6NOnj7tqWKdOHbvsssts3LhxdujQITcbn3To0MGKFy/uakPJvffea88//7w9+OCDdv/999umTZtcofMHHnggiY4QAAAgZeMjAACAtMaXpNQrr7xyxm0yZMhgpUuXTtD+VBPqzz//tEGDBrkheDVq1LA5c+YEi59v27bN1WPw6Crk3LlzrXfv3latWjWXsFKC6pFHHjmHowIAAEg98REAAEBa40tSSj2SypUrF6tnknovaVidejqdrZ49e7olLnEVBNVVyG+++easnwcAACCtxEcAAABpiS81pd5//31X1ylS/fr17b333vOjCQAAAKkK8REAAIh2viSl/v77b8uTJ0+s9ZrV7q+//vKjCQAAAKkK8REAAIh2viSl1DVdNZ/+v/buBVrLqkwc+MNFOIDDRZHrkAheiKWAwoJwNE1JNFd5D12OIBpOGjM6jDccBRUNNUWsSApDZbKBbLSmi6iROJooE5i30lFHxBs3DUhIcMH3X3vPOufPBweEgpdzzvf7rfV2znvfH8tv95zn3e+zN/fQQw9Fjx49imgCAECdIj4CACpdITWl0mx5qf5TKk5+zDHH5G1z5syJ2267Tb0EAKAiiY8AgEpXSFLqvPPOi3Xr1sWNN94YEyZMyNu6d+8ed955ZwwfPryIJgAA1CniIwCg0hWSlEouvPDCvKSngS1atIg999yzqFsDANRJ4iMAoJIVlpSqts8++xR9SwCAOk18BABUokIKnS9dujTOOeec6NKlSzRt2jSaNGlStgAAVBrxEQBQ6QoZKXXuuefG4sWL45prronOnTtHo0aNirgtAECdJT4CACpdIUmpJ598Mp544ono169fEbcDAKjzxEcAQKUr5PW9bt26RalUKuJWAAD1gvgIAKh0hSSlJk+eHFdeeWUsWrSoiNsBANR54iMAoNIV8vresGHDYu3atdGzZ89o2bJl7LHHHmX7P/jggyKaAQBQZ4iPAIBK17SoJ4EAAPx/4iMAoNIVkpQaMWJEEbcB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KZB+6623onXr1ru7OcBupD+oXOkJXwqounTpsrubUueJoaimzwQSfUHlKm1n/FTvklLt27ePJk2axNKlS8u2p/VOnTrVek7aviPHN2/ePC+batu27V/dduqv1IHqRIFEf1CZ6uIIqV0ZG1X/TNvS7HubHtOvX7+ttkUMxeb0mUCiL6hMbbYjfqp3hc6bNWsW/fv3jzlz5pQ9hUvrgwcPrvWctH3T45NHH310q8cDAFRybLTffvvlxNSmx6Sn3WkWPvETALCz1LuRUkkaFj5ixIgYMGBADBw4MM8Os2bNmjzjTDJ8+PDo2rVrrmuQXHzxxXHUUUfFbbfdFieeeGLMnDkzfvvb38b3vve93fxJAADqXmyUXrW75JJL4oYbbogDDjggJ6muueaaPAT/5JNP3q2fFQBoOOplUmrYsGGxfPnyGDduXC62mYaRz549u6YY5+LFi/OsM9UOP/zw+OEPf5inNb7qqqtycJVmlzn44IN346egPkivIIwfP36LVxGAyqM/oNJio8svvzwnti644IJYuXJlHHHEEfmaVVVVu+UzUr/oM4FEX8AnaVSqq/MbAwAAANBg1buaUgAAAADUf5JSAAAAABROUgoAAACAwklKwW6waNGiPLPR7373u+0+59xzzzXjEWxD+k6lQs3b4nsEUL+JoWDnEj+xu0lKUSelGYQuvPDC+NSnPpVnaujUqVMMHTo0fvOb30R9U1sn3q1bt3jvvffMAElFS9+NFAh99atf3WLf1772tbwvHbMz/2i544474p577vmL2wxQ14mhoGETP9HQNN3dDYDanHbaabF+/fq49957o0ePHrF06dKYM2dOvP/++9EQNGnSJAeJUOnSHxczZ86M22+/PVq0aJG3ffTRR3mq+vQH1c7Wpk2b2BU+/vjj2GOPPXbJtQF2hBgKGj7xEw2JkVLUOStXrownnngibr755vjc5z4X++67bwwcODDGjh0bX/rSl2qO+cpXvhL77LNPtG7dOo455ph47rnnyq5z0003RceOHeNv/uZv4vzzz48rr7wy+vXrV7P/6KOPjksuuaTsnPQ0btMnC+vWrYtLL700unbtGq1atYpBgwbF3Llza/anJwZt27aNhx9+OD796U/HnnvuGccff3x+gpdce+21OSj86U9/mp86pCWdv/lTiA0bNuQ27rfffvn/WA466KD8RAIausMO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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "โšก Overall Benchmark Results:\n", " Total time (sequential): 209.38s\n", " Total time (multi-process): 200.74s\n", " Overall speedup: 1.0x faster with multiprocessing\n", " Data processed: 500,000 vectors loaded, 10,000 queries executed\n", " Peak QPS achieved: 8664 queries/second\n" ] } ], "source": [ "# Create performance visualization\n", "fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(12, 8))\n", "\n", "# Throughput comparisons\n", "loading_throughput = df[df['Operation'] == 'Data Loading']['Throughput']\n", "query_throughput = df[df['Operation'] == 'Vector Queries']['Throughput']\n", "\n", "ax1.bar(['Sequential', 'Multi-process'], loading_throughput, color=['lightcoral', 'lightblue'])\n", "ax1.set_title('Data Loading Throughput (ops/sec)')\n", "ax1.set_ylabel('Operations/second')\n", "\n", "ax2.bar(['Sequential', 'Multi-process'], query_throughput, color=['lightcoral', 'lightblue'])\n", "ax2.set_title('Query Throughput (QPS)')\n", "ax2.set_ylabel('Queries/second')\n", "\n", "# Latency comparisons (only for queries)\n", "query_avg_latency = df[df['Operation'] == 'Vector Queries']['Avg_Latency_ms']\n", "query_p95_latency = df[df['Operation'] == 'Vector Queries']['P95_Latency_ms']\n", "\n", "ax3.bar(['Sequential', 'Multi-process'], query_avg_latency, color=['lightcoral', 'lightblue'])\n", "ax3.set_title('Query Average Latency (ms)')\n", "ax3.set_ylabel('Latency (ms)')\n", "\n", "ax4.bar(['Sequential', 'Multi-process'], query_p95_latency, color=['lightcoral', 'lightblue'])\n", "ax4.set_title('Query P95 Latency (ms)')\n", "ax4.set_ylabel('Latency (ms)')\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "# Calculate overall performance gains\n", "total_sequential_time = sequential_load_stats['total_time'] + sequential_query_stats['total_time']\n", "total_multiprocess_time = multiprocess_load_stats['total_time'] + multiprocess_query_stats['total_time']\n", "overall_speedup = total_sequential_time / total_multiprocess_time\n", "\n", "print(f\"\\nโšก Overall Benchmark Results:\")\n", "print(f\" Total time (sequential): {total_sequential_time:.2f}s\")\n", "print(f\" Total time (multi-process): {total_multiprocess_time:.2f}s\")\n", "print(f\" Overall speedup: {overall_speedup:.1f}x faster with multiprocessing\")\n", "print(f\" Data processed: {config.data_size:,} vectors loaded, {config.query_count:,} queries executed\")\n", "print(f\" Peak QPS achieved: {multiprocess_query_stats['qps']:.0f} queries/second\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## ๐Ÿš€ Quick Summary & Best Practices\n", "\n", "**What did we learn?**\n", "- ๐Ÿงต Multi-process loading bypasses Python's GIL for true parallelism.\n", "- Query speedups depend on your Redis server's CPU & network.\n", "- Each process gets its own Redis connection for safe parallel access.\n", "\n", "**Metrics to watch:**\n", "- **QPS** (Queries/sec): Main throughput stat\n", "- **P95/P99 Latency**: Key for user experience\n", "- **Success Rate**: Should stay >99%\n", "- **Memory Usage**: Keep an eye on Redis RAM\n", "\n", "**Performance factors:**\n", "- More CPU cores = more workers = more speed (up to a point)\n", "- Network can bottleneck with big vectors\n", "- Redis needs enough RAM for all your data\n", "- Persistence (AOF/RDB) can slow down writes\n", " \n", "**Redis options:**\n", "- OSS: Free, single-threaded, good for dev/test\n", "- Enterprise/Cloud: Multi-threaded, clustering, auto-scaling ๐Ÿšฆ\n", " \n", "**Top tips:**\n", "1. Match worker count to CPU cores, then tune\n", "2. Batch ops for speed, but watch memory on client-side\n", "3. Pick the right index for the use case: FLAT (exact), HNSW (fast/approx)\n", "4. Higher vector dims = more RAM, slower queries\n", "5. Be aware of networking bottlenecks and serialization overhead\n", "\n", "**Next steps:**\n", "- Benchmark with your real data & queries\n", "- Benchmark in a production / cloud environment using VPC peering\n", "- Test concurrency that matches your app\n", "- Monitor memory, CPU, and QPS\n", "- Plan for growth ๐Ÿ“ˆ\n", "\n", "**More info:**\n", "- [RedisVL Docs](https://docs.redisvl.com)\n", "- [Redis Optimization](https://redis.io/docs/operate/oss_and_stack/management/optimization/)\n", "- [Benchmarking Blog](https://redis.io/blog/benchmarking-results-for-vector-databases/)\n", "\n", "---\n", "\n", "๐ŸŽ‰ **Congrats!** You're ready to benchmark and tune Redis query engine with RedisVL. Use these tips to get the best performance for your app!" ] } ], "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.11" } }, "nbformat": 4, "nbformat_minor": 4 }