Passer au contenu principal
Le jeu de données Laion-400M contient 400 millions d’images avec des légendes en anglais. Laion propose désormais un jeu de données encore plus vaste, mais son utilisation sera similaire. Le jeu de données contient l’URL de l’image, les embeddings de l’image et de sa légende, un score de similarité entre l’image et sa légende, ainsi que des métadonnées, par exemple la largeur et la hauteur de l’image, la licence et un indicateur NSFW. Nous pouvons utiliser ce jeu de données pour illustrer la recherche approximative des plus proches voisins dans ClickHouse.

Préparation des données

Les embeddings et les métadonnées sont stockés dans des fichiers distincts parmi les données brutes. Une étape de préparation des données permet de télécharger les données, de fusionner les fichiers, de les convertir en CSV et de les importer dans ClickHouse. Vous pouvez utiliser pour cela le script download.sh suivant :
number=${1}
if [[ $number == '' ]]; then
    number=1
fi;
wget --tries=100 https://deploy.laion.ai/8f83b608504d46bb81708ec86e912220/embeddings/img_emb/img_emb_${number}.npy          # download image embedding
wget --tries=100 https://deploy.laion.ai/8f83b608504d46bb81708ec86e912220/embeddings/text_emb/text_emb_${number}.npy        # download text embedding
wget --tries=100 https://deploy.laion.ai/8f83b608504d46bb81708ec86e912220/embeddings/metadata/metadata_${number}.parquet    # download metadata
python3 process.py $number # merge files and convert to CSV
Le script process.py est défini comme suit :
import pandas as pd
import numpy as np
import os
import sys

str_i = str(sys.argv[1])
npy_file = "img_emb_" + str_i + '.npy'
metadata_file = "metadata_" + str_i + '.parquet'
text_npy =  "text_emb_" + str_i + '.npy'

# load all files
im_emb = np.load(npy_file)
text_emb = np.load(text_npy) 
data = pd.read_parquet(metadata_file)

# combine files
data = pd.concat([data, pd.DataFrame({"image_embedding" : [*im_emb]}), pd.DataFrame({"text_embedding" : [*text_emb]})], axis=1, copy=False)

# columns to be imported into ClickHouse
data = data[['url', 'caption', 'NSFW', 'similarity', "image_embedding", "text_embedding"]]

# transform np.arrays to lists
data['image_embedding'] = data['image_embedding'].apply(lambda x: x.tolist())
data['text_embedding'] = data['text_embedding'].apply(lambda x: x.tolist())

# this small hack is needed because caption sometimes contains all kind of quotes
data['caption'] = data['caption'].apply(lambda x: x.replace("'", " ").replace('"', " "))

# export data as CSV file
data.to_csv(str_i + '.csv', header=False)

# removed raw data files
os.system(f"rm {npy_file} {metadata_file} {text_npy}")
Pour lancer le pipeline de préparation des données, exécutez :
seq 0 409 | xargs -P1 -I{} bash -c './download.sh {}'
Le jeu de données est réparti en 410 fichiers, chacun contenant env. 1 million de lignes. Si vous souhaitez travailler sur un sous-ensemble plus restreint des données, modifiez simplement les limites, par ex. seq 0 9 | .... (Le script Python ci-dessus est très lent (~2 à 10 minutes par fichier), consomme beaucoup de mémoire (41 GB par fichier), et les fichiers CSV obtenus sont volumineux (10 GB chacun) ; soyez donc prudent. Si vous avez suffisamment de RAM, augmentez la valeur de -P1 pour accroître le parallélisme. Si cela reste trop lent, envisagez une meilleure procédure d’ingestion — par exemple en convertissant les fichiers .npy en Parquet, puis en effectuant tout le reste du traitement avec ClickHouse.)

Créer une table

Pour créer une table sans index dans un premier temps, exécutez :
CREATE TABLE laion
(
    `id` Int64,
    `url` String,
    `caption` String,
    `NSFW` String,
    `similarity` Float32,
    `image_embedding` Array(Float32),
    `text_embedding` Array(Float32)
)
ENGINE = MergeTree
ORDER BY id
Pour importer les fichiers CSV dans ClickHouse :
INSERT INTO laion FROM INFILE '{path_to_csv_files}/*.csv'
Notez que la colonne id n’est là qu’à titre d’illustration et que le script la renseigne avec des valeurs non uniques. Pour effectuer une recherche vectorielle approximative par force brute, exécutez :
SELECT url, caption FROM laion ORDER BY cosineDistance(image_embedding, {target:Array(Float32)}) LIMIT 10
target est un tableau de 512 éléments et un paramètre client. Une méthode pratique pour obtenir de tels tableaux sera présentée à la fin de l’article. Pour l’instant, nous pouvons utiliser comme target l’embedding d’une image aléatoire d’un set LEGO. Résultat
    ┌─url───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬─caption──────────────────────────────────────────────────────────────────────────┐
 1. │ https://s4.thcdn.com/productimg/600/600/11340490-9914447026352671.jpg                                                                                                                         │ LEGO Friends: Puppy Treats & Tricks (41304)                                      │
 2. │ https://www.avenuedelabrique.com/img/uploads/f20fd44bfa4bd49f2a3a5fad0f0dfed7d53c3d2f.jpg                                                                                                     │ Nouveau LEGO Friends 41334 Andrea s Park Performance 2018                        │
 3. │ http://images.esellerpro.com/2489/I/667/303/3938_box_in.jpg                                                                                                                                   │ 3938 LEGO Andreas Bunny House Girls Friends Heartlake Age 5-12 / 62 Pieces  New! │
 4. │ http://i.shopmania.org/180x180/7/7f/7f1e1a2ab33cde6af4573a9e0caea61293dfc58d.jpg?u=https%3A%2F%2Fs.s-bol.com%2Fimgbase0%2Fimagebase3%2Fextralarge%2FFC%2F4%2F0%2F9%2F9%2F9200000049789904.jpg │ LEGO Friends Avonturenkamp Boomhuis - 41122                                      │
 5. │ https://s.s-bol.com/imgbase0/imagebase/large/FC/5/5/9/4/1004004011684955.jpg                                                                                                                  │ LEGO Friends Andrea s Theatershow - 3932                                         │
 6. │ https://www.jucariicucubau.ro/30252-home_default/41445-lego-friends-ambulanta-clinicii-veterinare.jpg                                                                                         │ 41445 - LEGO Friends - Ambulanta clinicii veterinare                             │
 7. │ https://cdn.awsli.com.br/600x1000/91/91201/produto/24833262/234c032725.jpg                                                                                                                    │ LEGO FRIENDS 41336 EMMA S ART CAFÉ                                               │
 8. │ https://media.4rgos.it/s/Argos/6174930_R_SET?$Thumb150$&$Web$                                                                                                                             │ more details on LEGO Friends Stephanie s Friendship Cake Set - 41308.            │
 9. │ https://thumbs4.ebaystatic.com/d/l225/m/mG4k6qAONd10voI8NUUMOjw.jpg                                                                                                                           │ Lego Friends Gymnast 30400 Polybag 26 pcs                                        │
10. │ http://www.ibrickcity.com/wp-content/gallery/41057/thumbs/thumbs_lego-41057-heartlake-horse-show-friends-3.jpg                                                                                │ lego-41057-heartlake-horse-show-friends-3                                        │
    └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────┘

10 rows in set. Elapsed: 4.605 sec. Processed 100.38 million rows, 309.98 GB (21.80 million rows/s., 67.31 GB/s.)

Effectuer une recherche approximative de similarité vectorielle avec un index de similarité vectorielle

Définissons maintenant deux index de similarité vectorielle sur la table.
ALTER TABLE laion ADD INDEX image_index image_embedding TYPE vector_similarity('hnsw', 'cosineDistance', 512, 'bf16', 64, 256)
ALTER TABLE laion ADD INDEX text_index text_embedding TYPE vector_similarity('hnsw', 'cosineDistance', 512, 'bf16', 64, 256)
Les paramètres et les considérations de performance pour la création de l’index et la recherche sont décrits dans la documentation. La définition d’index ci-dessus spécifie un index HNSW utilisant la “distance cosinus” comme métrique de distance, avec le paramètre “hnsw_max_connections_per_layer” défini sur 64 et le paramètre “hnsw_candidate_list_size_for_construction” défini sur 256. L’index utilise des nombres à virgule flottante bfloat16 en demi-précision comme quantification afin d’optimiser l’utilisation de la mémoire. Pour construire et matérialiser l’index, exécutez ces instructions SQL :
ALTER TABLE laion MATERIALIZE INDEX image_index;
ALTER TABLE laion MATERIALIZE INDEX text_index;
La création et l’enregistrement de l’index peuvent prendre quelques minutes, voire plusieurs heures, selon le nombre de lignes et les paramètres de l’index HNSW. Pour effectuer une recherche vectorielle, il suffit d’exécuter à nouveau la même requête :
SELECT url, caption FROM laion ORDER BY cosineDistance(image_embedding, {target:Array(Float32)}) LIMIT 10
Résultat
    ┌─url───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬─caption──────────────────────────────────────────────────────────────────────────┐
 1. │ https://s4.thcdn.com/productimg/600/600/11340490-9914447026352671.jpg                                                                                                                         │ LEGO Friends: Puppy Treats & Tricks (41304)                                      │
 2. │ https://www.avenuedelabrique.com/img/uploads/f20fd44bfa4bd49f2a3a5fad0f0dfed7d53c3d2f.jpg                                                                                                     │ Nouveau LEGO Friends 41334 Andrea s Park Performance 2018                        │
 3. │ http://images.esellerpro.com/2489/I/667/303/3938_box_in.jpg                                                                                                                                   │ 3938 LEGO Andreas Bunny House Girls Friends Heartlake Age 5-12 / 62 Pieces  New! │
 4. │ http://i.shopmania.org/180x180/7/7f/7f1e1a2ab33cde6af4573a9e0caea61293dfc58d.jpg?u=https%3A%2F%2Fs.s-bol.com%2Fimgbase0%2Fimagebase3%2Fextralarge%2FFC%2F4%2F0%2F9%2F9%2F9200000049789904.jpg │ LEGO Friends Avonturenkamp Boomhuis - 41122                                      │
 5. │ https://s.s-bol.com/imgbase0/imagebase/large/FC/5/5/9/4/1004004011684955.jpg                                                                                                                  │ LEGO Friends Andrea s Theatershow - 3932                                         │
 6. │ https://www.jucariicucubau.ro/30252-home_default/41445-lego-friends-ambulanta-clinicii-veterinare.jpg                                                                                         │ 41445 - LEGO Friends - Ambulanta clinicii veterinare                             │
 7. │ https://cdn.awsli.com.br/600x1000/91/91201/produto/24833262/234c032725.jpg                                                                                                                    │ LEGO FRIENDS 41336 EMMA S ART CAFÉ                                               │
 8. │ https://media.4rgos.it/s/Argos/6174930_R_SET?$Thumb150$&$Web$                                                                                                                             │ more details on LEGO Friends Stephanie s Friendship Cake Set - 41308.            │
 9. │ https://thumbs4.ebaystatic.com/d/l225/m/mG4k6qAONd10voI8NUUMOjw.jpg                                                                                                                           │ Lego Friends Gymnast 30400 Polybag 26 pcs                                        │
10. │ http://www.ibrickcity.com/wp-content/gallery/41057/thumbs/thumbs_lego-41057-heartlake-horse-show-friends-3.jpg                                                                                │ lego-41057-heartlake-horse-show-friends-3                                        │
    └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────┘

10 rows in set. Elapsed: 0.019 sec. Processed 137.27 thousand rows, 24.42 MB (7.38 million rows/s., 1.31 GB/s.)
La latence de la requête a fortement diminué, car les plus proches voisins ont été récupérés à l’aide de l’index vectoriel. La recherche de similarité vectorielle utilisant un index de similarité vectorielle peut renvoyer des résultats légèrement différents de ceux d’une recherche par force brute. Un index HNSW peut potentiellement atteindre un taux de rappel proche de 1 (la même précision qu’une recherche par force brute) grâce à une sélection rigoureuse des paramètres HNSW et à l’évaluation de la qualité de l’index.

Création d’embeddings avec des UDF

On cherche généralement à créer des embeddings pour de nouvelles images ou de nouvelles légendes d’image, puis à rechercher dans les données des paires image/légende similaires. On peut utiliser des UDF pour créer le vecteur target sans quitter le client. Il est important d’utiliser le même modèle pour créer les données et les nouveaux embeddings utilisés pour les recherches. Les scripts suivants utilisent le modèle ViT-B/32, qui est également celui sur lequel repose le jeu de données.

Embeddings de texte

Commencez par enregistrer le script Python suivant dans le répertoire user_scripts/ de votre répertoire de données ClickHouse, puis rendez-le exécutable (chmod +x encode_text.py). encode_text.py:
#!/usr/bin/python3
#!Note: Change the above python3 executable location if a virtual env is being used.
import clip
import torch
import numpy as np
import sys

if __name__ == '__main__':
    device = "cuda" if torch.cuda.is_available() else "cpu"
    model, preprocess = clip.load("ViT-B/32", device=device)
    for text in sys.stdin:
        inputs = clip.tokenize(text)
        with torch.no_grad():
            text_features = model.encode_text(inputs)[0].tolist()
            print(text_features)
        sys.stdout.flush()
Créez ensuite encode_text_function.xml à un emplacement indiqué par <user_defined_executable_functions_config>/path/to/*_function.xml</user_defined_executable_functions_config> dans le fichier de configuration du serveur ClickHouse.
<functions>
    <function>
        <type>executable</type>
        <name>encode_text</name>
        <return_type>Array(Float32)</return_type>
        <argument>
            <type>String</type>
            <name>text</name>
        </argument>
        <format>TabSeparated</format>
        <command>encode_text.py</command>
        <command_read_timeout>1000000</command_read_timeout>
    </function>
</functions>
Vous pouvez désormais utiliser :
SELECT encode_text('cat');
La première exécution sera lente, car elle charge le modèle, mais les suivantes seront rapides. Nous pouvons ensuite copier le résultat dans SET param_target=... et écrire facilement des requêtes. Sinon, la fonction encode_text() peut être utilisée directement comme argument de la fonction cosineDistance :
SELECT url
FROM laion
ORDER BY cosineDistance(text_embedding, encode_text('a dog and a cat')) ASC
LIMIT 10
Notez que l’UDF encode_text() elle-même peut nécessiter quelques secondes pour effectuer le calcul et renvoyer le vecteur d’embedding.

Embeddings d’images

Les embeddings d’images peuvent être créés de la même manière, et nous fournissons un script Python capable de générer un embedding à partir d’une image stockée localement sous forme de fichier. encode_image.py
#!/usr/bin/python3
#!Note: Change the above python3 executable location if a virtual env is being used.
import clip
import torch
import numpy as np
from PIL import Image
import sys

if __name__ == '__main__':
    device = "cuda" if torch.cuda.is_available() else "cpu"
    model, preprocess = clip.load("ViT-B/32", device=device)
    for text in sys.stdin:
        image = preprocess(Image.open(text.strip())).unsqueeze(0).to(device)
        with torch.no_grad():
            image_features = model.encode_image(image)[0].tolist()
            print(image_features)
        sys.stdout.flush()
encode_image_function.xml
<functions>
    <function>
        <type>executable_pool</type>
        <name>encode_image</name>
        <return_type>Array(Float32)</return_type>
        <argument>
            <type>String</type>
            <name>path</name>
        </argument>
        <format>TabSeparated</format>
        <command>encode_image.py</command>
        <command_read_timeout>1000000</command_read_timeout>
    </function>
</functions>
Récupérez une image d’exemple pour la recherche :
# get a random image of a LEGO set
$ wget http://cdn.firstcry.com/brainbees/images/products/thumb/191325a.jpg
Exécutez ensuite cette requête pour générer l’embedding de l’image ci-dessus :
SELECT encode_image('/path/to/your/image');
La requête de recherche complète est :
SELECT
    url,
    caption
FROM laion
ORDER BY cosineDistance(image_embedding, encode_image('/path/to/your/image')) ASC
LIMIT 10
Dernière modification le 29 juin 2026