Diseño de modelos de datos para búsqueda
Los sistemas de recuperación de información, también conocidos como motores de búsqueda, son esenciales para diversas aplicaciones de IA, como la generación aumentada de recuperación (RAG), la búsqueda visual y la recomendación de productos. En el núcleo de estos sistemas se encuentra un modelo de datos cuidadosamente diseñado para organizar, indexar y recuperar la información.
Milvus le permite especificar el modelo de datos de búsqueda mediante un esquema de colección, organizando los datos no estructurados, sus representaciones vectoriales densas o dispersas y los metadatos estructurados. Tanto si trabaja con texto, imágenes u otros tipos de datos, esta guía práctica le ayudará a comprender y aplicar los conceptos clave del esquema para diseñar un modelo de datos de búsqueda en la práctica.
Anatomía del modelo de datos
Modelo de datos
El diseño del modelo de datos de un sistema de búsqueda implica analizar las necesidades empresariales y abstraer la información en un modelo de datos expresado en un esquema. Un esquema bien definido es importante para alinear el modelo de datos con los objetivos empresariales, garantizando la coherencia de los datos y la calidad del servicio. Además, elegir los tipos de datos y el índice adecuados es importante para alcanzar el objetivo empresarial de forma económica.
Análisis de las necesidades empresariales
Para responder eficazmente a las necesidades de la empresa hay que empezar por analizar los tipos de consultas que realizarán los usuarios y determinar los métodos de búsqueda más adecuados.
Consultas de los usuarios: Identifique los tipos de consultas que se espera que realicen los usuarios. Esto ayuda a garantizar que su esquema es compatible con los casos de uso del mundo real y optimiza el rendimiento de la búsqueda. Por ejemplo
Recuperar documentos que coincidan con una consulta en lenguaje natural
Búsqueda de imágenes similares a una imagen de referencia o que coincidan con una descripción de texto
Búsqueda de productos por atributos como nombre, categoría o marca
Filtrado de elementos en función de metadatos estructurados (por ejemplo, fecha de publicación, etiquetas, valoraciones).
Combinación de varios criterios en consultas híbridas (por ejemplo, en la búsqueda visual, teniendo en cuenta la similitud semántica tanto de las imágenes como de sus pies de foto).
Métodos de búsqueda: Elija las técnicas de búsqueda apropiadas que se ajusten a los tipos de consultas que realizarán sus usuarios. Los distintos métodos sirven para diferentes propósitos y a menudo pueden combinarse para obtener resultados más potentes:
Búsqueda semántica: Utiliza la similitud de vectores densos para encontrar elementos con significado similar, ideal para datos no estructurados como texto o imágenes.
Búsqueda de texto completo: Complementa la búsqueda semántica con la concordancia de palabras clave. La búsqueda de texto completo puede utilizar el análisis léxico para evitar dividir palabras largas en tokens fragmentados, captando los términos especiales durante la recuperación.
Filtrado de metadatos: Además de la búsqueda vectorial, aplica restricciones como intervalos de fechas, categorías o etiquetas.
Traducir los requisitos empresariales en un modelo de datos de búsqueda
El siguiente paso consiste en traducir tus requisitos empresariales en un modelo de datos concreto, identificando los componentes básicos de tu información y sus métodos de búsqueda:
Defina los datos que necesita almacenar, como el contenido en bruto (texto, imágenes, audio), los metadatos asociados (títulos, etiquetas, autoría) y los atributos contextuales (marcas de tiempo, comportamiento del usuario, etc.).
Determine los tipos de datos y formatos adecuados para cada elemento. Por ejemplo:
Descripciones de texto → cadena
Incrustaciones de imágenes o documentos → vectores densos o dispersos
Categorías, etiquetas o banderas → cadena, matriz y bool
Atributos numéricos como precio o calificación → entero o flotante
Información estructurada como detalles del autor -> json
Una definición clara de estos elementos garantiza la coherencia de los datos, la precisión de los resultados de búsqueda y la facilidad de integración con las lógicas de las aplicaciones posteriores.
Diseño del esquema
En Milvus, el modelo de datos se expresa a través de un esquema de colección. El diseño de los campos correctos dentro de un esquema de colección es clave para permitir una recuperación eficaz. Cada campo define un tipo particular de datos almacenados en la colección y desempeña un papel distinto en el proceso de búsqueda. A grandes rasgos, Milvus admite dos tipos principales de campos: campos vectoriales y campos escalares.
Ahora, puede mapear su modelo de datos en un esquema de campos, incluyendo vectores y cualquier campo escalar auxiliar. Asegúrese de que cada campo se correlaciona con los atributos de su modelo de datos, especialmente preste atención a su tipo de vector (denso o spase) y su dimensión.
Campo vectorial
Los campos vectoriales almacenan incrustaciones de tipos de datos no estructurados como texto, imágenes y audio. Estas incrustaciones pueden ser densas, dispersas o binarias, dependiendo del tipo de datos y del método de recuperación utilizado. Normalmente, los vectores densos se utilizan para la búsqueda semántica, mientras que los vectores dispersos son más adecuados para la búsqueda de texto completo o léxica. Los vectores binarios son útiles cuando el almacenamiento y los recursos informáticos son limitados. Una colección puede contener varios campos vectoriales para permitir estrategias de recuperación multimodales o híbridas. Para obtener una guía detallada sobre este tema, consulte la Búsqueda híbrida multivectorial.
Milvus admite los tipos de datos vectoriales: FLOAT_VECTOR para vectores densos, SPARSE_FLOAT_VECTOR para vectores dispersos y BINARY_VECTOR para vectores binarios.
Campo escalar
Los campos escalares almacenan valores primitivos estructurados, comúnmente denominados metadatos, como números, cadenas o fechas. Estos valores pueden devolverse junto con los resultados de la búsqueda vectorial y son esenciales para filtrar y ordenar. Permiten acotar los resultados de la búsqueda en función de atributos específicos, como limitar los documentos a una categoría concreta o a un intervalo de tiempo definido.
Milvus admite tipos escalares como BOOL, INT8/16/32/64, FLOAT, DOUBLE, VARCHAR, JSON, y ARRAY para almacenar y filtrar datos no vectoriales. Estos tipos mejoran la precisión y la personalización de las operaciones de búsqueda.
Aprovechar las funciones avanzadas en el diseño de esquemas
Al diseñar un esquema, no basta con asignar los datos a los campos utilizando los tipos de datos admitidos. Es esencial conocer a fondo las relaciones entre los campos y las estrategias disponibles para su configuración. Tener en cuenta las características clave durante la fase de diseño garantiza que el esquema no sólo satisfaga los requisitos inmediatos de gestión de datos, sino que también sea escalable y adaptable a las necesidades futuras. Al integrar cuidadosamente estas características, puede construir una arquitectura de datos sólida que maximice las capacidades de Milvus y apoye su estrategia y objetivos de datos más amplios. He aquí un resumen de las características clave que crean un esquema de colección:
Clave primaria
Un campo de clave primaria es un componente fundamental de un esquema, ya que identifica de forma única a cada entidad dentro de una colección. Es obligatorio definir una clave primaria. Deberá ser un campo escalar de tipo entero o cadena y marcado como is_primary=True. Opcionalmente, puede activar auto_id para la clave primaria, a la que se asignan automáticamente números enteros que crecen monolíticamente a medida que se ingestan más datos en la colección.
Para más detalles, consulte Campo primario y AutoID.
Particionamiento
Para acelerar la búsqueda, puede activar opcionalmente la partición. Al designar un campo escalar específico para la partición y especificar criterios de filtrado basados en este campo durante las búsquedas, el alcance de la búsqueda puede limitarse eficazmente sólo a las particiones relevantes. Este método mejora significativamente la eficacia de las operaciones de recuperación al reducir el ámbito de búsqueda.
Para más detalles, consulte Utilizar clave de partición.
Analizador
Un analizador es una herramienta esencial para procesar y transformar datos de texto. Su función principal es convertir el texto bruto en tokens y estructurarlos para su indexación y recuperación. Para ello, tokeniza la cadena, elimina las palabras vacías y convierte las palabras individuales en tokens.
Para más información, consulte la sección Descripción general del analizador.
Función
Milvus le permite definir funciones integradas como parte del esquema para derivar automáticamente ciertos campos. Por ejemplo, puede añadir una función BM25 integrada que genere un vector disperso a partir de un campo VARCHAR para facilitar la búsqueda de texto completo. Estos campos derivados de funciones agilizan el preprocesamiento y garantizan que la colección siga siendo autónoma y esté preparada para las consultas.
Para más información, consulte Búsqueda de texto completo.
Un ejemplo real
En esta sección, describiremos el diseño del esquema y el ejemplo de código para una aplicación de búsqueda de documentos multimedia que se muestra en el diagrama anterior. Este esquema está diseñado para gestionar un conjunto de datos que contiene artículos con datos asignados a los siguientes campos:
Campo |
Fuente de datos |
Utilizado por los métodos de búsqueda |
Clave primaria |
Clave de partición |
Analizador |
Función Entrada/Salida |
|---|---|---|---|---|---|---|
article_id ( |
auto-generado con enabled |
Y |
N |
N |
N |
|
título ( |
título del artículo |
N |
N |
Y |
N |
|
marca de tiempo ( |
fecha de publicación |
N |
Y |
N |
N |
|
texto ( |
texto en bruto del artículo |
N |
N |
Y |
entrada |
|
vector_denso_texto ( |
vector denso generado por un modelo de incrustación de texto |
N |
N |
N |
N |
|
text_sparse_vector ( |
vector disperso autogenerado por una función BM25 integrada |
N |
N |
N |
salida |
Si desea más información sobre los esquemas y una guía detallada para añadir distintos tipos de campos, consulte Explicación de los esquemas.
Inicializar el esquema
Para empezar, necesitamos crear un esquema vacío. Este paso establece una estructura fundacional para definir el modelo de datos.
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
Añadir campos
Una vez creado el esquema, el siguiente paso es especificar los campos que compondrán sus datos. Cada campo está asociado a sus respectivos tipos de datos y atributos.
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
}"
En este ejemplo, se especifican los siguientes atributos para los campos:
Clave primaria: el
article_idse utiliza como clave primaria permitiendo la asignación automática de claves primarias para las entidades entrantes.Clave de partición: el
timestampse asigna como clave de partición permitiendo el filtrado por particiones. Puede serAnalizador de texto: el analizador de texto se aplica a 2 campos de cadena
titleytextpara soportar la coincidencia de texto y la búsqueda de texto completo respectivamente.
(Opcional) Añadir funciones
Para mejorar las capacidades de consulta de datos, se pueden incorporar funciones al esquema. Por ejemplo, se puede crear una función para procesar relacionados con campos específicos.
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
}"
Este ejemplo añade una función BM25 incorporada en el esquema, utilizando el campo text como entrada y almacenando los vectores dispersos resultantes en el campo text_sparse_vector.