From f0cea5fad7700c67a17386a116ed3dec98a59441 Mon Sep 17 00:00:00 2001 From: Delacrobix Date: Tue, 15 Jul 2025 12:23:59 -0500 Subject: [PATCH 1/2] blog content: building-elasticsearch-apis-with-fastapi-websockets --- .../README.md | 45 ++++ .../elasticsearch_bulk_request.txt | 52 +++++ .../index.html | 196 ++++++++++++++++++ .../inegst_data.py | 54 +++++ .../main.py | 136 ++++++++++++ .../products.ndjson | 25 +++ .../requirements.txt | 5 + 7 files changed, 513 insertions(+) create mode 100644 supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/README.md create mode 100644 supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/elasticsearch_bulk_request.txt create mode 100644 supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/index.html create mode 100644 supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/inegst_data.py create mode 100644 supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/main.py create mode 100644 supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/products.ndjson create mode 100644 supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/requirements.txt diff --git a/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/README.md b/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/README.md new file mode 100644 index 00000000..21a507bd --- /dev/null +++ b/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/README.md @@ -0,0 +1,45 @@ +# Quickstart + +Follow these steps to set up and run the FastAPI API: + +1. **Create a virtual environment:** + +```bash +python3 -m venv venv +``` + +2. **Activate the virtual environment:** + +- On macOS/Linux: + ```bash + source venv/bin/activate + ``` +- On Windows: + ```bash + venv\Scripts\activate + ``` + +3. **Install dependencies:** + +```bash +pip install -r requirements.txt +``` + +4. **Run the API with uvicorn:** + +```bash +uvicorn main:app --reload +``` + +The API will be available at [http://localhost:8000](http://localhost:8000) + +## Credentials + +When you run the API, you will be prompted to insert your Elasticsearch endpoint and API key using `getpass`. These credentials are required to connect to your Elasticsearch instance. + +At the terminal, you will see prompts like this to insert your credentials: + +``` +Insert the Elasticsearch endpoint here: +Insert the Elasticsearch API key here: +``` \ No newline at end of file diff --git a/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/elasticsearch_bulk_request.txt b/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/elasticsearch_bulk_request.txt new file mode 100644 index 00000000..10260731 --- /dev/null +++ b/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/elasticsearch_bulk_request.txt @@ -0,0 +1,52 @@ + +POST products/_bulk +{"index":{}} +{"product_name": "iPhone 15 Pro", "price": 999.99, "description": "Latest flagship smartphone with titanium design, A17 Pro chip, and advanced camera system"} +{"index":{}} +{"product_name": "MacBook Pro 14-inch", "price": 1999.99, "description": "Professional laptop with M3 chip, Liquid Retina XDR display, and up to 22-hour battery life"} +{"index":{}} +{"product_name": "AirPods Pro 2nd Gen", "price": 249.99, "description": "Wireless earbuds with active noise cancellation, spatial audio, and USB-C charging case"} +{"index":{}} +{"product_name": "iPad Air", "price": 599.99, "description": "Versatile tablet with M1 chip, 10.9-inch Liquid Retina display, and Apple Pencil support"} +{"index":{}} +{"product_name": "Apple Watch Series 9", "price": 399.99, "description": "Advanced smartwatch with health monitoring, fitness tracking, and always-on Retina display"} +{"index":{}} +{"product_name": "Samsung Galaxy S24 Ultra", "price": 1199.99, "description": "Premium Android smartphone with S Pen, 200MP camera, and AI-powered features"} +{"index":{}} +{"product_name": "Dell XPS 13", "price": 1299.99, "description": "Ultra-portable laptop with Intel Core i7, 13.4-inch InfinityEdge display, and premium build"} +{"index":{}} +{"product_name": "Sony WH-1000XM5", "price": 399.99, "description": "Premium noise-canceling headphones with 30-hour battery and exceptional audio quality"} +{"index":{}} +{"product_name": "Nintendo Switch OLED", "price": 349.99, "description": "Hybrid gaming console with vibrant OLED screen, enhanced audio, and portable design"} +{"index":{}} +{"product_name": "Kindle Paperwhite", "price": 139.99, "description": "E-reader with 6.8-inch glare-free display, waterproof design, and weeks of battery life"} +{"index":{}} +{"product_name": "Google Pixel 8 Pro", "price": 999.99, "description": "AI-powered smartphone with advanced computational photography and 7 years of updates"} +{"index":{}} +{"product_name": "Microsoft Surface Pro 9", "price": 1099.99, "description": "2-in-1 laptop tablet with Intel Core processors, detachable keyboard, and Surface Pen support"} +{"index":{}} +{"product_name": "Dyson V15 Detect", "price": 749.99, "description": "Powerful cordless vacuum with laser dust detection and intelligent suction adjustment"} +{"index":{}} +{"product_name": "Fitbit Versa 4", "price": 199.99, "description": "Health and fitness smartwatch with GPS, heart rate monitoring, and 6+ day battery"} +{"index":{}} +{"product_name": "Bose QuietComfort 45", "price": 329.99, "description": "Wireless noise-canceling headphones with balanced sound and 24-hour battery life"} +{"index":{}} +{"product_name": "Tesla Model Y", "price": 47190.00, "description": "Electric SUV with autopilot, 330-mile range, and minimalist interior design"} +{"index":{}} +{"product_name": "Instant Pot Duo 7-in-1", "price": 99.99, "description": "Multi-use pressure cooker that replaces 7 kitchen appliances with smart programming"} +{"index":{}} +{"product_name": "LG OLED C3 55-inch TV", "price": 1499.99, "description": "4K OLED smart TV with perfect blacks, vibrant colors, and gaming-optimized features"} +{"index":{}} +{"product_name": "Vitamix A3500", "price": 549.99, "description": "Professional-grade blender with preset programs, self-cleaning, and 10-year warranty"} +{"index":{}} +{"product_name": "Herman Miller Aeron Chair", "price": 1395.00, "description": "Ergonomic office chair with breathable mesh, lumbar support, and 12-year warranty"} +{"index":{}} +{"product_name": "Canon EOS R5", "price": 3899.99, "description": "Professional mirrorless camera with 45MP sensor, 8K video, and advanced autofocus"} +{"index":{}} +{"product_name": "Sonos Arc Soundbar", "price": 899.99, "description": "Premium soundbar with Dolby Atmos, voice control, and seamless music streaming"} +{"index":{}} +{"product_name": "Peloton Bike+", "price": 2495.00, "description": "Interactive exercise bike with rotating HD touchscreen and live fitness classes"} +{"index":{}} +{"product_name": "Roomba j7+", "price": 849.99, "description": "Smart robot vacuum with object avoidance, self-emptying base, and app control"} +{"index":{}} +{"product_name": "KitchenAid Stand Mixer", "price": 379.99, "description": "Iconic stand mixer with 10-speed control, tilt-head design, and multiple attachments"} diff --git a/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/index.html b/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/index.html new file mode 100644 index 00000000..cc17812b --- /dev/null +++ b/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/index.html @@ -0,0 +1,196 @@ + + + + + + TechStore - Product Search + + +
+

🛍️ TechStore - Find Your Perfect Product

+
+ +
+
+
+ Product Search +

+
+ + +

+
+
+ +
+ Live Notifications +

🟡 Connecting to live notifications...

+
+ +
+

Search Results

+
+ 🔍 Enter a search term above to find products +
+
+
+ + + +
+ 🔔 Live Search Activity +

+

+ +

+
+
+ + + + diff --git a/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/inegst_data.py b/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/inegst_data.py new file mode 100644 index 00000000..6c601296 --- /dev/null +++ b/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/inegst_data.py @@ -0,0 +1,54 @@ +import json +import os + +from elasticsearch import Elasticsearch + +es_client = Elasticsearch( + hosts=[os.environ["ELASTICSEARCH_ENDPOINT"]], + api_key=os.environ["ELASTICSEARCH_API_KEY"], +) + +PRODUCTS_INDEX = "products" + + +def create_products_index(): + try: + mapping = { + "mappings": { + "properties": { + "product_name": {"type": "text", "analyzer": "standard"}, + "price": {"type": "float"}, + "description": {"type": "text", "analyzer": "standard"}, + } + } + } + + es_client.indices.create(index=PRODUCTS_INDEX, body=mapping) + print(f"Index {PRODUCTS_INDEX} created successfully") + except Exception as e: + print(f"Error creating index: {e}") + + +def load_products_from_ndjson(): + try: + if not os.path.exists("products.ndjson"): + print("Error: products.ndjson file not found!") + return + + products_loaded = 0 + with open("products.ndjson", "r") as f: + for line in f: + if line.strip(): + product_data = json.loads(line.strip()) + es_client.index(index=PRODUCTS_INDEX, body=product_data) + products_loaded += 1 + + print(f"Successfully loaded {products_loaded} products into Elasticsearch") + + except Exception as e: + print(f"Error loading products: {e}") + + +if __name__ == "__main__": + create_products_index() + load_products_from_ndjson() diff --git a/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/main.py b/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/main.py new file mode 100644 index 00000000..b426e1e9 --- /dev/null +++ b/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/main.py @@ -0,0 +1,136 @@ +import json +import os +from datetime import datetime +from getpass import getpass +from typing import Dict, List + +import uvicorn +from elasticsearch import Elasticsearch +from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect +from fastapi.responses import FileResponse +from pydantic import BaseModel, Field + +app = FastAPI(title="Elasticsearch - FastAPI with websockets") + + +os.environ["ELASTICSEARCH_ENDPOINT"] = getpass( + "Insert the Elasticsearch endpoint here: " +) +os.environ["ELASTICSEARCH_API_KEY"] = getpass("Insert the Elasticsearch API key here: ") + +es_client = Elasticsearch( + hosts=[os.environ["ELASTICSEARCH_ENDPOINT"]], + api_key=os.environ["ELASTICSEARCH_API_KEY"], +) + +PRODUCTS_INDEX = "products" + + +class Product(BaseModel): + product_name: str + price: float + description: str + + +class SearchNotification(BaseModel): + session_id: str + query: str + results_count: int + timestamp: datetime = Field(default_factory=datetime.now) + + +class SearchResponse(BaseModel): + query: str + results: List[Dict] + total: int + + +# Store active WebSocket connections +connections: List[WebSocket] = [] + + +@app.websocket("/ws") +async def websocket_endpoint(websocket: WebSocket): + await websocket.accept() + connections.append(websocket) + print(f"Client connected. Total connections: {len(connections)}") + + try: + while True: + await websocket.receive_text() + + except WebSocketDisconnect: + connections.remove(websocket) + print(f"Client disconnected. Total connections: {len(connections)}") + + +@app.get("/search") +async def search_products(q: str, session_id: str = "unknown"): + # List of product names that should trigger a notification + NOTIFY_PRODUCTS = ["iPhone 15 Pro", "Kindle Paperwhite"] + + try: + query = { + "query": { + "bool": { + "should": [ + {"match": {"product_name": q}}, + {"match_phrase": {"description": q}}, + ], + "minimum_should_match": 1, + } + }, + "size": 20, + } + + response = es_client.search(index=PRODUCTS_INDEX, body=query) + + results = [] + notify_found = False + + for hit in response["hits"]["hits"]: + product = hit["_source"] + product["score"] = hit["_score"] + results.append(product) + + # Check if this product should trigger a notification + if product.get("product_name") in NOTIFY_PRODUCTS: + notify_found = True + + results_count = response["hits"]["total"]["value"] + + if notify_found: + notification = SearchNotification( + session_id=session_id, query=q, results_count=results_count + ) + + for connection in connections.copy(): + try: + await connection.send_text( + json.dumps( + { + "type": "search", + "session_id": session_id, + "query": q, + "results_count": results_count, + "timestamp": notification.timestamp.isoformat(), + } + ) + ) + except: + connections.remove(connection) + + return SearchResponse(query=q, results=results, total=results_count) + + except Exception as e: + status_code = getattr(e, "status_code", 500) + return HTTPException(status_code=status_code, detail=str(e)) + + +@app.get("/") +async def get_main_page(): + return FileResponse("index.html") + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/products.ndjson b/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/products.ndjson new file mode 100644 index 00000000..605af749 --- /dev/null +++ b/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/products.ndjson @@ -0,0 +1,25 @@ +{"product_name": "iPhone 15 Pro", "price": 999.99, "description": "Latest flagship smartphone with titanium design, A17 Pro chip, and advanced camera system"} +{"product_name": "MacBook Pro 14-inch", "price": 1999.99, "description": "Professional laptop with M3 chip, Liquid Retina XDR display, and up to 22-hour battery life"} +{"product_name": "AirPods Pro 2nd Gen", "price": 249.99, "description": "Wireless earbuds with active noise cancellation, spatial audio, and USB-C charging case"} +{"product_name": "iPad Air", "price": 599.99, "description": "Versatile tablet with M1 chip, 10.9-inch Liquid Retina display, and Apple Pencil support"} +{"product_name": "Apple Watch Series 9", "price": 399.99, "description": "Advanced smartwatch with health monitoring, fitness tracking, and always-on Retina display"} +{"product_name": "Samsung Galaxy S24 Ultra", "price": 1199.99, "description": "Premium Android smartphone with S Pen, 200MP camera, and AI-powered features"} +{"product_name": "Dell XPS 13", "price": 1299.99, "description": "Ultra-portable laptop with Intel Core i7, 13.4-inch InfinityEdge display, and premium build"} +{"product_name": "Sony WH-1000XM5", "price": 399.99, "description": "Premium noise-canceling headphones with 30-hour battery and exceptional audio quality"} +{"product_name": "Nintendo Switch OLED", "price": 349.99, "description": "Hybrid gaming console with vibrant OLED screen, enhanced audio, and portable design"} +{"product_name": "Kindle Paperwhite", "price": 139.99, "description": "E-reader with 6.8-inch glare-free display, waterproof design, and weeks of battery life"} +{"product_name": "Google Pixel 8 Pro", "price": 999.99, "description": "AI-powered smartphone with advanced computational photography and 7 years of updates"} +{"product_name": "Microsoft Surface Pro 9", "price": 1099.99, "description": "2-in-1 laptop tablet with Intel Core processors, detachable keyboard, and Surface Pen support"} +{"product_name": "Dyson V15 Detect", "price": 749.99, "description": "Powerful cordless vacuum with laser dust detection and intelligent suction adjustment"} +{"product_name": "Fitbit Versa 4", "price": 199.99, "description": "Health and fitness smartwatch with GPS, heart rate monitoring, and 6+ day battery"} +{"product_name": "Bose QuietComfort 45", "price": 329.99, "description": "Wireless noise-canceling headphones with balanced sound and 24-hour battery life"} +{"product_name": "Tesla Model Y", "price": 47190.00, "description": "Electric SUV with autopilot, 330-mile range, and minimalist interior design"} +{"product_name": "Instant Pot Duo 7-in-1", "price": 99.99, "description": "Multi-use pressure cooker that replaces 7 kitchen appliances with smart programming"} +{"product_name": "LG OLED C3 55-inch TV", "price": 1499.99, "description": "4K OLED smart TV with perfect blacks, vibrant colors, and gaming-optimized features"} +{"product_name": "Vitamix A3500", "price": 549.99, "description": "Professional-grade blender with preset programs, self-cleaning, and 10-year warranty"} +{"product_name": "Herman Miller Aeron Chair", "price": 1395.00, "description": "Ergonomic office chair with breathable mesh, lumbar support, and 12-year warranty"} +{"product_name": "Canon EOS R5", "price": 3899.99, "description": "Professional mirrorless camera with 45MP sensor, 8K video, and advanced autofocus"} +{"product_name": "Sonos Arc Soundbar", "price": 899.99, "description": "Premium soundbar with Dolby Atmos, voice control, and seamless music streaming"} +{"product_name": "Peloton Bike+", "price": 2495.00, "description": "Interactive exercise bike with rotating HD touchscreen and live fitness classes"} +{"product_name": "Roomba j7+", "price": 849.99, "description": "Smart robot vacuum with object avoidance, self-emptying base, and app control"} +{"product_name": "KitchenAid Stand Mixer", "price": 379.99, "description": "Iconic stand mixer with 10-speed control, tilt-head design, and multiple attachments"} \ No newline at end of file diff --git a/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/requirements.txt b/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/requirements.txt new file mode 100644 index 00000000..ebeb6519 --- /dev/null +++ b/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.104.1 +uvicorn==0.24.0 +websockets==11.0.3 +pydantic==2.5.0 +elasticsearch==8.11.0 \ No newline at end of file From 6dfa7fba816c60647e0a70b66141f122c9783ca6 Mon Sep 17 00:00:00 2001 From: Delacrobix Date: Thu, 17 Jul 2025 11:05:17 -0500 Subject: [PATCH 2/2] code update --- .../README.md | 2 +- .../index.html | 5 +---- .../main.py | 21 +++++++------------ 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/README.md b/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/README.md index 21a507bd..825f761a 100644 --- a/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/README.md +++ b/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/README.md @@ -31,7 +31,7 @@ pip install -r requirements.txt uvicorn main:app --reload ``` -The API will be available at [http://localhost:8000](http://localhost:8000) +The API will be available at [http://localhost:8000](http://localhost:8000) ## Credentials diff --git a/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/index.html b/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/index.html index cc17812b..273f624c 100644 --- a/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/index.html +++ b/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/index.html @@ -102,10 +102,7 @@

Search Results

const dialog = document.getElementById("notificationDialog"); const messageElement = document.getElementById("notificationMessage"); - messageElement.innerHTML = ` -

Someone is searching for: "${notification.query}"

-

Found ${notification.results_count} products

- `; + messageElement.innerHTML = `

Hot search alert! Other users are looking for "${notification.query}" right now.

`; dialog.showModal(); } diff --git a/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/main.py b/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/main.py index b426e1e9..cb9fffe1 100644 --- a/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/main.py +++ b/supporting-blog-content/building-elasticsearch-apis-with-fastapi-websockets/main.py @@ -35,7 +35,6 @@ class Product(BaseModel): class SearchNotification(BaseModel): session_id: str query: str - results_count: int timestamp: datetime = Field(default_factory=datetime.now) @@ -66,11 +65,11 @@ async def websocket_endpoint(websocket: WebSocket): @app.get("/search") async def search_products(q: str, session_id: str = "unknown"): - # List of product names that should trigger a notification - NOTIFY_PRODUCTS = ["iPhone 15 Pro", "Kindle Paperwhite"] + # List of search terms that should trigger a notification + WATCH_LIST = ["iphone", "kindle"] try: - query = { + query_body = { "query": { "bool": { "should": [ @@ -83,23 +82,18 @@ async def search_products(q: str, session_id: str = "unknown"): "size": 20, } - response = es_client.search(index=PRODUCTS_INDEX, body=query) + response = es_client.search(index=PRODUCTS_INDEX, body=query_body) results = [] - notify_found = False - for hit in response["hits"]["hits"]: product = hit["_source"] product["score"] = hit["_score"] results.append(product) - # Check if this product should trigger a notification - if product.get("product_name") in NOTIFY_PRODUCTS: - notify_found = True - results_count = response["hits"]["total"]["value"] - if notify_found: + # Only send notification if the search term matches + if q.lower() in WATCH_LIST: notification = SearchNotification( session_id=session_id, query=q, results_count=results_count ) @@ -112,7 +106,6 @@ async def search_products(q: str, session_id: str = "unknown"): "type": "search", "session_id": session_id, "query": q, - "results_count": results_count, "timestamp": notification.timestamp.isoformat(), } ) @@ -129,7 +122,7 @@ async def search_products(q: str, session_id: str = "unknown"): @app.get("/") async def get_main_page(): - return FileResponse("index.html") + return FileResponse("test.html") if __name__ == "__main__":