Image retrieval using Lindorm's multimodal capabilities

Updated at:
Copy as MD

Quickly build an image retrieval service that supports search by text and search by image, using Lindorm's scalar, full-text, and vector hybrid search combined with online inference.

Background

As autonomous driving, AI-generated content (AIGC), and multimodal foundation models evolve, the demand for image retrieval continues to grow. Whether you need to filter high-quality datasets from massive image libraries for model training or improve generated content by searching the internet for images, intelligent image retrieval is a key part of the workflow.

Lindorm's multimodal database provides an intelligent search solution that addresses the following core challenges:

  • High-quality recall: Combines scalar, full-text, and vector retrieval with AI multimodal models for precise matching of images, text, and features.

  • Efficient cost control: Stores and retrieves massive amounts of image data at lower cost.

  • Elastic scaling: Scales in seconds to handle rapid data growth.

With this solution, you can quickly build a high-performance, low-cost image retrieval service. The storage-compute separation architecture enables scaling in seconds to meet your business needs.

Architecture

The following Python code examples demonstrate how to build an image retrieval service by using Lindorm's scalar, full-text, and vector hybrid search capabilities along with online inference.

image

Step 1: Enable Lindorm's multimodal capabilities

  1. Log on to the Lindorm console.

  2. In the upper-left corner of the page, click Create.

  3. On the Lindorm purchase page, configure the following parameters.

    Parameter

    Description

    Product type

    Select Lindorm.

    Instance configuration

    Select Search Engine, Vector Engine, and AI Engine.

    Note
    • You must submit a ticket to enable the AI Engine.

      • Before you create an instance, submit a ticket to be added to the allowlist. Then, you can select AI Engine during the creation process.

      • If you have already created an instance, submit a ticket to enable the AI Engine feature separately.

    • The Vector Engine depends on the Search Engine. You must enable both engines.

    • For more information about the engines, see Engine types.

    • You can change the instance specification and number of nodes based on your workload. For more information, see Change instance specifications.

  4. Click Buy Now and complete the payment process.

Step 2: Configure an IP address whitelist

Add your client's IP address to the Lindorm IP address whitelist. To set the whitelist, see Set an IP address whitelist.

Step 3: Download the sample code

Download the code sample multimodalRetrieval for building the image retrieval service in the following steps.

Step 4: Configure the environment

Runtime environment

Ensure Python 3.10 or later is installed.

Install dependencies

 pip3 install -r requirements.txt 

Configure Lindorm connection endpoints

In the env script of the downloaded code, configure the connection endpoints for each Lindorm engine. For information about how to obtain connection endpoints, see View connection endpoints.

Large model inference uses Model Studio and requires an API key. For information about how to obtain an API key, see Obtain an API key.

# AI host (Configure the AI Engine endpoint)
AI_HOST="ld-bp17j28j2y7pm****-proxy-ai-pub.lindorm.aliyuncs.com"
AI_PORT="9002"

# Search host (Configure the Search Engine endpoint)
SEARCH_HOST="ld-bp17j28j2y7pm****-proxy-search-pub.lindorm.aliyuncs.com"
SEARCH_PORT="30070"

# Lindorm user and password (Configure the Lindorm user password)
LD_USER="root"
LD_PASSWORD="test****"

# The maximum number of results to return
SEARCH_TOP_K="9"

# Configure the API key for Model Studio
DASHSCOPE_API_KEY="sk-****"

# Fixed model names  
LD_VL_MODEL="bge_visual0"
LD_TEXT_MODEL="bge_m3_model"

Install Jupyter Notebook

  1. Install Jupyter.

    pip3 install jupyter
  2. Generate the configuration file ~/.jupyter/jupyter_notebook_config.py.

    jupyter notebook --generate-config
  3. Generate a password hash for Jupyter access.

    from passlib.hash import argon2
    print(argon2.hash('Vector123'))

    The output is a password hash used for Jupyter Notebook password configuration. The following is a sample output:

    $argon2id$v=19$m=65536,t=3,p=4$4TyndM75H8N4b+291xqjdA$n0QSxlv/uCLjGR0TX/jbD/XFlEu9BzQGI1b2Mcu6gxg
  4. Run the vim ~/.jupyter/jupyter_notebook_config.py command to open and edit the configuration file.

    # Add the following configurations to the end of the file.
    c.NotebookApp.ip = '*'
    # The default directory for the notebook. Set this yourself.
    # Whether to open a browser after the notebook starts. Set to False.
    c.NotebookApp.open_browser = False
    
    # The default access port. You can change this.
    c.NotebookApp.port = 9000
    
    # Replace the content after argon2 in the code below with the Jupyter password hash you obtained in the previous step.
    c.NotebookApp.password = 'argon2:$argon2id$v=19$m=65536,t=3,p=4$4TyndM75H8N4b+291xqjdA$n0QSxlv/uCLjGR0TX/jbD/XFlEu9BzQGI1b2Mcu6gxg'
    
    # This home directory is important. You must place the files that you want to access in this directory.
    c.NotebookApp.notebook_dir = u'/data/lindorm/LindormDemo'  # Set the directory that you want to display when you open Jupyter Notebook. You can set it to a frequently used path.
  5. Start the Jupyter service. Start the service in the foreground to check for startup errors. To stop the service, press Ctrl+C.

    jupyter notebook
    Note

    In production environments, we recommend that you start the Jupyter service in the background to prevent service interruptions if the terminal is closed. To start the service in the background, run the following command: nohup jupyter notebook --allow-root >/tmp/jupyter.log 2>&1 &.

Step 5: Run the .ipynb script

Create Lindorm multimodal retrieval models

The sample code depends on two Lindorm multimodal retrieval models:

  • Text embedding model: bge_m3_model.

  • Image embedding model: bge_visual0.

Note

For more information about how to deploy models in the AI Engine and for parameter details, see Model management and Use the AI Engine by using cURL commands.

Deploy the bge_m3_model

curl -i -k --location --header 'x-ld-ak:<username>' --header 'x-ld-sk:<password>' -X POST http://ld-bp17j28j2y7pm****-proxy-ai-pub.lindorm.aliyuncs.com:9002/v1/ai/models/create \
     -H "Content-Type: application/json" \
     -d '{
          "model_name": "bge_m3_model",
          "model_path": "huggingface://BAAI/bge-m3",
          "task": "FEATURE_EXTRACTION",
          "algorithm": "BGE_M3",
          "settings": {"instance_count": "2"}
     }'

Deploy the bge_visual0 model

curl -X POST "http://ld-bp17j28j2y7pm****-proxy-ai-pub.lindorm.aliyuncs.com:9002/v1/ai/models/create" \
-H "x-ld-ak: <username>" \
-H "x-ld-sk: <password>" \
-H "Content-Type: application/json" \
-d '{
    "model_name": "bge_visual0",
    "model_path": "huggingface://BAAI/bge-visualized",
    "task": "FEATURE_EXTRACTION",
    "algorithm": "BGE_VISUALIZED_M3",    
    "settings": {
        "model_type": "ensemble",
        "instance_count": "4"
    }}'

List all models

curl -i -k --location --header 'x-ld-ak:<username>' --header 'x-ld-sk:<password>' --request GET 'http://ld-bp17j28j2y7pm****-proxy-ai-pub.lindorm.aliyuncs.com:9002/v1/ai/models/list'

Deployment process

In Jupyter, run the LindormQwenVlMultiModalSearch.ipynb script to build a high-quality image retrieval library by using the Qwen-VL image recognition model and Lindorm's multimodal models.

Step

Description

Engines involved

Create a search index

Create a search index table that contains a column for image vectors.

def create_search_index(self):
    body = {
        "settings": {
            "index": {
                "number_of_shards": 2,
                "knn": True
            }
        },
        "mappings": {
            "_source": {
                "excludes": ["image_embedding"]
            },
            "properties": {
                "image_embedding": {
                    "type": "knn_vector",
                    "dimension": self.dimension,
                    "data_type": "float",
                    "method": {
                        "engine": "lvector",
                        "name": "hnsw",
                        "space_type": "cosinesimil",
                        "parameters": {
                            "m": 24,
                            "ef_construction": 500
                        }
                    }
                },
                "description": {
                    "type": "text"
                }
            }
        }
    }
    print(self.index_name, self.client.indices.create(self.index_name, body=body, timeout=60))

Search Engine

Vector Engine

Ingest images

Process raw image data into vectors and write them to the index table.

  1. Function to process images.

    def handle_picture(file_path: str):
        # Read the content of the local image.
        with open(file_path, 'rb') as f:
            # print(f"Describing image {file_path}")
            code, description = lindorm.qwen_vl_picture_withdraw(file_path)
            if code != 0:
                print(f"Failed to describe image {file_path}, code {code}, error {description}")
                return None
            # print(f"Image described successfully {file_path}, description {description}")
            content = f.read()
            code, embedding = lindorm.image_text_embedding(content, description)
            if code != 0:
                print(f"Failed to vectorize image and text {file_path}, error {embedding}")
                raise Exception(f"Failed to vectorize image and text {file_path}, error {embedding}")
            return lindorm.write_doc_with_description(file_path, description, embedding)
  2. Use the Qwen-VL model to extract a text description from the image.

    def qwen_vl_picture_withdraw(self, file_path):
        """Simple single-round multimodal conversation call.
        """
        image_path = f"file://{file_path}"
    
        # Build the messages.
        messages = [
            {"role": "system", "content": [{"text": "You are a helpful assistant."}]},
            {"role": "user", "content": [{"image": image_path}, {"text": "What is depicted in the image?"}]}
        ]
    
        # Call the model.
        response = dashscope.MultiModalConversation.call(
            api_key=self.api_key,  # Use the API key directly if you have not configured environment variables.
            model='qwen-vl-max-latest',
            messages=messages
        )
    
        # If the status_code of the response is HTTPStatus.OK, the request is successful.
        # Otherwise, the request fails. You can get the error code and message.
        if response.status_code == HTTPStatus.OK:
            result = response.output.get('choices')[0].get('message').get('content')[0].get('text')
            return 0, result
        else:
            print(response)
            print(messages, response.code, response.message)  # The error code.
            # raise Exception("http request failed, status code: {}".format(response.message))
            return response.status_code, response.message
  3. Convert the text and image into a fused text-image vector by using the multimodal model.

    def image_text_embedding(self, image, text):
        encoded_str = base64.b64encode(image).decode('utf-8')
        data = {"input": [{"image": encoded_str, "text": text}]}
        return self.post_model_request(Config.LD_VL_MODEL, data)
    def post_model_request(self, model: str, data: dict):
        data = json.dumps(data)
        url = 'http://{}:{}/v1/ai/models/{}/infer'.format(Config.AI_HOST, Config.AI_PORT, model)
        try:
            result = requests.post(url, data=data, headers=self.headers, verify=False)
            # Make sure the request is successful.
            result.raise_for_status()
            # print(f'Request successful: {url}, Response: {result.json()}')
            return 0, result.json()['data'][0]
        except requests.exceptions.Timeout:
            return -1, "Request timed out"
        except requests.exceptions.HTTPError as http_err:
            return -1, f"HTTP error: {http_err}"
        except requests.exceptions.RequestException as err:
            return -1, f"An error occurred during the request: {err}"  
  4. Write the text and vector to the index table.

    def write_doc_with_description(self, url: str, description: str, embedding: list):
        element = {
            "image_embedding": embedding,
            "description": description
        }
        return self.client.index(self.index_name, element, id=url)

AI Engine

Search Engine

Vector Engine

Retrieval methods

  • Retrieval methods: search by text and search by image.

  • Search options: vector search and hybrid search (full-text and vector).

  • Image deduplication: Uses search by image to identify and filter out images that meet or exceed a specified similarity threshold, preventing duplicate imports and returning similar images for reference.

Retrieval method

Description

Engines involved

Search by text

Search for images by using text. You can use pure vector search or full-text and vector hybrid search.

  1. Enter a text description.

    def on_button_clicked(b):
        with output:
            clear_output()  # Clear the previous output.
            if text_input.value:
                code, embedding = lindorm.text_embedding(text_input.value)
            else:
                print("Please enter a description first.")
                return
            
            if b.description == "Pure Vector Search":
                hits = lindorm.knn_search(embedding)
            elif b.description == "RRF Hybrid Search":
                if text_input.value == '':
                    print("Please enter keywords first.")
                    return
                hits = lindorm.rrf_search(text_input.value, embedding)
            print("Search by text")
            show_hits(hits)
  2. Convert text to a vector.

    if text_input.value:
                code, embedding = lindorm.text_embedding(text_input.value)
    def text_embedding(self, text: str):
            data = {"input": [text]}
            return self.post_model_request(Config.LD_TEXT_MODEL, data)
         
  3. Retrieve images.

    • Vector search.

      if b.description == "Pure Vector Search":
          hits = lindorm.knn_search(embedding)
      def knn_search(self, picture_embedding):
              body = {
                  "query": {
                      "knn": {
                          "image_embedding": {
                              "vector": picture_embedding,
                              "k": self.top_k
                          }
                      }
                  },
                  "size": self.top_k
              }
              response = self.client.search(index=self.index_name, body=body)
              return response.get('hits').get('hits')
      
    • Hybrid search (full-text and vector).

      elif b.description == "RRF Hybrid Search":
          if text_input.value == '':
              print("Please enter keywords first.")
              return
          hits = lindorm.rrf_search(text_input.value, embedding)
      def rrf_search(self, text, embedding):
              query = {
                  "_source": ["description"],
                  "size": self.top_k,
                  "query": {
                      "knn": {
                          "image_embedding": {
                              "vector": embedding,
                              "filter": {
                                  "match": {
                                      "description": text
                                  }
                              },
                              "k": self.top_k
                          }
                      }
                  },
                  "ext": {"lvector": {
                      "hybrid_search_type": "filter_rrf",
                      "rrf_rank_constant": "60",
                      "rrf_knn_weight_factor": "0.4"
                  }}
              }
              response = self.client.search(index=self.index_name, body=query)
              return response.get('hits').get('hits')
              

AI Engine

Search Engine

Vector Engine

Search by image

Search for images by using an image. This method performs a vector search.

  1. Display the uploaded image.

    def show_uploaded_image(change):
        with output:
            clear_output()  # Clear the previous output.
            if change['new']:
                # Get the content of the uploaded file.
                uploaded_file = change['new'][0]
                content = uploaded_file['content']
                # Open the image by using PIL.
                image = Image.open(io.BytesIO(content))
                # Display the image by using matplotlib.
                plt.imshow(image)
                plt.axis('off')  # Do not display the axes.
                plt.show()
                code, embedding = lindorm.picture_embedding(content)
                hits = lindorm.knn_search(embedding)
                print("Image Upload Search")
                show_hits(hits)
  2. Convert the image to a vector.

    code , embedding = lindorm.picture_embedding(content)
    def picture_embedding(self, image):
        encoded_str = base64.b64encode(image).decode('utf-8')
        data = {"input": {"images": [encoded_str]}}
        return self.post_model_request(Config.LD_VL_MODEL, data)
  3. Retrieve images by performing a vector search.

    hits = lindorm.knn_search(embedding)
    def knn_search(self, picture_embedding):
            body = {
                "query": {
                    "knn": {
                        "image_embedding": {
                            "vector": picture_embedding,
                            "k": self.top_k
                        }
                    }
                },
                "size": self.top_k
            }
            response = self.client.search(index=self.index_name, body=body)
            return response.get('hits').get('hits')
        

AI Engine

Search Engine

Vector Engine

Search by image - image deduplication

Run LindormMultiModalSearch.ipynb to perform image deduplication.

  1. Create an index and ingest images.

    if __name__ == '__main__':
        print("start")
        lindorm = Lindorm("multimodal_search_index2")
        
        if lindorm.get_index() is None:
            # lindorm.drop_index()
            lindorm.create_search_index()
        print("Index created.")
        import_all_data(dir_input.value)
  2. Calculate image similarity.

    # Calculate the image vector.
    code, picture_embedding = lindorm.picture_embedding(image_data)
    hits = lindorm.knn_search(picture_embedding)
    
    # Display float_input.
    print('Image similarity threshold:', float_input.value)
    print('Image type:', image_type)
  3. If a similar image that meets the threshold already exists, the new image is not imported. Otherwise, it is added to the database.

    if hits[0].get('_score') >= float_input.value:
        print(f"An image with a similarity score >= {float_input.value} already exists. The new image will not be imported.")
    else:
        print(f"No image with a similarity score >= {float_input.value} exists. Importing this image.")
        dir = dir_input.value
        if not os.path.exists(dir):
            os.makedirs(dir)
        pic_name = str(uuid.uuid4()) + "." + image_type
        pic_path = os.path.join(dir, f"{os.path.basename(pic_name)}")
        import_new_image(image_data, pic_path, picture_embedding)

AI Engine

Search Engine

Vector Engine