Skip to content

Commit bc95658

Browse files
authored
blog content: building-elasticsearch-apis-with-fastapi-websockets (#472)
1 parent c88f3c9 commit bc95658

File tree

7 files changed

+503
-0
lines changed

7 files changed

+503
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Quickstart
2+
3+
Follow these steps to set up and run the FastAPI API:
4+
5+
1. **Create a virtual environment:**
6+
7+
```bash
8+
python3 -m venv venv
9+
```
10+
11+
2. **Activate the virtual environment:**
12+
13+
- On macOS/Linux:
14+
```bash
15+
source venv/bin/activate
16+
```
17+
- On Windows:
18+
```bash
19+
venv\Scripts\activate
20+
```
21+
22+
3. **Install dependencies:**
23+
24+
```bash
25+
pip install -r requirements.txt
26+
```
27+
28+
4. **Run the API with uvicorn:**
29+
30+
```bash
31+
uvicorn main:app --reload
32+
```
33+
34+
The API will be available at [http://localhost:8000](http://localhost:8000)
35+
36+
## Credentials
37+
38+
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.
39+
40+
At the terminal, you will see prompts like this to insert your credentials:
41+
42+
```
43+
Insert the Elasticsearch endpoint here:
44+
Insert the Elasticsearch API key here:
45+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
2+
POST products/_bulk
3+
{"index":{}}
4+
{"product_name": "iPhone 15 Pro", "price": 999.99, "description": "Latest flagship smartphone with titanium design, A17 Pro chip, and advanced camera system"}
5+
{"index":{}}
6+
{"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"}
7+
{"index":{}}
8+
{"product_name": "AirPods Pro 2nd Gen", "price": 249.99, "description": "Wireless earbuds with active noise cancellation, spatial audio, and USB-C charging case"}
9+
{"index":{}}
10+
{"product_name": "iPad Air", "price": 599.99, "description": "Versatile tablet with M1 chip, 10.9-inch Liquid Retina display, and Apple Pencil support"}
11+
{"index":{}}
12+
{"product_name": "Apple Watch Series 9", "price": 399.99, "description": "Advanced smartwatch with health monitoring, fitness tracking, and always-on Retina display"}
13+
{"index":{}}
14+
{"product_name": "Samsung Galaxy S24 Ultra", "price": 1199.99, "description": "Premium Android smartphone with S Pen, 200MP camera, and AI-powered features"}
15+
{"index":{}}
16+
{"product_name": "Dell XPS 13", "price": 1299.99, "description": "Ultra-portable laptop with Intel Core i7, 13.4-inch InfinityEdge display, and premium build"}
17+
{"index":{}}
18+
{"product_name": "Sony WH-1000XM5", "price": 399.99, "description": "Premium noise-canceling headphones with 30-hour battery and exceptional audio quality"}
19+
{"index":{}}
20+
{"product_name": "Nintendo Switch OLED", "price": 349.99, "description": "Hybrid gaming console with vibrant OLED screen, enhanced audio, and portable design"}
21+
{"index":{}}
22+
{"product_name": "Kindle Paperwhite", "price": 139.99, "description": "E-reader with 6.8-inch glare-free display, waterproof design, and weeks of battery life"}
23+
{"index":{}}
24+
{"product_name": "Google Pixel 8 Pro", "price": 999.99, "description": "AI-powered smartphone with advanced computational photography and 7 years of updates"}
25+
{"index":{}}
26+
{"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"}
27+
{"index":{}}
28+
{"product_name": "Dyson V15 Detect", "price": 749.99, "description": "Powerful cordless vacuum with laser dust detection and intelligent suction adjustment"}
29+
{"index":{}}
30+
{"product_name": "Fitbit Versa 4", "price": 199.99, "description": "Health and fitness smartwatch with GPS, heart rate monitoring, and 6+ day battery"}
31+
{"index":{}}
32+
{"product_name": "Bose QuietComfort 45", "price": 329.99, "description": "Wireless noise-canceling headphones with balanced sound and 24-hour battery life"}
33+
{"index":{}}
34+
{"product_name": "Tesla Model Y", "price": 47190.00, "description": "Electric SUV with autopilot, 330-mile range, and minimalist interior design"}
35+
{"index":{}}
36+
{"product_name": "Instant Pot Duo 7-in-1", "price": 99.99, "description": "Multi-use pressure cooker that replaces 7 kitchen appliances with smart programming"}
37+
{"index":{}}
38+
{"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"}
39+
{"index":{}}
40+
{"product_name": "Vitamix A3500", "price": 549.99, "description": "Professional-grade blender with preset programs, self-cleaning, and 10-year warranty"}
41+
{"index":{}}
42+
{"product_name": "Herman Miller Aeron Chair", "price": 1395.00, "description": "Ergonomic office chair with breathable mesh, lumbar support, and 12-year warranty"}
43+
{"index":{}}
44+
{"product_name": "Canon EOS R5", "price": 3899.99, "description": "Professional mirrorless camera with 45MP sensor, 8K video, and advanced autofocus"}
45+
{"index":{}}
46+
{"product_name": "Sonos Arc Soundbar", "price": 899.99, "description": "Premium soundbar with Dolby Atmos, voice control, and seamless music streaming"}
47+
{"index":{}}
48+
{"product_name": "Peloton Bike+", "price": 2495.00, "description": "Interactive exercise bike with rotating HD touchscreen and live fitness classes"}
49+
{"index":{}}
50+
{"product_name": "Roomba j7+", "price": 849.99, "description": "Smart robot vacuum with object avoidance, self-emptying base, and app control"}
51+
{"index":{}}
52+
{"product_name": "KitchenAid Stand Mixer", "price": 379.99, "description": "Iconic stand mixer with 10-speed control, tilt-head design, and multiple attachments"}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>TechStore - Product Search</title>
7+
</head>
8+
<body>
9+
<header>
10+
<h1>🛍️ TechStore - Find Your Perfect Product</h1>
11+
</header>
12+
13+
<main>
14+
<form onsubmit="event.preventDefault(); searchProducts();">
15+
<fieldset>
16+
<legend>Product Search</legend>
17+
<p>
18+
<label for="searchQuery">Search Products:</label><br />
19+
<input
20+
type="text"
21+
id="searchQuery"
22+
placeholder="Search for phones, laptops, headphones..."
23+
size="50"
24+
required />
25+
<button type="submit">🔍 Search</button>
26+
</p>
27+
</fieldset>
28+
</form>
29+
30+
<fieldset>
31+
<legend>Live Notifications</legend>
32+
<p id="status">🟡 Connecting to live notifications...</p>
33+
</fieldset>
34+
35+
<section id="searchResults">
36+
<h2>Search Results</h2>
37+
<blockquote>
38+
<em>🔍 Enter a search term above to find products</em>
39+
</blockquote>
40+
</section>
41+
</main>
42+
43+
<!-- HTML Dialog for notifications -->
44+
<dialog id="notificationDialog">
45+
<fieldset>
46+
<legend>🔔 Live Search Activity</legend>
47+
<p id="notificationMessage"></p>
48+
<p>
49+
<button onclick="closeNotification()" autofocus>OK</button>
50+
</p>
51+
</fieldset>
52+
</dialog>
53+
54+
<script>
55+
let ws = null;
56+
let sessionId = null;
57+
58+
window.onload = function () {
59+
sessionId = "session_" + Date.now();
60+
connectWebSocket();
61+
};
62+
63+
function connectWebSocket() {
64+
const statusDiv = document.getElementById("status");
65+
66+
ws = new WebSocket("ws://localhost:8000/ws");
67+
68+
ws.onopen = function () {
69+
statusDiv.innerHTML =
70+
"🟢 Connected - You will see when others search for products";
71+
console.log("Connected to WebSocket");
72+
};
73+
74+
ws.onmessage = function (event) {
75+
try {
76+
const notification = JSON.parse(event.data);
77+
if (notification.type === "search") {
78+
showSearchNotification(notification);
79+
}
80+
} catch (error) {
81+
console.error("Error parsing notification:", error);
82+
}
83+
};
84+
85+
ws.onclose = function () {
86+
statusDiv.innerHTML = "🔴 Disconnected from live notifications";
87+
console.log("Disconnected from WebSocket");
88+
};
89+
90+
ws.onerror = function (error) {
91+
statusDiv.innerHTML = "❌ Connection error";
92+
console.error("WebSocket error:", error);
93+
};
94+
}
95+
96+
function showSearchNotification(notification) {
97+
// Skip notifications from the same session (same browser window)
98+
if (notification.session_id === sessionId) {
99+
return;
100+
}
101+
102+
const dialog = document.getElementById("notificationDialog");
103+
const messageElement = document.getElementById("notificationMessage");
104+
105+
messageElement.innerHTML = `<p><strong>Hot search alert!</strong> Other users are looking for <em>"${notification.query}"</em> right now.</p>`;
106+
107+
dialog.showModal();
108+
}
109+
110+
function closeNotification() {
111+
const dialog = document.getElementById("notificationDialog");
112+
dialog.close();
113+
}
114+
115+
async function searchProducts() {
116+
const query = document.getElementById("searchQuery").value.trim();
117+
const resultsDiv = document.getElementById("searchResults");
118+
119+
if (!query) {
120+
resultsDiv.innerHTML = `
121+
<h2>Search Results</h2>
122+
<blockquote>
123+
<strong>⚠️ Please enter a search term</strong>
124+
</blockquote>
125+
`;
126+
return;
127+
}
128+
129+
resultsDiv.innerHTML = `
130+
<h2>Search Results</h2>
131+
<blockquote>
132+
<em>🔍 Searching TechStore inventory...</em>
133+
</blockquote>
134+
`;
135+
136+
try {
137+
const response = await fetch(
138+
`/search?q=${encodeURIComponent(
139+
query
140+
)}&session_id=${encodeURIComponent(sessionId)}`
141+
);
142+
const data = await response.json();
143+
144+
if (response.ok) {
145+
displaySearchResults(data);
146+
} else {
147+
throw new Error(data.error || "Search failed");
148+
}
149+
} catch (error) {
150+
resultsDiv.innerHTML = `
151+
<h2>Search Results</h2>
152+
<fieldset>
153+
<legend>❌ Search Error</legend>
154+
<p><strong>Error:</strong> ${error.message}</p>
155+
</fieldset>
156+
`;
157+
}
158+
}
159+
160+
function displaySearchResults(data) {
161+
const resultsDiv = document.getElementById("searchResults");
162+
163+
if (data.results.length === 0) {
164+
resultsDiv.innerHTML = `
165+
<h2>Search Results</h2>
166+
<fieldset>
167+
<legend>❌ No products found</legend>
168+
<p>No products match "<strong>${data.query}</strong>"</p>
169+
<p><em>Try searching for: iPhone, laptop, headphones, watch, etc.</em></p>
170+
</fieldset>
171+
`;
172+
return;
173+
}
174+
175+
let html = `<h2>✅ Found ${data.total} products for "${data.query}"</h2>`;
176+
177+
data.results.forEach((product) => {
178+
html += `
179+
<fieldset>
180+
<legend><strong>${
181+
product.product_name
182+
}</strong></legend>
183+
<p><big>💰 $${product.price.toFixed(2)}</big></p>
184+
<p>${product.description}</p>
185+
</fieldset>
186+
`;
187+
});
188+
189+
resultsDiv.innerHTML = html;
190+
}
191+
</script>
192+
</body>
193+
</html>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import json
2+
import os
3+
4+
from elasticsearch import Elasticsearch
5+
6+
es_client = Elasticsearch(
7+
hosts=[os.environ["ELASTICSEARCH_ENDPOINT"]],
8+
api_key=os.environ["ELASTICSEARCH_API_KEY"],
9+
)
10+
11+
PRODUCTS_INDEX = "products"
12+
13+
14+
def create_products_index():
15+
try:
16+
mapping = {
17+
"mappings": {
18+
"properties": {
19+
"product_name": {"type": "text", "analyzer": "standard"},
20+
"price": {"type": "float"},
21+
"description": {"type": "text", "analyzer": "standard"},
22+
}
23+
}
24+
}
25+
26+
es_client.indices.create(index=PRODUCTS_INDEX, body=mapping)
27+
print(f"Index {PRODUCTS_INDEX} created successfully")
28+
except Exception as e:
29+
print(f"Error creating index: {e}")
30+
31+
32+
def load_products_from_ndjson():
33+
try:
34+
if not os.path.exists("products.ndjson"):
35+
print("Error: products.ndjson file not found!")
36+
return
37+
38+
products_loaded = 0
39+
with open("products.ndjson", "r") as f:
40+
for line in f:
41+
if line.strip():
42+
product_data = json.loads(line.strip())
43+
es_client.index(index=PRODUCTS_INDEX, body=product_data)
44+
products_loaded += 1
45+
46+
print(f"Successfully loaded {products_loaded} products into Elasticsearch")
47+
48+
except Exception as e:
49+
print(f"Error loading products: {e}")
50+
51+
52+
if __name__ == "__main__":
53+
create_products_index()
54+
load_products_from_ndjson()

0 commit comments

Comments
 (0)