from vespa.package import ApplicationPackage
= ApplicationPackage(name="imagesearch") app_package
Image search
This notebook walks through the pyvespa code used to create the text to image search sample application.
ToDo: This notebook is still work in progress and cannot yet be auto-run
Create the application package
Create an application package:
Add a field to hold the name of the image file. This is used in the sample app to load the final images that should be displayed to the end user.
The summary
indexing ensures this field is returned as part of the query response. The attribute
indexing store the fields in memory as an attribute for sorting, querying, and grouping:
from vespa.package import Field
app_package.schema.add_fields(="image_file_name", type="string", indexing=["summary", "attribute"]),
Field(name )
Add a field to hold an image embedding. The embeddings are usually generated by a ML model. We can add multiple embedding fields to our application. This is useful when making experiments. For example, the sample app adds 6 image embeddings, one for each of the six pre-trained CLIP models available at the time.
In the example below, the embedding vector has size 512
and is of type float
. The index
is required to enable approximate matching and the HNSW
instance configure the HNSW index:
from vespa.package import HNSW
app_package.schema.add_fields(
Field(="embedding_image",
nametype="tensor<float>(x[512])",
=["attribute", "index"],
indexing=HNSW(
ann="angular",
distance_metric=16,
max_links_per_node=500,
neighbors_to_explore_at_insert
),
) )
Add a rank profile that ranks the images by how close the image embedding vector is from the query embedding vector. The tensors used in queries must have their type declared in the application package, the code below declares the text embedding that will be sent in the query - it has the same size and type of the image embedding:
from vespa.package import RankProfile
app_package.schema.add_rank_profile(
RankProfile(="embedding_similarity",
name="default",
inherits="closeness(embedding_image)",
first_phase=[("query(embedding_text)", "tensor<float>(x[512])")],
inputs
) )
Deploy the application
The application package created above can be deployed using Docker or Vespa Cloud. Follow the instructions based on the desired deployment mode. Either option will create a Vespa connection instance that can be stored in a variable that will be denoted here as app
.
We can then use app
to interact with the deployed application:
import os
from vespa.deployment import VespaDocker
= VespaDocker(
vespa_docker =8080
port
)
= vespa_docker.deploy(application_package = app_package) app
Waiting for configuration server, 0/300 seconds...
Waiting for configuration server, 5/300 seconds...
Waiting for application status, 0/300 seconds...
Waiting for application status, 5/300 seconds...
Waiting for application status, 10/300 seconds...
Waiting for application status, 15/300 seconds...
Waiting for application status, 20/300 seconds...
Waiting for application status, 25/300 seconds...
Finished deployment.
Feed the image data
ToDo: Add code below to create the feed and set batch - until then, disabled auto testing.
To feed the image data:
= app.feed_batch(batch) responses
where batch
is a list of dictionaries like the one below:
{"id": "dog1",
"fields": {
"image_file_name": "dog1.jpg",
"embedding_image": {"values": [0.884, -0.345, ..., 0.326]},
} }
One of the advantages of having a python API is that it can integrate with commonly used ML frameworks. The sample application show how to create a PyTorch DataLoader to generate batches of image data by using CLIP models to generate image embeddings.
Query the application
The following query will use approximate nearest neighbor search to match the closest images to the query text and rank the images according to their distance to the query text. The sample application used CLIP models to generate image and query embeddings.
= app.query(body={
response "yql": 'select * from sources * where ({targetHits:100}nearestNeighbor(embedding_image,embedding_text));',
"hits": 100,
"input.query(embedding_text)": [0.632, -0.987, ..., 0.534],
"ranking.profile": "embedding_similarity"
})
Evaluate different query models
Define metrics to evaluate:
from learntorank.evaluation import MatchRatio, Recall, ReciprocalRank
= [
eval_metrics
MatchRatio(), =100),
Recall(at=100)
ReciprocalRank(at ]
The sample application illustrates how to evaluate different CLIP models through the evaluate
method:
= app.evaluate(
result =labeled_data, # Labeled data to define which images should be returned to a given query
labeled_data=eval_metrics, # Metrics used
eval_metrics=query_models, # Each query model uses a different CLIP model version
query_model="image_file_name", # The name of the id field used by the labeled data to identify the image
id_field=True # Return results per query rather the aggragated.
per_query )
The figure below is the reciprocal rank at 100 computed based on the output of the evaluate
method.