Skip to content

blog content: building-elasticsearch-apis-with-fastapi-websockets #472

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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:
```
Original file line number Diff line number Diff line change
@@ -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"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TechStore - Product Search</title>
</head>
<body>
<header>
<h1>🛍️ TechStore - Find Your Perfect Product</h1>
</header>

<main>
<form onsubmit="event.preventDefault(); searchProducts();">
<fieldset>
<legend>Product Search</legend>
<p>
<label for="searchQuery">Search Products:</label><br />
<input
type="text"
id="searchQuery"
placeholder="Search for phones, laptops, headphones..."
size="50"
required />
<button type="submit">🔍 Search</button>
</p>
</fieldset>
</form>

<fieldset>
<legend>Live Notifications</legend>
<p id="status">🟡 Connecting to live notifications...</p>
</fieldset>

<section id="searchResults">
<h2>Search Results</h2>
<blockquote>
<em>🔍 Enter a search term above to find products</em>
</blockquote>
</section>
</main>

<!-- HTML Dialog for notifications -->
<dialog id="notificationDialog">
<fieldset>
<legend>🔔 Live Search Activity</legend>
<p id="notificationMessage"></p>
<p>
<button onclick="closeNotification()" autofocus>OK</button>
</p>
</fieldset>
</dialog>

<script>
let ws = null;
let sessionId = null;

window.onload = function () {
sessionId = "session_" + Date.now();
connectWebSocket();
};

function connectWebSocket() {
const statusDiv = document.getElementById("status");

ws = new WebSocket("ws://localhost:8000/ws");

ws.onopen = function () {
statusDiv.innerHTML =
"🟢 Connected - You will see when others search for products";
console.log("Connected to WebSocket");
};

ws.onmessage = function (event) {
try {
const notification = JSON.parse(event.data);
if (notification.type === "search") {
showSearchNotification(notification);
}
} catch (error) {
console.error("Error parsing notification:", error);
}
};

ws.onclose = function () {
statusDiv.innerHTML = "🔴 Disconnected from live notifications";
console.log("Disconnected from WebSocket");
};

ws.onerror = function (error) {
statusDiv.innerHTML = "❌ Connection error";
console.error("WebSocket error:", error);
};
}

function showSearchNotification(notification) {
// Skip notifications from the same session (same browser window)
if (notification.session_id === sessionId) {
return;
}

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

messageElement.innerHTML = `<p><strong>Hot search alert!</strong> Other users are looking for <em>"${notification.query}"</em> right now.</p>`;

dialog.showModal();
}

function closeNotification() {
const dialog = document.getElementById("notificationDialog");
dialog.close();
}

async function searchProducts() {
const query = document.getElementById("searchQuery").value.trim();
const resultsDiv = document.getElementById("searchResults");

if (!query) {
resultsDiv.innerHTML = `
<h2>Search Results</h2>
<blockquote>
<strong>⚠️ Please enter a search term</strong>
</blockquote>
`;
return;
}

resultsDiv.innerHTML = `
<h2>Search Results</h2>
<blockquote>
<em>🔍 Searching TechStore inventory...</em>
</blockquote>
`;

try {
const response = await fetch(
`/search?q=${encodeURIComponent(
query
)}&session_id=${encodeURIComponent(sessionId)}`
);
const data = await response.json();

if (response.ok) {
displaySearchResults(data);
} else {
throw new Error(data.error || "Search failed");
}
} catch (error) {
resultsDiv.innerHTML = `
<h2>Search Results</h2>
<fieldset>
<legend>❌ Search Error</legend>
<p><strong>Error:</strong> ${error.message}</p>
</fieldset>
`;
}
}

function displaySearchResults(data) {
const resultsDiv = document.getElementById("searchResults");

if (data.results.length === 0) {
resultsDiv.innerHTML = `
<h2>Search Results</h2>
<fieldset>
<legend>❌ No products found</legend>
<p>No products match "<strong>${data.query}</strong>"</p>
<p><em>Try searching for: iPhone, laptop, headphones, watch, etc.</em></p>
</fieldset>
`;
return;
}

let html = `<h2>✅ Found ${data.total} products for "${data.query}"</h2>`;

data.results.forEach((product) => {
html += `
<fieldset>
<legend><strong>${
product.product_name
}</strong></legend>
<p><big>💰 $${product.price.toFixed(2)}</big></p>
<p>${product.description}</p>
</fieldset>
`;
});

resultsDiv.innerHTML = html;
}
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -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()
Loading