Building an Instant Image Search Service with Easysearch kNN

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

Tags: Easysearch knn Flask ImageSearch VectorSearch

Posted on Thu, 29 Oct 2026 16:26:51 +0000 by ThEMakeR