Search your image collection using text descriptions or find visually similar images using CLIP embeddings.
Prerequisites
- Antfly running with Antfly inference and ONNX Runtime (CLIP requires ONNX)
- CLIP model:
antfly inference pull openai/clip-vit-base-patch32
Step 1: Create the Table
Create a table with a CLIP embeddings index. The template combines the image URL and caption for multimodal embedding:
// Step 2: Create the table with embeddings index
fmt.Println("Creating table 'images' with CLIP embeddings index...")
// Build the embedder config (union type)
var embedderConfig oapi.EmbedderConfig
embedderConfig.Provider = oapi.EmbedderProviderAntfly
embedderConfig.FromAntflyEmbedderConfig(oapi.AntflyEmbedderConfig{
Model: "openai/clip-vit-base-patch32",
})
// Build the path-identified index request.
indexConfig, err := antfly.NewCreateIndexRequest(oapi.EmbeddingsIndexConfig{
Dimension: 512,
Template: "{{media url=image_url}}{{caption}}",
Embedder: embedderConfig,
})
if err != nil {
log.Fatal(err)
}
err = client.CreateTable(ctx, "images", antfly.CreateTableRequest{
Indexes: map[string]antfly.CreateIndexRequest{
"embeddings": *indexConfig,
},
})
if err != nil {
errStr := err.Error()
if strings.Contains(errStr, "already exists") {
fmt.Println("Table 'images' already exists, continuing...")
} else if strings.Contains(errStr, "model not found") {
fmt.Printf("Warning: Embedder model not available (%v)\n", err)
fmt.Println("Creating table without embeddings index...")
err = client.CreateTable(ctx, "images", antfly.CreateTableRequest{})
if err != nil {
if strings.Contains(err.Error(), "already exists") {
fmt.Println("Table 'images' already exists, continuing...")
} else {
log.Fatalf("Failed to create table: %v", err)
}
} else {
fmt.Println("Created table 'images' (without embeddings)")
}
} else {
log.Fatalf("Failed to create table: %v", err)
}
} else {
fmt.Println("Created table 'images'")
}
Note: ONNX Runtime is experimental. If you encounter issues like "model not found" errors, empty results, or embeddings not being computed, try restarting Antfly. Check
antfly.logfor errors if problems persist.
Step 2: Add a Sample Image
Let's add the famous Utah teapot:
// Step 3: Add a sample image (Utah teapot)
fmt.Println("\nAdding Utah teapot sample image...")
_, err = client.Batch(ctx, "images", antfly.BatchRequest{
Inserts: map[string]any{
"utah_teapot": map[string]any{
"caption": "Utah teapot",
"image_url": "https://upload.wikimedia.org/wikipedia/commons/e/e7/Utah_teapot_simple_2.png",
},
},
})
if err != nil {
log.Printf("Warning: Failed to add teapot: %v", err)
} else {
fmt.Println("Added Utah teapot")
}
Antfly fetches and embeds the image automatically when using a URL.
Step 3: Search with Text
// Step 4: Search with text
fmt.Println("\nSearching for '3D model teapot'...")
results, err := client.Query(ctx, antfly.QueryRequest{
Table: "images",
SemanticSearch: "3D model teapot",
Indexes: []string{"embeddings"},
Limit: 5,
})
if err != nil {
log.Fatalf("Query failed: %v", err)
}
fmt.Println("\nSearch results:")
for _, resp := range results.Responses {
for _, hit := range resp.Hits.Hits {
fmt.Printf(" Score: %.4f, ID: %s\n", hit.Score, hit.ID)
}
}
The Utah teapot should appear as the top result:
Score: 0.0164, ID: utah_teapot
Score: 0.0161, ID: mmir_3bc4b3613ed9
Score: 0.0159, ID: mmir_83ca037bd2ad
...
Batch Import with Timing
For larger datasets, here's how to import images in bulk. This example uses the MMIR dataset from Google Research:
func batchImport(ctx context.Context, client *antfly.AntflyClient) {
numImages := 100
fmt.Printf("Importing first %d images...\n", numImages)
startTime := time.Now()
successCount := 0
f, err := os.Open("mmir_dataset.tsv.gz")
if err != nil {
log.Printf("Failed to open dataset: %v", err)
return
}
defer f.Close()
gz, err := gzip.NewReader(f)
if err != nil {
log.Printf("Failed to create gzip reader: %v", err)
return
}
defer gz.Close()
reader := csv.NewReader(gz)
reader.Comma = '\t'
reader.FieldsPerRecord = -1
reader.Read() // Skip header
for successCount < numImages {
row, err := reader.Read()
if err != nil {
break
}
if len(row) < 5 {
continue
}
imageURL, caption := row[0], row[4]
// Handle b'...' format in dataset
if strings.HasPrefix(imageURL, "b'") && strings.HasSuffix(imageURL, "'") {
imageURL = imageURL[2 : len(imageURL)-1]
}
if strings.HasPrefix(caption, "b'") && strings.HasSuffix(caption, "'") {
caption = caption[2 : len(caption)-1]
}
if !strings.HasPrefix(imageURL, "http") {
continue
}
hash := md5.Sum([]byte(imageURL))
docID := fmt.Sprintf("%x", hash[:6])
_, err = client.Batch(ctx, "images", antfly.BatchRequest{
Inserts: map[string]any{
"mmir_" + docID: map[string]any{"caption": caption, "image_url": imageURL},
},
})
if err == nil {
successCount++
fmt.Printf("\rImported: %d / %d", successCount, numImages)
}
}
elapsed := time.Since(startTime).Seconds()
fmt.Printf("\nImported %d images in %.1fs (%.1f images/sec)\n", successCount, elapsed, float64(successCount)/elapsed)
}
Example output:
Imported: 100 / 100
Imported 100 images in 45.2s (2.2 images/sec)
Running the Example
# From the repository root
go run ./examples/image-search
# Or build and run
go build -o examples/image-search/image-search ./examples/image-search
./examples/image-search/image-search
To run the batch import, first download the MMIR dataset:
curl -o mmir_dataset.tsv.gz "https://storage.googleapis.com/gresearch/wit-retrieval/mmir_dataset_train-00000-of-00005.tsv.gz"
Tips
Use visual descriptions: CLIP responds better to concrete visual concepts ("red sports car", "snowy mountain") than brand names or abstract terms.
Captions affect results: The template combines image + caption. For pure visual search, use "template": "{{media url=image_url}}" instead.
Related
- Multimodal Guide - PDFs, audio, and remote content
- Inference Models - Available CLIP variants