Progettazione del modello di dati per la ricerca
I sistemi di recupero delle informazioni, noti anche come motori di ricerca, sono essenziali per varie applicazioni di intelligenza artificiale, come la RAG (Retrieval-augmented generation), la ricerca visiva e la raccomandazione di prodotti. Alla base di questi sistemi c'è un modello di dati accuratamente progettato per organizzare, indicizzare e recuperare le informazioni.
Milvus consente di specificare il modello di dati di ricerca attraverso uno schema di raccolta, organizzando i dati non strutturati, le loro rappresentazioni vettoriali dense o rade e i metadati strutturati. Sia che si lavori con testo, immagini o altri tipi di dati, questa guida pratica vi aiuterà a capire e ad applicare i concetti chiave di schema per progettare un modello di dati di ricerca nella pratica.
Anatomia del modello di dati
Modello di dati
La progettazione del modello di dati di un sistema di ricerca comporta l'analisi delle esigenze aziendali e l'astrazione delle informazioni in un modello di dati espresso in forma di schema. Uno schema ben definito è importante per allineare il modello di dati agli obiettivi aziendali, garantendo la coerenza dei dati e la qualità del servizio. Inoltre, la scelta di tipi di dati e indici adeguati è importante per raggiungere l'obiettivo aziendale in modo economico.
Analizzare le esigenze aziendali
Per rispondere efficacemente alle esigenze aziendali è necessario analizzare i tipi di query che gli utenti eseguiranno e determinare i metodi di ricerca più adatti.
Query degli utenti: Identificare i tipi di query che gli utenti dovrebbero eseguire. Questo aiuta a garantire che lo schema supporti i casi d'uso reali e ottimizzi le prestazioni di ricerca. Queste possono includere
Recupero di documenti che corrispondono a una query in linguaggio naturale.
Trovare immagini simili a un'immagine di riferimento o corrispondenti a una descrizione testuale
Ricerca di prodotti in base ad attributi quali il nome, la categoria o il marchio
Filtrare gli articoli in base a metadati strutturati (ad esempio, data di pubblicazione, tag, valutazioni).
Combinazione di più criteri in query ibride (ad esempio, nella ricerca visiva, considerando la somiglianza semantica delle immagini e delle loro didascalie).
Metodi di ricerca: Scegliere le tecniche di ricerca più adatte ai tipi di query che gli utenti eseguiranno. I diversi metodi hanno scopi diversi e spesso possono essere combinati per ottenere risultati più efficaci:
Ricerca semantica: Utilizza la similarità vettoriale densa per trovare elementi con un significato simile, ideale per dati non strutturati come testo o immagini.
Ricerca full-text: Completa la ricerca semantica con la corrispondenza delle parole chiave. La ricerca full-text può utilizzare l'analisi lessicale per evitare di scomporre parole lunghe in token frammentati, cogliendo i termini speciali durante il recupero.
Filtraggio dei metadati: In aggiunta alla ricerca vettoriale, applicazione di vincoli come intervalli di date, categorie o tag.
Tradurre i requisiti aziendali in un modello di dati di ricerca
Il passo successivo consiste nel tradurre i requisiti aziendali in un modello di dati concreto, identificando i componenti principali delle informazioni e i relativi metodi di ricerca:
Definire i dati da memorizzare, come i contenuti grezzi (testo, immagini, audio), i metadati associati (titoli, tag, paternità) e gli attributi contestuali (timestamp, comportamento dell'utente, ecc.).
Determinare i tipi e i formati di dati appropriati per ciascun elemento. Ad esempio:
Descrizioni di testo → stringa
Incorporamenti di immagini o documenti → vettori densi o sparsi
Categorie, tag o flag → stringa, array e bool
Attributi numerici come il prezzo o la valutazione → integer o float
Informazioni strutturate come i dettagli dell'autore -> json
Una chiara definizione di questi elementi garantisce la coerenza dei dati, l'accuratezza dei risultati di ricerca e la facilità di integrazione con le logiche applicative a valle.
Progettazione dello schema
In Milvus, il modello dei dati è espresso attraverso uno schema di raccolta. La progettazione dei campi giusti all'interno di uno schema di raccolta è fondamentale per consentire un recupero efficace. Ogni campo definisce un particolare tipo di dati memorizzati nella collezione e svolge un ruolo distinto nel processo di ricerca. Ad alto livello, Milvus supporta due tipi principali di campi: campi vettoriali e campi scalari.
Ora è possibile mappare il modello di dati in uno schema di campi, compresi i vettori e gli eventuali campi scalari ausiliari. Assicurarsi che ogni campo sia correlato agli attributi del modello di dati, prestando particolare attenzione al tipo di vettore (denso o spase) e alla sua dimensione.
Campo vettoriale
I campi vettoriali memorizzano le incorporazioni per i tipi di dati non strutturati, come testo, immagini e audio. Queste incorporazioni possono essere dense, rade o binarie, a seconda del tipo di dati e del metodo di recupero utilizzato. In genere, i vettori densi sono utilizzati per la ricerca semantica, mentre i vettori radi sono più adatti per la ricerca full-text o lessicale. I vettori binari sono utili quando le risorse di memoria e di calcolo sono limitate. Una collezione può contenere diversi campi vettoriali per consentire strategie di recupero multimodali o ibride. Per una guida dettagliata su questo argomento, consultare la Ricerca ibrida multivettoriale.
Milvus supporta i tipi di dati vettoriali: FLOAT_VECTOR per Dense Vector, SPARSE_FLOAT_VECTOR per Sparse Vector e BINARY_VECTOR per Binary Vector.
Campo scalare
I campi scalari memorizzano valori primitivi e strutturati, comunemente chiamati metadati, come numeri, stringhe o date. Questi valori possono essere restituiti insieme ai risultati della ricerca vettoriale e sono essenziali per il filtraggio e l'ordinamento. Permettono di restringere i risultati della ricerca in base ad attributi specifici, come limitare i documenti a una particolare categoria o a un intervallo di tempo definito.
Milvus supporta tipi scalari come BOOL, INT8/16/32/64, FLOAT, DOUBLE, VARCHAR, JSON, e ARRAY per memorizzare e filtrare dati non vettoriali. Questi tipi migliorano la precisione e la personalizzazione delle operazioni di ricerca.
Sfruttare le funzionalità avanzate nella progettazione degli schemi
Quando si progetta uno schema, non è sufficiente mappare i dati nei campi utilizzando i tipi di dati supportati. È essenziale comprendere a fondo le relazioni tra i campi e le strategie disponibili per la configurazione. Tenere a mente le caratteristiche chiave durante la fase di progettazione assicura che lo schema non solo soddisfi i requisiti immediati di gestione dei dati, ma sia anche scalabile e adattabile alle esigenze future. Integrando attentamente queste caratteristiche, è possibile costruire una solida architettura di dati che massimizza le capacità di Milvus e supporta la strategia e gli obiettivi più ampi in materia di dati. Ecco una panoramica delle caratteristiche principali per la creazione di uno schema di raccolta:
Chiave primaria
Il campo chiave primaria è un componente fondamentale di uno schema, in quanto identifica in modo univoco ogni entità all'interno di una raccolta. La definizione di una chiave primaria è obbligatoria. Deve essere un campo scalare di tipo intero o stringa e contrassegnato come is_primary=True. Opzionalmente, è possibile abilitare auto_id per la chiave primaria, alla quale vengono assegnati automaticamente numeri interi che crescono monoliticamente man mano che vengono inseriti altri dati nella raccolta.
Per ulteriori dettagli, consultare Campo primario e AutoID.
Partizionamento
Per accelerare la ricerca, è possibile attivare il partizionamento. Designando un campo scalare specifico per il partizionamento e specificando i criteri di filtraggio basati su questo campo durante le ricerche, è possibile limitare efficacemente l'ambito di ricerca alle sole partizioni pertinenti. Questo metodo migliora significativamente l'efficienza delle operazioni di recupero, riducendo l'ambito di ricerca.
Per ulteriori dettagli, fare riferimento a Utilizzare la chiave di partizione.
Analizzatore
L'analizzatore è uno strumento essenziale per l'elaborazione e la trasformazione dei dati di testo. La sua funzione principale è quella di convertire il testo grezzo in token e di strutturarli per l'indicizzazione e il recupero. A tal fine, l'analizzatore effettua la tokenizzazione della stringa, l'eliminazione delle stop words e lo stemming delle singole parole in tokens.
Per ulteriori dettagli, consultare la sezione Panoramica dell'analizzatore.
Funzione
Milvus consente di definire funzioni integrate nello schema per ricavare automaticamente alcuni campi. Ad esempio, è possibile aggiungere una funzione incorporata BM25 che genera un vettore rado da un campo VARCHAR per supportare la ricerca full-text. Questi campi derivati da funzioni semplificano la preelaborazione e assicurano che la raccolta rimanga autonoma e pronta per le interrogazioni.
Per ulteriori dettagli, consultare la sezione Ricerca a testo completo.
Un esempio del mondo reale
In questa sezione, illustreremo il progetto dello schema e l'esempio di codice per un'applicazione di ricerca di documenti multimediali mostrata nel diagramma precedente. Questo schema è stato progettato per gestire un set di dati contenente articoli con mappatura dei dati nei seguenti campi:
Campo |
Fonte dei dati |
Utilizzato dai metodi di ricerca |
Chiave primaria |
Chiave di partizione |
Analizzatore |
Funzione Ingresso/Uscita |
|---|---|---|---|---|---|---|
article_id ( |
autogenerato con abilitato |
Y |
N |
N |
N |
|
titolo ( |
titolo dell'articolo |
N |
N |
Y |
N |
|
timestamp ( |
data di pubblicazione |
N |
Y |
N |
N |
|
testo ( |
testo grezzo dell'articolo |
N |
N |
Y |
input |
|
vettore_testo_denso ( |
vettore denso generato da un modello di incorporazione del testo |
N |
N |
N |
N |
|
text_sparse_vector ( |
vettore sparse autogenerato da una funzione BM25 integrata |
N |
N |
N |
output |
Per ulteriori informazioni sugli schemi e per una guida dettagliata sull'aggiunta di vari tipi di campi, consultare Schema Explained.
Inizializzare lo schema
Per iniziare, è necessario creare uno schema vuoto. Questo passo stabilisce una struttura di base per la definizione del modello dei dati.
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
Aggiungere campi
Una volta creato lo schema, il passo successivo è quello di specificare i campi che comporranno i dati. Ogni campo è associato ai rispettivi tipi di dati e attributi.
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
}"
In questo esempio, per i campi sono specificati i seguenti attributi:
Chiave primaria:
article_idè utilizzata come chiave primaria, consentendo l'assegnazione automatica delle chiavi primarie per le entità in arrivo.Chiave di partizione:
timestampè assegnata come chiave di partizione, consentendo il filtraggio per partizioni. Questa potrebbe essereAnalizzatore di testo: l'analizzatore di testo viene applicato ai due campi stringa
titleetextper supportare rispettivamente la corrispondenza del testo e la ricerca full-text.
(Opzionale) Aggiungere funzioni
Per migliorare le capacità di interrogazione dei dati, è possibile incorporare delle funzioni nello schema. Ad esempio, si può creare una funzione per elaborare i dati relativi a campi specifici.
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
}"
Questo esempio aggiunge una funzione BM25 integrata nello schema, che utilizza il campo text come input e memorizza i vettori sparsi risultanti nel campo text_sparse_vector.