Ever wanted to build your web app that can store data, let you search through it, and even update or delete stuff when needed? Well, that’s exactly what we’re going to do today but with a twist. Instead of using the usual databases, we’re going to use the power of Elasticsearch.
First things first, if you haven’t already installed Elasticsearch on your Ubuntu 22.04 VPS, no problem. I’ve put together an easy-to-follow guide to get you set up in no time.
Once you have that running, we’ll move on to building a Python Flask CRUD (Create, Read, Update, Delete) API app with Elasticsearch as the backend.
Don’t worry if that sounds a bit technical. We’ll walk you through it step by step. Whether you want to create a book database, a basic blog with a built-in search feature, or even a custom search tool for your website, this guide will help you get started from scratch. By the end, you’ll have a working app you can use and test.
Prerequisites and Setup
Here’s what you need:
- A VPS with Ubuntu 22.04 and Elastic Search running
- Basic Python knowledge
- Familiarity with HTTP methods: GET, POST, DELETE
Installing Python and Flask on Ubuntu VPS
Run the following commands to install the Python packages:
sudo apt install python3-pip -y
pip3 install flask elasticsearch
sudo apt install jq -y
This sets up Flask (a lightweight web framework) and the official Python client for Elastic Search.
Creating Your Flask Application Directory
mkdir mysearchapp
cd mysearchapp
nano app.py
In Config,
Go to root -> mysearchapp -> app.py
Paste the following base structure inside app.py:
from flask import Flask, request, jsonify
from elasticsearch import Elasticsearch
app = Flask(__name__)
es = Elasticsearch("http://localhost:9200")
@app.route('/')
def home();
return "Welcome to the Book Search API!"
Adding and Searching Books in Elastic Search
Add Books:
@app.route('/add', methods=['POST'])
def add_book():
data = request.json
res = es.index(index="books", document=data)
return jsonify(res['result'])
Search Books by Title or Author:
@app.route('/search')
def search():
query = request.args.get('q')
res = es.search(index="books", query={
"multi_match": {
"query": query,
"fields": ["title", "author"]
}
})
books = [doc['_source'] for doc in res['hits']['hits']]
return jsonify(books)
View All Books
@app.route('/all')
def get_all_books():
res = es.search(index="books", query={"match_all": {}}, size=1000)
books = [doc['_source'] for doc in res['hits']['hits']]
return jsonify(books)
Updating and Deleting Books
Update Book
@app.route('/update', methods=['POST'])
def update_book():
data = request.get_json()
old_title = data.get('old_title')
res = es.search(index="books", query={"match": {"title": old_title}})
if res['hits']['total']['value'] == 0:
return jsonify({"message": "Book not found!"}), 404
book_id = res['hits']['hits'][0]['_id']
es.update(index="books", id=book_id, body={"doc": data})
return jsonify({"message": "Book updated!"})
Delete Book
@app.route('/delete', methods=['POST'])
def delete_book():
data = request.get_json()
title = data.get('title')
res = es.search(index="books", query={"match": {"title": title}})
if res['hits']['total']['value'] == 0:
return jsonify({"message": "Book not found!"}), 404
book_id = res['hits']['hits'][0]['_id']
es.delete(index="books", id=book_id)
return jsonify({"message": "Book deleted!"})
Simple Frontend with HTML + JavaScript
Create a folder:
mkdir templates
Create file index.html
nano index.html
Inside templates/index.html:
In index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Book Manager</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Google Fonts for a modern look -->
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600&display=swap" rel="stylesheet">
<style>
:root {
--primary: #6C63FF;
--primary-light: #a39cff;
--accent: #F9A826;
--bg: #f7f7fa;
--card-bg: #fff;
--success: #4BB543;
--error: #FF5252;
}
body {
font-family: 'Montserrat', Arial, sans-serif;
background: var(--bg);
margin: 0;
padding: 0;
}
.container {
max-width: 800px;
margin: 40px auto;
padding: 30px 20px 40px 20px;
background: var(--card-bg);
border-radius: 18px;
box-shadow: 0 6px 32px rgba(108,99,255,0.10), 0 1.5px 8px rgba(0,0,0,0.07);
animation: fadeIn 1s;
}
h1 {
text-align: center;
color: var(--primary);
font-weight: 700;
margin-bottom: 24px;
letter-spacing: 1px;
font-size: 2.4rem;
}
h3 {
margin-top: 32px;
color: #333;
font-weight: 600;
letter-spacing: 0.5px;
}
.form-group {
display: flex;
gap: 10px;
margin-bottom: 10px;
flex-wrap: wrap;
}
input, button, textarea {
border-radius: 8px;
border: none;
outline: none;
font-size: 1rem;
transition: box-shadow 0.2s, border 0.2s;
}
input, textarea {
background: #f2f2f7;
padding: 10px 12px;
border: 1.5px solid #e0e0e0;
}
input:focus, textarea:focus {
border-color: var(--primary);
box-shadow: 0 0 0 2px #6c63ff22;
}
button {
background: var(--primary);
color: #fff;
padding: 10px 22px;
font-weight: 600;
cursor: pointer;
border: none;
box-shadow: 0 2px 8px #6c63ff22;
transition: background 0.2s, transform 0.1s;
}
button:hover, button:focus {
background: var(--accent);
color: #222;
transform: translateY(-2px) scale(1.04);
}
textarea {
margin-top: 10px;
resize: vertical;
min-height: 100px;
max-height: 300px;
}
table {
border-collapse: separate;
border-spacing: 0;
width: 100%;
margin-top: 18px;
background: #fff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 12px #6c63ff14;
animation: fadeInUp 0.7s;
}
th, td {
padding: 12px 14px;
text-align: left;
}
th {
background: var(--primary-light);
color: #fff;
font-weight: 600;
}
tr {
transition: background 0.2s;
}
tr:hover {
background: #f6f5ff;
}
tr.fade-in {
animation: fadeInRow 0.6s;
}
/* Toast notification */
.toast {
position: fixed;
top: 32px;
right: 32px;
min-width: 220px;
padding: 16px 28px;
border-radius: 8px;
color: #fff;
font-weight: 600;
z-index: 9999;
opacity: 0;
pointer-events: none;
transition: opacity 0.4s, transform 0.4s;
box-shadow: 0 2px 16px #0002;
}
.toast.show {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
.toast.success { background: var(--success); }
.toast.error { background: var(--error); }
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px);}
to { opacity: 1; transform: none;}
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(40px);}
to { opacity: 1; transform: none;}
}
@keyframes fadeInRow {
from { opacity: 0; transform: translateY(12px);}
to { opacity: 1; transform: none;}
}
/* Responsive */
@media (max-width: 600px) {
.container { padding: 12px 4px; }
h1 { font-size: 1.5rem; }
table, th, td { font-size: 0.98rem; }
.form-group { flex-direction: column; gap: 6px; }
}
</style>
</head>
<body>
<div class="container">
<h1>📚 Book Manager</h1>
<!-- Add a Book -->
<h3>Add a Book</h3>
<div class="form-group">
<input id="add-title" placeholder="Title" required>
<input id="add-author" placeholder="Author" required>
<button onclick="addBook()">Add Book</button>
</div>
<!-- Search Books -->
<h3>Search Books</h3>
<div class="form-group">
<input id="search-query" placeholder="Search by title or author" required>
<button onclick="searchBooks()">Search</button>
</div>
<!-- Update a Book -->
<h3>Update a Book</h3>
<div class="form-group">
<input id="old-title" placeholder="Old Title" required>
<input id="new-title" placeholder="New Title (optional)">
<input id="new-author" placeholder="New Author (optional)">
<button onclick="updateBook()">Update Book</button>
</div>
<!-- Delete a Book -->
<h3>Delete a Book</h3>
<div class="form-group">
<input id="delete-title" placeholder="Title to Delete" required>
<button onclick="deleteBook()">Delete Book</button>
</div>
<!-- Output -->
<h3>📄 Output</h3>
<textarea id="output" readonly></textarea>
<!-- Show All Books -->
<h3>📚 All Books</h3>
<button onclick="loadAllBooks()">Show All Books</button>
<table id="booksTable">
<thead>
<tr><th>Title</th><th>Author</th></tr>
</thead>
<tbody></tbody>
</table>
</div>
<!-- Toast -->
<div id="toast" class="toast"></div>
<script>
const baseUrl = 'http://<your-public-ip>:5000';
// Toast notification
function showToast(msg, type = "success") {
const toast = document.getElementById('toast');
toast.textContent = msg;
toast.className = `toast ${type} show`;
setTimeout(() => {
toast.className = `toast ${type}`;
}, 2200);
}
function setOutput(data) {
document.getElementById('output').value = typeof data === 'string'
? data
: JSON.stringify(data, null, 4);
}
// Animate table row
function animateRow(row) {
row.classList.add('fade-in');
setTimeout(() => row.classList.remove('fade-in'), 700);
}
async function addBook() {
const title = document.getElementById('add-title').value.trim();
const author = document.getElementById('add-author').value.trim();
if (!title || !author) {
showToast("Please enter both title and author.", "error");
return;
}
const response = await fetch(`${baseUrl}/add`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, author })
});
const result = await response.json();
setOutput(result);
if (result.success) {
showToast("Book added! 📚", "success");
document.getElementById('add-title').value = '';
document.getElementById('add-author').value = '';
loadAllBooks();
} else {
showToast(result.message || "Failed to add.", "error");
}
}
async function searchBooks() {
const query = document.getElementById('search-query').value.trim();
if (!query) {
showToast("Enter a search query.", "error");
return;
}
const response = await fetch(`${baseUrl}/search?q=${encodeURIComponent(query)}`);
const result = await response.json();
setOutput(result);
if (Array.isArray(result) && result.length) {
showToast(`Found ${result.length} book(s)!`, "success");
renderBooks(result);
} else {
showToast("No books found.", "error");
renderBooks([]);
}
}
async function updateBook() {
const old_title = document.getElementById('old-title').value.trim();
const new_title = document.getElementById('new-title').value.trim();
const new_author = document.getElementById('new-author').value.trim();
if (!old_title) {
showToast("Enter the old title.", "error");
return;
}
const response = await fetch(`${baseUrl}/update`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ old_title, new_title, new_author })
});
const result = await response.json();
setOutput(result);
if (result.success) {
showToast("Book updated! ✏️", "success");
document.getElementById('old-title').value = '';
document.getElementById('new-title').value = '';
document.getElementById('new-author').value = '';
loadAllBooks();
} else {
showToast(result.message || "Update failed.", "error");
}
}
async function deleteBook() {
const title = document.getElementById('delete-title').value.trim();
if (!title) {
showToast("Enter a title to delete.", "error");
return;
}
const response = await fetch(`${baseUrl}/delete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title })
});
const result = await response.json();
setOutput(result);
if (result.success) {
showToast("Book deleted! 🗑️", "success");
document.getElementById('delete-title').value = '';
loadAllBooks();
} else {
showToast(result.message || "Delete failed.", "error");
}
}
function renderBooks(books) {
const tbody = document.querySelector('#booksTable tbody');
tbody.innerHTML = '';
books.forEach(book => {
const row = document.createElement('tr');
row.innerHTML = `<td>${book.title}</td><td>${book.author}</td>`;
animateRow(row);
tbody.appendChild(row);
});
}
async function loadAllBooks() {
const response = await fetch(`${baseUrl}/all`);
const books = await response.json();
renderBooks(books);
}
// Load all books on page load
window.onload = loadAllBooks;
</script>
</body>
</html>
Update app.py:
from flask import render_template
@app.route('/')
def home():
return render_template('index.html')
Enable CORS for External Access
Install flask-cors :
pip install flask-cors
Add to your app.py:
from flask_cors import CORS
CORS(app)
Your app.py will look like
from flask import Flask, request, jsonify, render_template, Response
import json
from elasticsearch import Elasticsearch
from flask_cors import CORS
app = Flask(__name__)
CORS(app)
es = Elasticsearch("http://localhost:9200")
@app.route('/')
def home():
return render_template('index.html')
@app.route('/add', methods=['POST'])
def add_book():
data = request.get_json()
title = data.get('title')
author = data.get('author')
# Add book to Elasticsearch
es.index(index="books", body={
"title": title,
"author": author
})
return jsonify({"message": f"Book '{title}' added successfully!"})
@app.route('/update', methods=['POST'])
def update_book():
data = request.get_json()
old_title = data.get('old_title')
new_title = data.get('new_title')
new_author = data.get('new_author')
# Search for the book
res = es.search(index="books", body={
"query": {
"match": {
"title": old_title
}
}
})
if res['hits']['total']['value'] == 0:
return jsonify({"message": "Book not found!"}), 404
book_id = res['hits']['hits'][0]['_id']
update_doc = {}
if new_title:
update_doc["title"] = new_title
if new_author:
update_doc["author"] = new_author
es.update(index="books", id=book_id, body={"doc": update_doc})
return jsonify({"message": f"Book '{old_title}' updated successfully!"})
@app.route('/delete', methods=['POST'])
def delete_book():
data = request.get_json()
title = data.get('title')
# Search for the book
res = es.search(index="books", body={
"query": {
"match": {
"title": title
}
}
})
if res['hits']['total']['value'] == 0:
return jsonify({"message": "Book not found!"}), 404
# Get the book's ID
book_id = res['hits']['hits'][0]['_id']
# Delete the book
es.delete(index="books", id=book_id)
return jsonify({"message": f"Book '{title}' deleted successfully!"})
@app.route('/search')
def search():
query = request.args.get('q', '').strip()
if not query:
return jsonify({"message": "Search query is required."}), 400
res = es.search(index="books", query={
"multi_match": {
"query": query,
"fields": ["title", "author"]
}
})
books = [doc['_source'] for doc in res['hits']['hits']]
pretty = json.dumps(books, indent=4)
return Response(pretty, mimetype='application/json')
@app.route('/all')
def get_all_books():
res = es.search(index="books", query={"match_all": {}}, size=1000)
books = [doc['_source'] for doc in res['hits']['hits']]
pretty = json.dumps(books, indent=4)
return Response(pretty, mimetype='application/json')
if __name__ == '__main__':
if not es.indices.exists(index="books"):
es.indices.create(index="books")
app.run(host='38.108.127.155', port=5000, debug=True)
Disable firewall
Or you can allow port 5000 from the inbound rules
Final Thoughts
Congratulations! You’ve just built your very own Python Flask CRUD app with Elasticsearch, and now you’ve got a system that can search, store, update, and delete book data in real time. This is perfect for creating scalable applications that need to handle high-performance searches, all while keeping things simple and easy to develop. Whether you’re planning to build something small or scale it up in the future, this setup will give you the flexibility and speed you need.



















