Environment Setup
Start the Easysearch service:
# Ensure your vm.max_map_count meets the requirement
sudo sysctl -w vm.max_map_count=262144
docker run -it --rm -p 9200:9200 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms512m -Xmx512m" \
infinilabs/easysearch:1.3.0-24 \
-E "security.enabled=false"
Install Python dependencies:
pip install -r requirements.txt
Start the server:
ES_SERVER=http://localhost:9200 python main.py
Backend Implementation
The core functionality involves receiving uploaded images, processing them, converting them into vectors, and using Easysearch for image retrieval. The results are then rendered on a template page for user display. Users can upload images via the /req route and submit a form to initiate a search. The results appear on the search.html template.
This service primarily uses Flask and Easysearch. Flask is a widely used Python web framework that allows developers to build web applications easily and flexibly. Easysearch is a distributed search engine offering robust full-text search and analysis capabilities, including a kNN retrieval API for vector queries. Combining these tools enables the creation of a functional image search system.
First, we create a Flask application instance and define several routes and view functions. The /req route displays a form where users can upload an image. The /search route handles the image search request and returns the results.
In the /search route, we connect to the Easysearch engine, retrieve the uploaded image file, process it, and convert it into a vecter for similarity matching in the vector space. We then construct a query body using Easysearch's query syntax to describe our search needs. By calling the Easysearch search method, we perform the search and obtain the results.
After retrieving the results, we extract the necessary fields and pass them to the template for rendering. We use Flask's render_template function to pass the results to a template named search.html, which dynamically generates the page content based on the search results and displays them to the user.
This simple and efficient image search service allows users to upload images quickly, and the system will search through the image library and return similar results.
from PIL import Image
from elasticsearch import Elasticsearch
from flask import Flask, request, jsonify, render_template
from img2vec_pytorch import Img2Vec
import io
import os
DEFAULT_INDEX = "img-test"
app = Flask(__name__)
es = Elasticsearch(os.environ.get("ES_SERVER") or "http://localhost:9200")
def rgba2rgb(image_file):
# Open the image file
img = Image.open(image_file)
# Check if the image is in RGBA mode
if img.mode == "RGBA":
# Convert the image to RGB mode
img = img.convert("RGB")
# Create a BytesIO object and save the image to it
image_io = io.BytesIO()
img.save(image_io, format="JPEG")
# Seek to the beginning of the BytesIO object
image_io.seek(0)
return image_io
return image_file
def vectorize(input):
img2vec = Img2Vec()
try:
img = Image.open(input)
vec = img2vec.get_vec(img, tensor=True)
vec_np = vec.cpu().numpy().flatten().tolist()
return vec_np
except Exception as e:
print(f"Error processing image: {e}")
return None
def init_indicies(index: str):
if es.indices.exists(index):
return
# Initialize kNN index
print(f"Initializing {index}")
es.indices.create(
index,
body={
"settings": {"index.knn": True},
"mappings": {
"properties": {
"my_vec": {
"type": "knn_dense_float_vector",
"knn": {
"dims": 512,
"model": "lsh",
"similarity": "cosine",
"L": 99,
"k": 1,
},
}
}
},
},
)
img_dir = "static/img"
for title in os.listdir(img_dir):
print(f"Indexing {title}")
my_vec = vectorize(os.path.join(img_dir, title))
body = {"title": title, "my_vec": my_vec}
es.index(index=index, body=body)
@app.route("/search", methods=["POST"])
def search_service():
# Retrieve form data
index_name = request.form.get("index_name") or DEFAULT_INDEX # Index name
# Retrieve uploaded image file
image_file = request.files.get("image")
if not index_name or not image_file:
return jsonify({"error": "Both index_name and image are required."}), 400
# Process image
image0 = rgba2rgb(image_file)
vector_arr = vectorize(image0)
if vector_arr is None:
return jsonify({"error": "Error processing image."}), 400
query_body = {
"size": 50,
"_source": "title",
"query": {
"bool": {
"must": [
{
"knn_nearest_neighbors": {
"field": "my_vec",
"vec": {"values": vector_arr},
"model": "lsh",
"similarity": "cosine",
"candidates": 50,
}
}
]
}
},
}
if not index_name or not vector_arr:
return jsonify({"error": "Both index_name and query are required."}), 400
# Execute search
response = es.search(index=index_name, body=query_body)
# Render search results using template
results = response["hits"]["hits"]
print([r["_source"]["title"] for r in results], len(results))
return render_template("search.html", results=results)
@app.route("/", methods=["GET"])
def home():
return render_template("home.html")
if __name__ == "__main__":
init_indicies(DEFAULT_INDEX)
app.run(port=5000, debug=True)
Frontend Implementation
The focus here is on implementing the frontend of an instant search page. The approach is clear—create a clean and visually appealing interface to showcase the functionality. Some basic elements are omitted for brevity, and we will now detail the approach and implementation.
We start by creating an HTML document and specifying the document type as HTML5. In the head section, we set the page title to "Easysearch Search Service" to clearly express the purpose of the page.
Next, we use CSS styles to define the appearance and layout of the page. In the <style> tag, we set background images, fonts, margins, and shadow effects to enhance the visual appeal and user experience.
<style>
#searchForm {
background-image: url("/static/background/bluewhite.jpg");
background-repeat: no-repeat;
background-size: cover;
}
body {
font-family: Arial, sans-serif;
background-color: #f9f8f8;
margin: 0;
padding: 20px;
}
.searchImage {
max-width: 600px;
max-height: 500px;
}
.container {
max-width: 100%;
margin: 0 auto;
/* background: linear-gradient(to right, #8aa0ee, #3838ee); */
background: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
margin-bottom: 20px;
color: #000;
}
h2 {
margin-bottom: 10px;
}
form {
max-width: 400px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
label {
display: block;
font-weight: bold;
margin-bottom: 5px;
}
input[type="text"],
input[type="file"] {
width: 100%;
padding: 8px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
input[type="submit"] {
width: 100%;
padding: 10px;
background-color: #4caf50;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease;
}
input[type="submit"]:hover {
background-color: #45a049;
}
pre {
background-color: #f5f5f5;
padding: 10px;
border-radius: 5px;
white-space: pre-wrap;
}
#result {
text-align: center;
margin-top: 20px;
}
</style>
<style>
body {
font-family: Arial, sans-serif;
/* background-color: #f2f2f2; */
margin: 0;
padding: 20px;
}
.container {
max-width: 100%;
margin: 0 auto;
background-image: url("/static/background/daziran.png");
}
.waterfall-container {
display: flex;
flex-wrap: wrap;
}
.waterfall-item {
display: inline-block;
border-radius: 5px;
box-shadow: none;
margin-bottom: 20px;
text-align: center;
width: 25%; /* Each row shows 4 search results, adjust as needed */
padding: 10px;
box-sizing: border-box;
}
.waterfall-item img {
max-width: 100%;
max-height: 250px;
border-radius: 5px;
}
.waterfall-item p {
margin-top: 10px;
font-size: 25px;
font-weight: bold;
color: black;
background-color: white;
}
</style>
JavaScript part, we include the jQuery library and a custom JavaScript file. These scripts handle the interaction logic on the page. With jQuery, we can easily manage form submission events and implement asynchronous requests and data processing. When a user submits the form, an AJAX request is sent to a specified URL, and the index name and image file are included as parameters. After successfully returning the AJAX response, we parse the returned HTML data, extract image and paragraph elements, and combine them in a specific format. Finally, the combined results are added to the result container for display.
Frontend JavaScript code
<script src="/static/jquery-3.5.1.min.js"></script>
<script src="/static/m.js"></script>
<script>
$(document).ready(function () {
$("#searchForm").on("submit", function (event) {
event.preventDefault();
var indexName = $("#indexName").val();
// Check if the index name is empty or undefined
if (!indexName) {
indexName = "img-test"; // Set default value to "default index name"
}
var formData = new FormData();
formData.append("index_name", indexName);
formData.append("image", $("#image")[0].files[0]);
$.ajax({
url: "/search",
method: "POST",
processData: false, // Important!
contentType: false, // Important!
data: formData,
success: function (data) {
// Clear previous results
$("#result").empty();
// Parse returned HTML and extract image and paragraph elements
var parsedData = $.parseHTML(data);
// Group image and paragraph elements
var imageAndParagraphPairs = [];
var currentPair = [];
$(parsedData).each(function () {
if ($(this).is("img.searchImage")) {
if (currentPair.length === 1) {
currentPair.push(this);
imageAndParagraphPairs.push(currentPair);
currentPair = [];
} else {
currentPair.push(this);
}
} else if ($(this).is("p")) {
if (currentPair.length === 0) {
currentPair.push(this);
} else {
currentPair.push(this);
imageAndParagraphPairs.push(currentPair);
currentPair = [];
}
} else if ($(this).is("h1")) {
// Add the <h1> element back to the results
$("#resultTitle").html($(this));
}
});
// Create and append waterfall items
$.each(imageAndParagraphPairs, function (index, pair) {
var $item = $("<div>").addClass("waterfall-item");
$.each(pair, function (i, element) {
$item.append(element);
});
$("#result").append($item);
});
},
});
});
});
</script>
Main page content, we wrap the content in a <div> with the class "container". The page includes a heading and a search form. The search form consists of a file selection input for selecting an image and a submit button that triggers the JavaScript event handler when clicked.
Search results section, we use a <div> element to display the title of the search results and another <div> element as a waterfall container to show the images and related paragraphs from the search results.
Code as follows
<body>
<div class="container">
<h1>Easycsearch Search Service</h1>
<form id="searchForm" enctype="multipart/form-data">
<label for="image">Image:</label><br />
<input type="file" id="image" name="image" /><br />
<input type="submit" value="Search" />
</form>
<div id="resultTitle"></div>
<div id="result" class="waterfall-container"></div>
</div>
</body>
Final result as shown
Before Search
After Search
Summary
With this simple Easysearch kNN-based image search service, users can easily upload image files, perform searches, and view the results in a waterfall layout.
Project GitHub: https://github.com/infinilabs/image-search-demo
About Easysearch
INFINI Easysearch is a distributed near-real-time search and analysis engine, built on the open-source Apache Lucene. The goal of Easysearch is to provide a lightweight alternative to Elasticsearch while continuing to improve and support more enterprise-level features. Compared to Elasticsearch, Easysearch focuses more on optimizing for search scenarios and maintaining simplicity and usability of the product.
Official Documentation: https://www.infinilabs.com/docs/latest/easysearch
Download Page: https://www.infinilabs.com/download