Conception d'un modèle de données pour la recherche
Les systèmes de recherche d'informations, également connus sous le nom de moteurs de recherche, sont essentiels pour diverses applications d'intelligence artificielle telles que la génération augmentée par la recherche (RAG), la recherche visuelle et la recommandation de produits. Au cœur de ces systèmes se trouve un modèle de données soigneusement conçu pour organiser, indexer et récupérer les informations.
Milvus vous permet de spécifier le modèle de données de recherche par le biais d'un schéma de collection, organisant les données non structurées, leurs représentations vectorielles denses ou éparses et les métadonnées structurées. Que vous travailliez avec du texte, des images ou d'autres types de données, ce guide pratique vous aidera à comprendre et à appliquer les concepts clés des schémas pour concevoir un modèle de données de recherche dans la pratique.
Anatomie du modèle de données
Modèle de données
La conception du modèle de données d'un système de recherche implique l'analyse des besoins de l'entreprise et l'abstraction des informations dans un modèle de données exprimé par un schéma. Un schéma bien défini est important pour aligner le modèle de données sur les objectifs de l'entreprise et garantir la cohérence des données et la qualité du service. En outre, le choix de types de données et d'index appropriés est important pour atteindre l'objectif de l'entreprise de manière économique.
Analyse des besoins de l'entreprise
Pour répondre efficacement aux besoins de l'entreprise, il faut d'abord analyser les types de requêtes que les utilisateurs effectueront et déterminer les méthodes de recherche les plus appropriées.
Requêtes des utilisateurs : Identifiez les types de requêtes que les utilisateurs sont censés effectuer. Cela permet de s'assurer que votre schéma prend en charge les cas d'utilisation réels et optimise les performances de recherche. Il peut s'agir de
la récupération de documents correspondant à une requête en langage naturel
la recherche d'images similaires à une image de référence ou correspondant à une description textuelle
la recherche de produits en fonction d'attributs tels que le nom, la catégorie ou la marque
le filtrage d'éléments sur la base de métadonnées structurées (par exemple, la date de publication, les étiquettes, les évaluations)
Combinaison de plusieurs critères dans des requêtes hybrides (par exemple, dans le cas d'une recherche visuelle, prise en compte de la similarité sémantique des images et de leurs légendes).
Méthodes de recherche : Choisissez les techniques de recherche appropriées qui correspondent aux types de requêtes que vos utilisateurs effectueront. Les différentes méthodes ont des objectifs différents et peuvent souvent être combinées pour obtenir des résultats plus performants :
Recherche sémantique: Elle utilise la similarité vectorielle dense pour trouver des éléments ayant une signification similaire, ce qui est idéal pour les données non structurées telles que les textes ou les images.
Recherche en texte intégral: Complète la recherche sémantique avec la recherche par mots-clés. La recherche en texte intégral peut utiliser l'analyse lexicale pour éviter de diviser les mots longs en jetons fragmentés, en saisissant les termes spéciaux lors de la recherche.
Filtrage des métadonnées: En plus de la recherche vectorielle, il s'agit d'appliquer des contraintes telles que des plages de dates, des catégories ou des étiquettes.
Traduire les besoins de l'entreprise en un modèle de données de recherche
L'étape suivante consiste à traduire vos exigences professionnelles en un modèle de données concret, en identifiant les principaux composants de vos informations et leurs méthodes de recherche :
Définir les données à stocker, telles que le contenu brut (texte, images, audio), les métadonnées associées (titres, balises, auteur) et les attributs contextuels (horodatage, comportement de l'utilisateur, etc.).
Déterminer les types et formats de données appropriés pour chaque élément. Par exemple :
Description de texte → chaîne de caractères
Incrustations d'images ou de documents → vecteurs denses ou épars
Catégories, étiquettes ou drapeaux → chaîne, tableau et bool
Attributs numériques tels que le prix ou l'évaluation → integer ou float
Informations structurées telles que les détails de l'auteur -> json
Une définition claire de ces éléments garantit la cohérence des données, la précision des résultats de recherche et la facilité d'intégration avec les logiques d'application en aval.
Conception du schéma
Dans Milvus, le modèle de données est exprimé par un schéma de collection. La conception des champs appropriés dans un schéma de collection est essentielle pour permettre une recherche efficace. Chaque champ définit un type particulier de données stockées dans la collection et joue un rôle distinct dans le processus de recherche. Au niveau le plus élevé, Milvus prend en charge deux types principaux de champs : les champs vectoriels et les champs scalaires.
Vous pouvez maintenant mapper votre modèle de données dans un schéma de champs, y compris les vecteurs et tous les champs scalaires auxiliaires. Assurez-vous que chaque champ est en corrélation avec les attributs de votre modèle de données, en accordant une attention particulière à votre type de vecteur (dense ou spase) et à sa dimension.
Champ vectoriel
Les champs vectoriels stockent les données intégrées pour les types de données non structurées tels que le texte, les images et l'audio. Ces encastrements peuvent être denses, épars ou binaires, en fonction du type de données et de la méthode d'extraction utilisée. En règle générale, les vecteurs denses sont utilisés pour la recherche sémantique, tandis que les vecteurs épars conviennent mieux à la recherche plein texte ou à la recherche lexicale. Les vecteurs binaires sont utiles lorsque les ressources de stockage et de calcul sont limitées. Une collection peut contenir plusieurs champs de vecteurs pour permettre des stratégies de recherche multimodales ou hybrides. Pour un guide détaillé sur ce sujet, veuillez vous référer à la Recherche hybride multi-vecteurs.
Milvus prend en charge les types de données vectorielles : FLOAT_VECTOR pour Dense Vector, SPARSE_FLOAT_VECTOR pour Sparse Vector et BINARY_VECTOR pour Binary Vector.
Champ scalaire
Les champs scalaires stockent des valeurs primitives et structurées, communément appelées métadonnées, telles que des nombres, des chaînes de caractères ou des dates. Ces valeurs peuvent être renvoyées avec les résultats d'une recherche vectorielle et sont essentielles pour le filtrage et le tri. Elles permettent de restreindre les résultats de la recherche en fonction d'attributs spécifiques, par exemple en limitant les documents à une catégorie particulière ou à une période définie.
Milvus prend en charge les types scalaires tels que BOOL, INT8/16/32/64, FLOAT, DOUBLE, VARCHAR, JSON et ARRAY pour le stockage et le filtrage des données non vectorielles. Ces types améliorent la précision et la personnalisation des opérations de recherche.
Exploiter les fonctionnalités avancées dans la conception des schémas
Lors de la conception d'un schéma, il ne suffit pas de faire correspondre vos données aux champs en utilisant les types de données pris en charge. Il est essentiel de bien comprendre les relations entre les champs et les stratégies disponibles pour la configuration. Le fait de garder à l'esprit les caractéristiques clés au cours de la phase de conception garantit que le schéma répond non seulement aux exigences immédiates en matière de traitement des données, mais qu'il est également évolutif et adaptable aux besoins futurs. En intégrant soigneusement ces fonctionnalités, vous pouvez construire une architecture de données solide qui maximise les capacités de Milvus et prend en charge votre stratégie et vos objectifs plus larges en matière de données. Voici une vue d'ensemble des principales caractéristiques permettant de créer un schéma de collecte :
Clé primaire
Un champ de clé primaire est un composant fondamental d'un schéma, car il identifie de manière unique chaque entité au sein d'une collection. La définition d'une clé primaire est obligatoire. Il doit s'agir d'un champ scalaire de type entier ou chaîne et marqué comme is_primary=True. En option, vous pouvez activer auto_id pour la clé primaire, qui se voit automatiquement attribuer des nombres entiers qui augmentent de manière monolithique au fur et à mesure que des données sont ingérées dans la collection.
Pour plus de détails, voir Champ primaire et AutoID.
Partitionnement
Pour accélérer la recherche, vous pouvez activer le partitionnement. En désignant un champ scalaire spécifique pour le partitionnement et en spécifiant des critères de filtrage basés sur ce champ pendant les recherches, l'étendue de la recherche peut être efficacement limitée aux seules partitions pertinentes. Cette méthode améliore considérablement l'efficacité des opérations de recherche en réduisant le domaine de recherche.
Pour plus de détails, voir Utiliser la clé de partition.
Analyseur
Un analyseur est un outil essentiel pour le traitement et la transformation des données textuelles. Sa fonction principale est de convertir le texte brut en jetons et de les structurer pour l'indexation et la recherche. Pour ce faire, l'analyseur procède à la tokenisation de la chaîne de caractères, à la suppression des mots vides et à la troncature des mots individuels en tokens.
Pour plus de détails, reportez-vous à la section Vue d'ensemble de l'analyseur.
Fonction
Milvus vous permet de définir des fonctions intégrées dans le cadre du schéma pour dériver automatiquement certains champs. Par exemple, vous pouvez ajouter une fonction BM25 intégrée qui génère un vecteur épars à partir d'un champ VARCHAR pour prendre en charge la recherche en texte intégral. Ces champs dérivés de fonctions simplifient le prétraitement et garantissent que la collection reste autonome et prête à être interrogée.
Pour plus de détails, voir Recherche en texte intégral.
Un exemple concret
Dans cette section, nous décrivons la conception du schéma et l'exemple de code pour une application de recherche de documents multimédias illustrée dans le diagramme ci-dessus. Ce schéma est conçu pour gérer un ensemble de données contenant des articles dont les données correspondent aux champs suivants :
Champ |
Source des données |
Utilisé par les méthodes de recherche |
Clé primaire |
Clé de partition |
Analyseur |
Fonction Entrée/Sortie |
|---|---|---|---|---|---|---|
article_id ( |
auto-généré avec activé |
Y |
N |
N |
N |
|
titre ( |
titre de l'article |
N |
N |
Y |
N |
|
horodatage ( |
date de publication |
N |
Y |
N |
N |
|
texte ( |
texte brut de l'article |
N |
N |
Y |
entrée |
|
vecteur_dense_texte ( |
vecteur dense généré par un modèle d'intégration de texte |
N |
N |
N |
N |
|
vecteur_sparse_texte ( |
vecteur clairsemé généré automatiquement par une fonction BM25 intégrée |
N |
N |
N |
sortie |
Pour plus d'informations sur les schémas et des conseils détaillés sur l'ajout de différents types de champs, veuillez vous référer au document Schema Explained.
Initialisation du schéma
Pour commencer, nous devons créer un schéma vide. Cette étape permet d'établir une structure de base pour définir le modèle de données.
from pymilvus import MilvusClient
schema = MilvusClient.create_schema()
import io.milvus.v2.client.ConnectConfig;
import io.milvus.v2.client.MilvusClientV2;
import io.milvus.v2.service.collection.request.CreateCollectionReq;
// 1. Connect to Milvus server
ConnectConfig connectConfig = ConnectConfig.builder()
.uri("http://localhost:19530")
.build();
MilvusClientV2 client = new MilvusClientV2(connectConfig);
// 2. Create an empty schema
CreateCollectionReq.CollectionSchema schema = client.createSchema();
import { MilvusClient, DataType } from "@zilliz/milvus2-sdk-node";
//Skip this step using JavaScript
import "github.com/milvus-io/milvus/client/v2/entity"
schema := entity.NewSchema()
# Skip this step using cURL
Ajouter des champs
Une fois le schéma créé, l'étape suivante consiste à spécifier les champs qui composeront vos données. Chaque champ est associé à un type de données et à des attributs.
from pymilvus import DataType
schema.add_field(field_name="article_id", datatype=DataType.INT64, is_primary=True, auto_id=True, description="article id")
schema.add_field(field_name="title", datatype=DataType.VARCHAR, enable_analyzer=True, enable_match=True, max_length=200, description="article title")
schema.add_field(field_name="timestamp", datatype=DataType.INT32, description="publish date")
schema.add_field(field_name="text", datatype=DataType.VARCHAR, max_length=2000, enable_analyzer=True, description="article text content")
schema.add_field(field_name="text_dense_vector", datatype=DataType.FLOAT_VECTOR, dim=768, description="text dense vector")
schema.add_field(field_name="text_sparse_vector", datatype=DataType.SPARSE_FLOAT_VECTOR, description="text sparse vector")
import io.milvus.v2.common.DataType;
import io.milvus.v2.service.collection.request.AddFieldReq;
schema.addField(AddFieldReq.builder()
.fieldName("article_id")
.dataType(DataType.Int64)
.isPrimaryKey(true)
.autoID(true)
.build());
schema.addField(AddFieldReq.builder()
.fieldName("title")
.dataType(DataType.VarChar)
.maxLength(200)
.enableAnalyzer(true)
.enableMatch(true)
.build());
schema.addField(AddFieldReq.builder()
.fieldName("timestamp")
.dataType(DataType.Int32)
.build())
schema.addField(AddFieldReq.builder()
.fieldName("text")
.dataType(DataType.VarChar)
.maxLength(2000)
.enableAnalyzer(true)
.build());
schema.addField(AddFieldReq.builder()
.fieldName("text_dense_vector")
.dataType(DataType.FloatVector)
.dimension(768)
.build());
schema.addField(AddFieldReq.builder()
.fieldName("text_sparse_vector")
.dataType(DataType.SparseFloatVector)
.build());
const fields = [
{
name: "article_id",
data_type: DataType.Int64,
is_primary_key: true,
auto_id: true
},
{
name: "title",
data_type: DataType.VarChar,
max_length: 200,
enable_analyzer: true,
enable_match: true
},
{
name: "timestamp",
data_type: DataType.Int32
},
{
name: "text",
data_type: DataType.VarChar,
max_length: 2000,
enable_analyzer: true
},
{
name: "text_dense_vector",
data_type: DataType.FloatVector,
dim: 768
},
{
name: "text_sparse_vector",
data_type: DataType.SparseFloatVector
}
]
schema.WithField(entity.NewField().
WithName("article_id").
WithDataType(entity.FieldTypeInt64).
WithIsPrimaryKey(true).
WithIsAutoID(true).
WithDescription("article id"),
).WithField(entity.NewField().
WithName("title").
WithDataType(entity.FieldTypeVarChar).
WithMaxLength(200).
WithEnableAnalyzer(true).
WithEnableMatch(true).
WithDescription("article title"),
).WithField(entity.NewField().
WithName("timestamp").
WithDataType(entity.FieldTypeInt32).
WithDescription("publish date"),
).WithField(entity.NewField().
WithName("text").
WithDataType(entity.FieldTypeVarChar).
WithMaxLength(2000).
WithEnableAnalyzer(true).
WithDescription("article text content"),
).WithField(entity.NewField().
WithName("text_dense_vector").
WithDataType(entity.FieldTypeFloatVector).
WithDim(768).
WithDescription("text dense vector"),
).WithField(entity.NewField().
WithName("text_sparse_vector").
WithDataType(entity.FieldTypeSparseVector).
WithDescription("text sparse vector"),
)
export fields='[
{
"fieldName": "article_id",
"dataType": "Int64",
"isPrimary": true
},
{
"fieldName": "title",
"dataType": "VarChar",
"elementTypeParams": {
"max_length": 200,
"enable_analyzer": true,
"enable_match": true
}
},
{
"fieldName": "timestamp",
"dataType": "Int32"
},
{
"fieldName": "text",
"dataType": "VarChar",
"elementTypeParams": {
"max_length": 2000,
"enable_analyzer": true
}
},
{
"fieldName": "text_dense_vector",
"dataType": "FloatVector",
"elementTypeParams": {
"dim": 768
}
},
{
"fieldName": "text_sparse_vector",
"dataType": "SparseFloatVector",
}
]'
export schema="{
\"autoID\": true,
\"fields\": $fields
}"
Dans cet exemple, les attributs suivants sont spécifiés pour les champs :
Clé primaire : le site
article_idest utilisé comme clé primaire, ce qui permet d'attribuer automatiquement des clés primaires aux entités entrantes.Clé de partition : le site
timestampest attribué comme clé de partition, ce qui permet de filtrer les entités par partition. Il peut s'agirAnalyseur de texte : l'analyseur de texte est appliqué à deux champs de chaîne
titleettextpour prendre en charge respectivement la correspondance de texte et la recherche en texte intégral.
(Facultatif) Ajouter des fonctions
Pour améliorer les capacités d'interrogation des données, des fonctions peuvent être incorporées dans le schéma. Par exemple, une fonction peut être créée pour traiter des champs spécifiques.
from pymilvus import Function, FunctionType
bm25_function = Function(
name="text_bm25",
input_field_names=["text"],
output_field_names=["text_sparse_vector"],
function_type=FunctionType.BM25,
)
schema.add_function(bm25_function)
import io.milvus.common.clientenum.FunctionType;
import io.milvus.v2.service.collection.request.CreateCollectionReq.Function;
import java.util.*;
schema.addFunction(Function.builder()
.functionType(FunctionType.BM25)
.name("text_bm25")
.inputFieldNames(Collections.singletonList("text"))
.outputFieldNames(Collections.singletonList("text_sparse_vector"))
.build());
import FunctionType from "@zilliz/milvus2-sdk-node";
const functions = [
{
name: 'text_bm25',
description: 'bm25 function',
type: FunctionType.BM25,
input_field_names: ['text'],
output_field_names: ['text_sparse_vector'],
params: {},
},
];
function := entity.NewFunction().
WithName("text_bm25").
WithInputFields("text").
WithOutputFields("text_sparse_vector").
WithType(entity.FunctionTypeBM25)
schema.WithFunction(function)
export myFunctions='[
{
"name": "text_bm25",
"type": "BM25",
"inputFieldNames": ["text"],
"outputFieldNames": ["text_sparse_vector"],
"params": {}
}
]'
export schema="{
\"autoID\": true,
\"fields\": $fields
\"functions\": $myFunctions
}"
Cet exemple ajoute une fonction BM25 intégrée au schéma, utilisant le champ text comme entrée et stockant les vecteurs épars résultants dans le champ text_sparse_vector.