Ti presentiamo Loon: un nuovo motore di archiviazione per dati vettoriali che non smettono mai di cambiare
Punti chiave
Questa è un’analisi ingegneristica lunga e approfondita, quindi ecco i punti chiave prima di entrare nei dettagli.
- I dataset AI non sono tabelle statiche. Le stesse righe continuano a cambiare mentre i team sostituiscono i modelli di embedding, aggiungono vettori sparsi, rivedono le didascalie, integrano etichette mancanti, ricostruiscono indici ed eseguono analisi offline.
- I layout di storage tradizionali si rompono in tre modi: le colonne vettoriali lunghe rendono costosi i backfill, un singolo formato di file non può servire bene sia le scansioni sia le letture puntuali, e lo storage privato dei database costringe le pipeline esterne a creare copie aggiuntive della fonte di verità.
- Loon è il nuovo motore di storage per Milvus e Zilliz Vector Lakebase. È costruito attorno a formati di file ibridi, allineamento degli ID di riga e un Manifest che definisce lo stato versionato del dataset.
- L’obiettivo è consentire a un singolo dataset vettoriale di supportare ricerca online, analisi offline, backfill, compattazione e calcolo esterno senza copiare, riscrivere o reimportare continuamente i dati.
Introduzione
Per un po’, c’è stato un argomento contro i database vettoriali che sembrava ragionevole.
I database tradizionali memorizzano già interi, stringhe, JSON, blob e indici. Perché non aggiungere un tipo _vector_ , costruirci accanto un indice ANN e chiuderla lì?
Per la ricerca semantica iniziale, funziona abbastanza bene. Una colonna vettoriale più un indice può supportare una demo, una piccola applicazione RAG o una funzionalità di ricerca interna. Il problema emerge più tardi, quando il dataset inizia a comportarsi meno come una tabella e più come un sistema di dati AI.
Un dataset vettoriale di produzione ha righe, chiavi primarie, campi scalari e colonne interrogabili. In questo senso, assomiglia a una tabella di database. Ma ha anche la scala e la forma di workflow di un data lake. Può contenere centinaia di milioni di record. Viene letto e riscritto ripetutamente da Spark, Ray, DuckDB, pipeline di training, job di valutazione e sistemi di qualità dei dati.
Dipende anche dallo storage a oggetti. Gli oggetti sorgente sono spesso video, immagini, PDF, file audio o documenti web che rimangono in S3, GCS, OSS o un altro object store. Il database memorizza riferimenti, metadati, feature derivate e indici. Poi aggiunge elementi che i modelli di storage tradizionali non sono stati progettati per gestire come oggetti di prima classe: embedding densi, vettori sparsi, didascalie, indici vettoriali, indici testuali, log di cancellazione, statistiche, versioni di modello, versioni di parser, riferimenti a blob esterni e le relazioni di versione tra tutti questi elementi.
È qui che “basta aggiungere una colonna vettoriale” inizia a cedere. Il punto non è se un database possa memorizzare byte vettoriali. Molti sistemi possono farlo. La domanda più difficile è se il modello di storage possa gestire come cambiano i dati vettoriali, come vengono interrogati e come vengono condivisi nell’intero stack di dati AI.
Per questo abbiamo costruito Loon, il nuovo motore di storage per Milvus e Zilliz Vector Lakebase (la prossima evoluzione di Zilliz Cloud).
Loon è progettato attorno a tre idee:
- Usare formati fisici diversi per tipi diversi di colonne.
- Allineare quelle colonne attraverso uno spazio di ID di riga condiviso.
- Usare un Manifest per definire lo stato versionato del dataset.
Per capire perché questi elementi siano importanti, partiamo da un workflow multimodale comune.
Un dataset vettoriale non è mai davvero finito.
Immagina un team AI che costruisce un dataset video per il training multimodale.
Un video lungo viene caricato nello storage a oggetti. Una pipeline lo suddivide in clip in base a cambi di scena, limiti di inquadratura o finestre temporali. Le clip troppo lunghe o troppo corte, sfocate, duplicate o di bassa qualità vengono filtrate. Le clip rimanenti vengono valutate da un modello estetico, descritte con didascalie da un altro modello, trasformate in embedding da un modello visione-linguaggio e memorizzate in un database vettoriale per ricerca, deduplicazione e filtraggio dei dati di training.
Ad alto livello, il workflow sembra semplice:
video
→ clip
→ metadati
→ aesthetic_score
→ didascalia
→ embedding
→ ricerca / dedup / filtraggio dei dati di training
Ma il dataset non arriva già completamente formato.
- Nella prima settimana, la tabella può contenere solo
clip_id,video_id,start_offseteduration. - Nella seconda settimana, il team aggiunge
aesthetic_score. - Nella terza settimana, viene eseguito un modello di captioning e ogni clip riceve una
caption. - Nella quarta settimana, il primo modello di embedding va online e ogni clip riceve un embedding CLIP a 768 dimensioni.
- Un mese dopo, il team cambia modello ed esegue il backfill di
embedding_v2, ora con 1024 dimensioni. - Due mesi dopo, la ricerca ibrida diventa un requisito, quindi il team aggiunge una colonna di vettori sparsi.
- Tre mesi dopo, le didascalie vengono sottoposte a revisione umana e devono essere corrette in loco.
Il dataset non è mai stato completato. Ha continuato ad accumulare nuove interpretazioni delle stesse righe sottostanti.
Questa è una delle differenze fondamentali tra i dati vettoriali e i dati aziendali tradizionali. La stessa riga viene rielaborata più e più volte. E la scala trasforma questo aspetto da un inconveniente in un problema di storage: i dataset multimodali spesso non contengono milioni di record, ma centinaia di milioni o miliardi. LAION-5B è un riferimento utile per capirne la forma — miliardi di coppie immagine-testo, ciascuna con metadati, didascalie ed embedding. Quindi la parte difficile non è il primo inserimento. La parte difficile è tutto ciò che accade dopo che il dataset inizia a evolvere. Questa evoluzione mette in luce tre problemi.
Il primo problema: le colonne lunghe rendono costosa la write amplification
I formati colonnari come Parquet sono eccellenti per molti carichi di lavoro analitici. Funzionano bene quando gli schemi sono abbastanza stabili, i dati vengono letti più spesso di quanto vengano riscritti, le scansioni toccano solo un sottoinsieme di colonne e la compressione è importante. Questo è il mondo per cui molti formati analitici sono stati ottimizzati.
Le righe vettoriali sono molto più larghe delle righe analitiche
TPC-H lineitem è un buon riferimento di base. Ha 16 colonne: chiavi intere, valori decimali, date, stringhe brevi e un piccolo campo commento. Una riga non compressa è di circa 150 byte. Dopo la compressione, può essere molto più piccola. Con un row group da 64 MB, un sistema di storage può comprimere centinaia di migliaia di righe in un gruppo.
I dataset vettoriali non hanno questo aspetto.
Un dataset immagine-testo in stile LAION è molto più vicino a ciò che molte pipeline AI producono oggi. Ogni riga ha ancora metadati ordinari: un URL, una didascalia, larghezza, altezza, punteggi di qualità, etichette e così via. Ma una volta aggiunto l’embedding, la forma fisica della riga cambia.
Un vettore CLIP a 768 dimensioni occupa circa 1,5 KB in fp16 o 3 KB in fp32. Quella singola colonna può essere molto più grande di un’intera riga TPC-H lineitem.
E 768 dimensioni non sono insolite o grandi per gli standard odierni. Un embedding a 1024 o 2048 dimensioni è comune nelle pipeline multimodali. text-embedding-3-large di OpenAI arriva fino a 3072 dimensioni, cioè circa 12 KB per vettore in fp32.
Il confronto è netto:
| Forma del dataset | Dimensione approssimativa della riga | Campo dominante |
|---|---|---|
| TPC-H lineitem | ~150 byte non compressi | scalari e stringhe brevi |
| Riga in stile LAION con vettore fp16 a 768 dim | ~1,5 KB+ | embedding |
| Riga in stile LAION con vettore fp32 a 768 dim | ~3 KB+ | embedding |
| Riga con vettore fp32 a 3072 dim | ~12 KB+ solo per il vettore | embedding |
In molti dataset AI, la colonna vettoriale non è solo un altro campo. Fisicamente, costituisce la maggior parte della riga. Questo cambia il costo dell’evoluzione dello schema.
Aggiungere una sola colonna vettoriale può significare centinaia di gigabyte
Supponiamo che un dataset abbia 100 milioni di clip video. Aggiungere una nuova colonna di embedding fp32 a 1024 dimensioni significa scrivere circa 400 GB di dati vettoriali grezzi. Questo non include statistiche, indici, aggiornamenti dei metadati, overhead dello storage a oggetti, validazione o integrazione nel percorso di serving.
Se il team aggiunge una o due colonne simili a vettori ogni mese, come embedding_v2, sparse_vector o funzionalità di rerank, l’evoluzione dello schema diventa un lavoro ricorrente di data engineering misurato in centinaia di gigabyte o terabyte.
Piccoli aggiornamenti logici possono innescare grandi riscritture fisiche
Gli aggiornamenti sono altrettanto importanti.
Nei sistemi colonnari, i vecchi dati di solito non vengono aggiornati in loco. Un log di eliminazione registra ciò che è cambiato, e la compattazione in seguito riscrive le righe attive in nuovi file. Questo modello è gestibile quando le righe sono piccole.
Con i dati vettoriali, un piccolo aggiornamento logico può innescare una grande riscrittura fisica.
Un processo di revisione umana può limitarsi a correggere poche centinaia di byte in una didascalia. Ma se la didascalia, il vettore denso, il vettore sparso e altre funzionalità derivate condividono lo stesso ciclo di vita del file fisico, il sistema può finire per riscrivere anche i vettori. La modifica logica è piccola. L’I/O fisico può essere enorme.
Questo è il problema dell’amplificazione delle scritture nello storage vettoriale. La parte costosa non è solo che i vettori sono grandi. È che grandi campi derivati e piccoli campi mutabili vengono spesso legati insieme da un layout di storage che li tratta come un’unica unità.
Per i dataset di IA, il backfill è un carico di lavoro di routine
Per le tabelle analitiche tradizionali, l’evoluzione dello schema può verificarsi solo occasionalmente. Per i dataset di IA, è routine. I modelli di didascalie vengono aggiornati. I modelli di embedding vengono sostituiti. I vettori sparsi vengono aggiunti in seguito. Compaiono funzionalità di rerank. Le etichette umane vengono corrette. I tag di governance vengono sottoposti a backfill. Gli indici vengono ricostruiti.
Queste operazioni non sono semplici append. Modificano o estendono frequentemente righe esistenti.
Ecco perché lo storage vettoriale non può ottimizzare solo per il throughput di scansione. Deve anche rendere più economici i backfill e gli aggiornamenti parziali.
Il secondo problema: gli stessi dati devono supportare scansioni e letture puntuali
Dopo che i dati sono stati scritti, il percorso di lettura si divide. Lo stesso dataset vettoriale ha in genere due modelli di accesso distinti: scansione analitica e letture puntuali.
I carichi di lavoro analitici richiedono scansioni ampie e compresse
Una pipeline può eseguire filtri come:
WHERE aesthetic_score > 0.8 AND duration > 5
Oppure può eseguire analisi offline, valutazione completa degli embedding, statistiche BM25, costruzione di bitmap, controlli di qualità dei dati, conteggi e group-by.
Questo modello legge molte righe ma solo poche colonne. Predilige I/O sequenziale, row group più grandi, compressione, pruning delle colonne, decodifica batch ed esecuzione vettorializzata.
I row group grandi aiutano in questo caso. Consentono a una singola richiesta di I/O di recuperare una grande quantità di dati utili, migliorano l’efficienza della compressione e forniscono al motore di esecuzione dati contigui sufficienti per ammortizzare l’overhead. Quando più colonne vengono lette insieme, mantenerle organizzate per il throughput di scansione aiuta anche a ridurre i cache miss durante l’esecuzione vettorializzata.
Parquet è forte su questo percorso.
I risultati ANN richiedono lookup stretti a livello di riga
Dopo che la ricerca ANN restituisce gli ID delle righe candidate, il sistema spesso deve recuperare campi come:
caption
embedding
rerank feature
video_uri
metadata
Questo modello legge meno righe, spesso centinaia o migliaia, ma richiede accesso preciso per ID riga. Vuole individuare una riga e una colonna specifiche, recuperare solo l’intervallo di byte richiesto ed evitare di caricare un intero row group solo per recuperare pochi record.
Il lookup puntuale ha preferenze quasi opposte rispetto alla scansione. Vuole una granularità di lettura più piccola. Idealmente, il livello di storage può trovare il segmento o l’intervallo di byte rilevante tramite ID riga, leggere solo quell’intervallo e decodificare solo i dati necessari per il risultato.
Anche la compressione presenta un tradeoff diverso. Per le scansioni, una compressione più pesante spesso vale la pena perché il sistema legge molti dati e risparmia I/O. Per il lookup puntuale, la compressione può diventare uno svantaggio se recuperare una riga richiede la decodifica di un blocco compresso molto più grande.
Un unico layout non può ottimizzare entrambi i percorsi
Questo è il conflitto centrale. Il filtraggio scalare e le analytics vogliono layout ampi, compressi e ottimizzati per la scansione. La ricerca vettoriale vuole layout stretti, precisi e indirizzabili per riga.
Un singolo formato di file può supportare entrambi in una certa misura, ma non può essere ottimale per entrambi contemporaneamente.
Se tutte le colonne vivono in Parquet, le scansioni scalari sono agevoli. Ma la ricerca ANN dopo il recall diventa più difficile. Il sistema potrebbe avere bisogno solo di poche centinaia di vettori, caption o record di metadati, mentre il livello di storage potrebbe dover leggere grandi row group che contengono soprattutto righe irrilevanti.
Su un SSD locale, cache e mmap possono nascondere parte di questo costo. Una volta che i dati sono archiviati in object storage, il costo diventa più visibile. Ogni cache miss può diventare una lettura remota di intervallo. Se le righe candidate sono sparse su molti row group, una singola query può attivare più letture, ognuna delle quali recupera più dati di quelli necessari alla query. In un layout progettato male, il recupero di 1.000 righe candidate può facilmente comportare decine o centinaia di megabyte di I/O non necessario e, in casi estremi, molto di più.
Rendere i row group più piccoli aiuta la ricerca puntuale, ma danneggia le scansioni. Troppi piccoli frammenti riducono l’efficienza della compressione, aumentano l’overhead dei metadati e interrompono le lunghe letture sequenziali da cui dipendono i motori analitici.
Quindi il problema non è trovare una singola dimensione magica del row group. Il problema è che allo stesso dataset viene chiesto di comportarsi come due sistemi di storage diversi.
La ricerca ibrida forza entrambi i percorsi in una sola query
La ricerca ibrida rende il conflitto più difficile da ignorare. Una singola query può prima applicare filtri scalari:
aesthetic_score > 0.8 AND duration > 5
Poi esegue la ricerca ANN.
Poi recupera caption, vettore e metadati tramite ID riga.
Per l’utente, questa è un’unica richiesta di ricerca. Per il livello di storage, è sia una scansione analitica sia una ricerca casuale a bassa latenza.
Ecco perché lo storage vettoriale ha bisogno di più di una migliore impostazione di Parquet. Ha bisogno di un modo per collocare colonne diverse in base a come vengono effettivamente lette.
Il terzo problema: il dataset non vive dentro un solo motore
I primi due problemi si verificano all’interno del database. Il terzo si verifica al confine tra sistemi.
Le pipeline di dati AI attraversano molti sistemi
Nel workflow video, molto poco accade all’interno del database vettoriale stesso.
I video grezzi vivono in object storage. La generazione delle clip può essere eseguita in Spark o Ray. Il punteggio estetico può essere calcolato in un servizio GPU. La generazione delle caption può essere eseguita in una pipeline di inferenza LLM. Gli embedding possono essere generati da un altro job GPU. I vettori sparsi possono provenire da un servizio SPLADE. Valutazione offline, filtraggio dei dati di training, revisione umana e job di governance possono tutti essere eseguiti altrove.
Il database vettoriale serve la ricerca online, ma il dataset viene prodotto, corretto, valutato ed esteso da molti sistemi.
I formati di storage privati creano più copie della verità
Se il database usa un formato fisico privato che solo esso può leggere e scrivere, ogni job esterno necessita di un export, una conversione, una copia e un import. La stessa collection può esistere nel database, in una directory temporanea Spark, in un output di valutazione e in una directory locale di backfill. A quel punto la vera domanda diventa:
- Quale copia è la fonte di verità?
- Quale contiene il modello di caption del mese scorso?
- Quali righe sono già state corrette dalla revisione umana?
- Quale colonna di vettori sparsi è stata generata da quale modello?
- Quale indice vettoriale è ancora valido dopo il backfill?
- A quale oggetto video originale si riferisce questa riga?
Su piccola scala, i team a volte riescono a cavarsela con convenzioni di denominazione e controlli manuali. Con centinaia di milioni di righe e terabyte di embedding, questo diventa un problema di coerenza.
I dataset vettoriali hanno bisogno di uno stato condiviso e versionato
I sistemi Lakehouse hanno affrontato una versione di questo problema per i dati strutturati. Iceberg, Delta Lake e Hudi non riguardano soltanto l’archiviazione di file. Il loro contributo principale è permettere a più motori di coordinarsi attorno allo stesso stato della tabella.
I database vettoriali ora hanno bisogno di una capacità simile, ma lo stato è più complesso. Deve includere non solo file di tabella e partizioni, ma anche indici vettoriali, indici testuali, funzionalità sparse, log di eliminazione, statistiche, intervalli di ID di riga e riferimenti a blob esterni.
La domanda non è semplicemente: “Spark può leggere i file Milvus?”
La domanda è: dopo che Spark ha eseguito il backfill di una colonna vettoriale sparsa, come fa Milvus a sapere a quale versione appartiene quella colonna, quali righe copre, quale modello l’ha prodotta e quando le query online possono usarla in sicurezza?
La risposta deve risiedere nel modello di archiviazione.
Perché le patch non bastano
È allettante trattare questi come tre problemi ingegneristici separati.
- Amplificazione della scrittura? Aggiungere batching.
- Letture puntuali? Aggiungere una cache.
- Sistemi esterni? Aggiungere strumenti di esportazione e importazione.
Queste patch possono aiutare, ma non affrontano il problema sottostante: un dataset vettoriale è fisicamente eterogeneo.
Nell’esempio del video, clip_id, video_id, duration e aesthetic_score sono campi scalari brevi. Sono utili per il filtraggio e l’analisi.
captionè testo. Può essere usato per BM25, revisione, correzione e backfill.embeddingè un vettore denso lungo. Viene usato per il richiamo ANN e in seguito per lookup a livello di riga o reranking.embedding_v2è l’output di un nuovo modello, spesso sottoposto a backfill molto tempo dopo l’inserimento dei dati originali.sparse_vectorsupporta la ricerca ibrida e ha un proprio pattern di accesso.- Il video grezzo dovrebbe restare nell’object storage. Il database dovrebbe archiviare un riferimento, un checksum, un tipo MIME, una versione del parser e una relazione a livello di riga.
- Indici vettoriali, indici testuali, statistiche e log di eliminazione sono oggetti derivati con una propria semantica di versione.
Questi oggetti condividono una riga logica, ma non dovrebbero condividere tutti lo stesso layout fisico o ciclo di vita.
- Se vengono forzati in un unico layout di tabella ordinario, gli aggiornamenti diventano costosi.
- Se vengono forzati in un unico formato di file colonnare, le letture puntuali diventano costose.
- Se vengono trattati come file oggetto non correlati, la gestione delle versioni diventa fragile.
Quindi il modello di archiviazione deve partire dal fatto che il dataset è eterogeneo.
Questo porta a tre requisiti di progettazione:
- Primo, gruppi di colonne diversi dovrebbero essere archiviati in formati fisici diversi.
- Secondo, questi gruppi di colonne hanno bisogno di uno spazio di ID di riga condiviso, in modo da potersi comunque comportare come un’unica tabella logica.
- Terzo, il dataset ha bisogno di un Manifest versionato che dichiari quali file, indici, log, statistiche e riferimenti a oggetti appartengono alla vista corrente.
Questo è il design alla base di Loon, il nostro nuovo motore di archiviazione dietro Milvus e Zilliz Cloud.
Loon: un motore di archiviazione dietro Milvus e Zilliz Cloud per dataset vettoriali in evoluzione
Per risolvere tutti i problemi sopra descritti, abbiamo creato Loon, il nuovo motore di archiviazione per Milvus e Zilliz Vector Lakebase (la prossima evoluzione di Zilliz Cloud), progettato per dataset vettoriali in evoluzione.
Il nome segue la tradizione di Zilliz di usare nomi di uccelli. Un loon è un uccello tuffatore che vive sui laghi, il che si adatta bene all’obiettivo del sistema: un database vettoriale non dovrebbe dover spostare, scansionare o riscrivere un intero lago di dati ogni volta che esegue una query, effettua il backfill di una colonna o costruisce un indice. Dovrebbe prima comprendere la versione corrente del dataset, incluse le sue colonne, indici, statistiche, log di eliminazione e riferimenti a oggetti, quindi leggere solo la parte di cui ha effettivamente bisogno.
Formati di file ibridi, allineamento degli ID di riga e Manifest non sono tre funzionalità separate. Derivano dalla stessa assunzione progettuale: un dataset vettoriale è intrinsecamente eterogeneo.
Tre elementi, un unico modello di archiviazione
I formati di file ibridi riconoscono che colonne diverse hanno pattern di accesso diversi. I campi scalari sono adatti a scansioni e filtri. I campi vettoriali richiedono un lookup efficiente a livello di riga. Gli oggetti grezzi come video, PDF, immagini e file audio appartengono all’object storage, non all’interno dei file di dati del database.
L’allineamento degli ID di riga riconosce che queste colonne possono essere fisicamente separate, ma descrivono comunque le stesse righe logiche. Una didascalia, un embedding, un vettore sparso e l’URI di un video possono risiedere in file e formati diversi, ma devono comunque essere ricomposti come un unico risultato.
Il Manifest riconosce che il dataset non viene scritto una volta e poi lasciato invariato. Verrà modificato da più sistemi, attraverso più versioni, per più attività. Indici, statistiche, log di eliminazione, riferimenti a oggetti esterni e gruppi di colonne devono tutti comparire nella stessa vista versionata.
Ecco perché Loon non è solo un formato di file vettoriale più veloce. Un formato più veloce aiuta il lookup puntuale, ma non risolve l’evoluzione dello schema né il coordinamento multi-engine. L’allineamento degli ID di riga consente alle colonne suddivise di comportarsi come un’unica tabella, ma non specifica quali file appartengano alla versione corrente. Un Manifest può descrivere lo stato di un dataset, ma senza gruppi di colonne e allineamento degli ID di riga non può rappresentare in modo pulito layout fisici diversi all’interno di un’unica raccolta logica.
Il modello di archiviazione ha bisogno di tutti e tre: formati diversi per gruppi di colonne diversi, uno spazio condiviso di ID di riga per ricostruire le righe e un Manifest versionato che dica a ogni lettore e scrittore cos’è attualmente il dataset.
Dove si inserisce Loon in Milvus e Zilliz Vector Lakebase
In Milvus, sostituisce il vecchio livello di archiviazione dei binlog dei segmenti con un modello costruito attorno ad astrazioni di Manifest, ColumnGroup, formato di file e filesystem. In Zilliz Vector Lakebase (la prossima evoluzione di Zilliz Cloud), la stessa direzione si applica all’architettura di Vector Lakebase: mantenere veloce il percorso di serving del database vettoriale, rendendo al contempo i dati sottostanti più facili da evolvere, analizzare e coordinare con sistemi esterni.
I componenti Milvus di livello superiore mantengono ancora i loro ruoli familiari. Proxy gestisce il routing. QueryCoord e DataCoord gestiscono la pianificazione. IndexNode costruisce gli indici. Le API esposte alle applicazioni per collection, insert, search e hybrid search non devono esporre file Manifest o ColumnGroups.
Il cambiamento è sotto la superficie.
DataNode, QueryNode, segcore, compaction e connettori esterni possono operare attraverso la stessa astrazione di archiviazione. Questo è importante perché il dataset non viene più scritto e letto solo dal database. Può essere esteso da sistemi di calcolo esterni e consumato simultaneamente dalla ricerca online.
A un livello alto, gli strati appaiono così:
Manifest
→ ColumnGroup
→ file format layer
→ filesystem abstraction
Il Manifest descrive lo stato versionato del dataset. I ColumnGroups mappano una collection logica in gruppi fisici di colonne. Il livello del formato di file consente a ciascun ColumnGroup di scegliere un formato appropriato. L’astrazione del filesystem funziona su object storage e storage locale.
Il punto importante è che i formati di file ibridi, l’allineamento degli ID di riga e il Manifest non sono funzionalità separate. Insieme, definiscono il modello di archiviazione.
Con questo modello in atto, possiamo esaminare una per una le tre scelte progettuali: come Loon archivia diversi ColumnGroups, come li riallinea in righe e come il Manifest trasforma quei file in un dataset versionato.
Design 1: usare il formato di file giusto per il giusto gruppo di colonne
Colonne diverse hanno pattern di accesso diversi. Non dovrebbero essere forzate nello stesso formato di file.
Loon separa una collezione logica in ColumnGroup.
- I campi scalari, i campi filtro, le chiavi di business e i campi statistici vengono spesso scansionati, filtrati, aggregati o usati per la pianificazione delle query. Beneficiano della compressione, del pruning delle colonne e della compatibilità con l’ecosistema. Parquet è una buona scelta per queste colonne.
- I vettori densi, i vettori sparsi e le funzionalità di reranking vengono spesso letti dopo il recall ANN tramite ID riga. Hanno bisogno di accesso casuale a bassa latenza, letture byte-range precise e decodifica selettiva. Un layout orientato ai segmenti è più adatto. Loon usa Vortex in questa direzione.
- Gli oggetti grezzi come video, PDF, immagini e file audio non dovrebbero essere incorporati nei file di dati del database vettoriale. Dovrebbero rimanere nell’object storage. Il database registra riferimenti, checksum, tipi MIME, versioni dei parser e relazioni a livello di riga.
Per l’esempio del video, un layout fisico potrebbe apparire così:
Parquet ColumnGroup:
clip_id / video_id / start_offset / duration / aesthetic_score / caption
Vortex ColumnGroups:
embedding
embedding_v2
sparse_vector
Object storage:
raw video objects
Per l’applicazione, questa è comunque un’unica collezione. Per il livello di storage, parti diverse di quella collezione usano formati fisici diversi. Questo riduce direttamente le riscritture non necessarie. L’aggiunta di embedding_v2 può diventare un nuovo ColumnGroup vettoriale più un commit del Manifest. Non richiede la riscrittura della colonna caption, dei metadati scalari o della colonna embedding esistente.
La stessa idea si applica ai vettori sparsi, alle funzionalità di reranking o ad altri campi derivati. Se una nuova colonna può essere fisicamente indipendente e allineata tramite ID riga, non deve trascinare colonne non correlate attraverso lo stesso percorso di riscrittura.
Loon adatta anche l’uso dei formati di file.
Per Parquet, le impostazioni predefinite non sono sempre ideali per dati fortemente vettoriali. Un row group da 64 MB può essere troppo grande per il lookup puntuale, perché una piccola lettura casuale può recuperare molti più dati del necessario. Loon restringe i row group a 1 MB nei percorsi rilevanti e disabilita le codifiche, come la dictionary encoding sulle colonne vettoriali, quando non aiutano con dati vettoriali dall’aspetto casuale.
Per Vortex, il lavoro più importante è il layout. Loon usa un layout che bilancia efficienza di scansione e lookup puntuale. All’interno di un row group, i segmenti di colonne correlate possono essere posizionati vicini per supportare la scansione. Per eseguire operazioni, le letture di sotto-segmenti consentono al sistema di recuperare solo i byte rilevanti invece di caricare un intero segmento.
Loon supporta anche l’integrazione Lance in sola lettura, così i dataset Lance esistenti possono essere montati come ColumnGroup quando la compatibilità è importante.
Cosa mostra il benchmark
In un test locale, usando un singolo file con 40.000 righe e lo schema {id: int64, name: utf8, value: float64, vector: list<float32>[128]}, Vortex ha mostrato questi risultati rispetto a Parquet con row group da 1 MB:
| Operazione | Vortex | Parquet | Differenza |
|---|---|---|---|
| Take, K=1000 righe casuali | 5,8 ms | 144 ms | 25x più veloce |
| Scansione completa della colonna vettoriale | 21 ms | 142 ms | 6,76x più veloce |
| Dimensione file, ~21 MB di dati grezzi | 6,62 MB | 7,16 MB | 7% più piccolo |
Il risultato di take deriva dalla riduzione della quantità di dati irrilevanti che devono essere letti e decodificati. Il risultato della scansione deriva dalla compressione e dalle scelte implementative.
Questi numeri dovrebbero restare collegati alla loro configurazione: 8 vCPU Ubuntu 22.04 KVM, filesystem locale, un file, 40.000 righe, row group da 1 MB e lo schema sopra. Su object storage, l’I/O di rete può dominare, quindi ridurre l’amplificazione delle letture può contare ancora di più. I risultati effettivi dipendono dalla forma del dataset, dal comportamento dell’object storage, dallo stato della cache e dal pattern di query.
Il punto più ampio non è che ogni colonna dovrebbe usare Vortex.
Il punto è che i dataset vettoriali hanno bisogno di una scelta del formato di file a livello di ColumnGroup.
Design 2: allineare i file fisici tramite gli ID di riga
I formati di file ibridi risolvono un problema: colonne diverse possono ora risiedere nei formati più adatti a ciascuna.
Ma questo crea un secondo problema. Se i campi scalari risiedono in Parquet, i vettori in Vortex e gli oggetti grezzi nello storage a oggetti, come fa il sistema a trattarli ancora come un’unica raccolta?
Loon risolve questo problema con l’allineamento degli ID di riga.
L’ID di riga è il sistema di coordinate del livello di storage
Ogni ColumnGroupFile fisico registra il percorso del file e l’intervallo di ID di riga che copre:
path
start_index
end_index
ColumnGroups diversi possono coprire lo stesso spazio di ID di riga anche se risiedono in file e formati diversi.
Per l’ID di riga 12345, i metadati scalari possono trovarsi in un ColumnGroup Parquet, l’embedding in un ColumnGroup Vortex e il video grezzo può essere rappresentato da un riferimento allo storage a oggetti. Logicamente, sono ancora un’unica riga. Questo fornisce al livello di storage un sistema di coordinate stabile.
L’ID di riga non è la chiave primaria di business. È il sistema di coordinate del livello di storage che consente a Loon di suddividere fisicamente una raccolta senza perdere la capacità di ricostruirla logicamente.
Le nuove colonne non devono riscrivere le vecchie colonne
Aggiungere embedding_v2 non richiede di riscrivere la didascalia originale, i metadati o i ColumnGroups di embedding_v1. Loon può scrivere un nuovo ColumnGroup vettoriale, registrare l’intervallo di ID di riga che copre e confermare quella modifica tramite il Manifest.
Lo stesso vale per vettori sparsi, funzionalità di rerank o altri campi derivati che arrivano in seguito.
Finché il nuovo ColumnGroup copre il giusto intervallo di ID di riga, può unirsi alla stessa raccolta logica senza costringere dati non correlati a spostarsi.
Eliminazioni e compattazione possono essere più mirate
L’allineamento degli ID di riga aiuta anche con le eliminazioni.
Un’eliminazione può prima essere espressa tramite un log di eliminazione. La riga diventa invisibile a livello logico, mentre la pulizia fisica viene rimandata fino alla compattazione. Quando la compattazione viene infine eseguita, non deve sempre riscrivere ogni ColumnGroup collegato alle righe interessate. Può concentrarsi sui ColumnGroups che richiedono pulizia.
Questo è importante perché non tutte le colonne hanno lo stesso profilo di costo. Riscrivere un breve ColumnGroup scalare è molto diverso dal riscrivere centinaia di gigabyte di vettori densi.
La ricerca ibrida può recuperare solo le colonne di cui ha bisogno
L’allineamento degli ID di riga è anche ciò che rende pratica la ricerca ibrida sopra i formati di file ibridi.
Dopo che la ricerca ANN restituisce gli ID di riga candidati, il sistema può recuperare solo i campi necessari per il risultato finale: didascalie, metadati, vettori, funzionalità di rerank o riferimenti a oggetti.
Ad esempio, una query può richiedere:
caption
embedding
video_uri
Questi campi possono risiedere in ColumnGroups diversi. Loon può individuare i file pertinenti in base all’intervallo di ID di riga, leggere gli intervalli di byte necessari e assemblare il risultato.
Senza l’allineamento degli ID di riga, i formati ibridi sarebbero semplicemente file separati affiancati. Con l’allineamento degli ID di riga, si comportano come un’unica raccolta logica.
Packed Reader nasconde la suddivisione al livello superiore
Il componente runtime che rende tutto questo utilizzabile è il Packed Reader.
Il livello superiore vede un flusso unificato di Arrow RecordBatch. Sotto, i dati possono provenire da più ColumnGroups in formati di file diversi. Il Packed Reader nasconde queste differenze, allinea i dati in base agli intervalli di ID di riga e pianifica l’I/O multi-file con un uso controllato della memoria.
Supporta anche il take diretto per ID di riga. Dato un insieme di ID di riga, individua i ColumnGroupFiles pertinenti, emette letture di intervalli e restituisce i campi richiesti.
Per il workflow video, una query ANN può richiedere caption, embedding e video_uri. Il Packed Reader può recuperare il ColumnGroup scalare e il ColumnGroup vettoriale senza toccare colonne non correlate.
Questa è la differenza tra “file separati” e “una tabella con più layout fisici.”
Design 3: rendere il Manifest la fonte di verità
I formati di file ibridi definiscono come i dati vengono archiviati fisicamente. L’allineamento degli ID di riga determina come i ColumnGroups separati formino comunque un’unica tabella logica. Ma il sistema deve ancora rispondere a una domanda più ampia: quali file, log, statistiche, indici e riferimenti a oggetti appartengono alla versione corrente del dataset? Questo è il compito del Manifest.
Le directory dell’object storage non bastano
L’object storage non è un catalogo di database. Una directory può contenere file vecchi, file nuovi, output di job falliti, file temporanei, log di eliminazione, file ancora referenziati da snapshot precedenti e file in attesa di pulizia. Il fatto che un file esista non significa che appartenga alla versione corrente del dataset.
Un dataset Loon può essere organizzato in directory come:
_metadata/
_data/
_delta/
_stats/
_index/
Ma la struttura delle directory non è la fonte di verità. Lo è il Manifest. I reader non dovrebbero elencare le directory e inferire lo stato da qualunque file capiti di esistere. Dovrebbero leggere il Manifest corrente e seguire la vista versionata che dichiara.
Il Manifest definisce una vista versionata del dataset
Il Manifest definisce il dataset in una data versione. Registra:
- quali ColumnGroups esistono
- quali intervalli di ID di riga coprono
- quale formato fisico usa ciascun ColumnGroup
- dove si trovano i file
- quali log di eliminazione sono attivi
- quali statistiche sono disponibili
- quali indici esistono
- quali blob esterni sono referenziati
- quali colonne e intervalli di righe coprono tali statistiche o indici
Ogni aggiornamento scrive una nuova versione del Manifest. Un reader che apre la versione N vede una vista stabile del dataset alla versione N. Un writer può preparare la versione N+1 senza disturbare i reader che stanno ancora usando la versione N.
Il Manifest tiene traccia di più dei file di tabella
In Loon, il corpo del Manifest è codificato con Apache Avro e organizzato attorno a quattro sezioni principali.
- I ColumnGroups descrivono le colonne, i formati, i file e gli intervalli di ID di riga.
- I DeltaLogs descrivono le eliminazioni. Diversi tipi di eliminazione coprono diverse fonti di modifica, come eliminazioni per chiave primaria dai client, eliminazioni posizionali dalla compattazione interna o eliminazioni per uguaglianza da engine esterni.
- Le Stats includono metadati di pianificazione come filtri bloom, statistiche BM25 e valori min/max.
- Gli Indexes descrivono tipo di indice, parametri, colonne coperte e intervalli di ID di riga. Questo può includere indici vettoriali come HNSW o IVF, indici testuali, indici invertiti, indici bitmap e strutture correlate.
È qui che Loon differisce da un manifest di tabella tradizionale.
Un dataset vettoriale deve tenere traccia non solo dei file di dati e delle partizioni. Deve anche tenere traccia di indici vettoriali, indici testuali, feature sparse, log di eliminazione, statistiche, riferimenti a oggetti esterni e degli intervalli di ID di riga che li collegano.
Il Manifest deve essere scrivibile da più soggetti oltre al database
La parte più importante non è solo cosa contiene il Manifest. È chi può scriverlo.
- Se solo il database può scrivere il Manifest, rimane metadato interno. Metadati più ordinati, ma comunque privati di un solo engine.
- Se engine esterni possono generare nuovi ColumnGroups, stats e voci del Manifest, il Manifest diventa un’interfaccia di coordinamento.
- Un job Spark, per esempio, può eseguire il backfill di una colonna vettoriale sparse. Scrive un nuovo ColumnGroup, registra copertura delle righe e statistiche, e committa un nuovo Manifest. Le query online possono continuare a leggere la vecchia versione durante il job. Una volta che il commit riesce, la nuova versione diventa visibile.
Questo è simile nello spirito a Iceberg e Delta Lake, ma il modello a oggetti è più ampio. Un dataset vettoriale deve tenere traccia di indici vettoriali, indici testuali, feature sparse, log di eliminazione, stats, riferimenti a blob e intervalli di ID di riga, non solo di file di tabella e partizioni.
I commit ottimistici mantengono semplici gli aggiornamenti di versione
Ogni commit scrive una nuova versione del Manifest. Un writer può creare nuovi contenuti basati sulla versione N, quindi tentare di scrivere manifest-{N+1}.avro. La scrittura condizionale dell'object storage o le semantiche di generation-match possono far fallire il commit se quella versione esiste già. Il writer può quindi riprovare sulla versione più recente.
Questo offre a Loon una concorrenza ottimistica senza costringere ogni aggiornamento a passare attraverso un percorso di coordinamento pesante e fortemente consistente. Senza un Manifest, lo storage multi-formato e multi-engine finisce per trasformarsi in convenzioni di denominazione e riconciliazione manuale. Questo può funzionare per dataset piccoli. Non funziona per dati vettoriali su scala TB.
Il Manifest è ciò che trasforma file eterogenei in un dataset che più sistemi possono leggere e aggiornare in sicurezza.
Cosa cambia per gli utenti quando lo storage diventa versionato
Per gli sviluppatori di applicazioni, Loon non dovrebbe diventare un nuovo onere a livello di API.
Gli utenti dovrebbero continuare a lavorare con concetti Milvus familiari: collection, insert, search e hybrid search. Non dovrebbero aver bisogno di pensare a file Manifest, ColumnGroups, intervalli di ID di riga o layout dei file durante il normale sviluppo applicativo.
Il cambiamento è sottostante. Lo storage diventa più consapevole di come evolvono realmente i dataset AI.
Aggiungere un nuovo embedding non dovrebbe spostare i vecchi dati
In precedenza, aggiungere embedding_v2 a una collection esistente richiedeva spesso di esportare i dati, addestrare un nuovo modello, generare vettori e quindi reimportare o aggiornare in blocco la collection tramite l'SDK. Questo percorso crea molto lavoro operativo: tracciamento delle versioni, retry dei job falliti, ricostruzione degli indici, impatto sul serving e controlli di consistenza.
Con Loon, questo può diventare un'evoluzione dello schema più un nuovo commit ColumnGroup. La nuova colonna di embedding può essere scritta come un proprio ColumnGroup fisico, allineato per ID di riga, e resa visibile tramite il Manifest. La vecchia colonna caption, la colonna di metadati scalari e la colonna di embedding originale non devono essere spostate.
I backfill non dovrebbero richiedere un loop di aggiornamento lato client
Molti aggiornamenti dei dati AI sono backfill. Un team può aggiungere vettori sparse dopo che la hybrid search diventa importante. Può aggiungere feature di rerank dopo l'addestramento di un nuovo modello. Può correggere caption dopo una revisione umana. Può aggiungere tag di governance dopo un aggiornamento delle policy.
In un layout tradizionale, questi cambiamenti avvengono spesso tramite aggiornamenti dell'SDK client o percorsi di scrittura solo database, anche quando i dati sono prodotti da Spark, Ray o un altro engine esterno.
Con Loon, i sistemi di calcolo esterni possono produrre nuovi ColumnGroups e committarli tramite il Manifest. Il database non deve più essere l'unico punto di ingresso per ogni riscrittura.
L'analisi offline non dovrebbe richiedere un'altra copia della verità
In precedenza, i team spesso esportavano una collection online in Parquet per valutazioni o analisi offline. Questo crea due versioni dello stesso dataset: la collection online e la copia di analisi. Una volta che le caption vengono corrette, gli embedding rigenerati, i log di delete applicati o gli indici ricostruiti, il team deve chiedersi quale copia sia aggiornata.
Con un modello di storage basato su Manifest, gli engine di analisi possono leggere la stessa vista versionata del dataset del sistema di serving. Possono proiettare solo le colonne di cui hanno bisogno, scansionare solo gli intervalli di righe rilevanti e lavorare su una versione dichiarata del dataset invece che su uno snapshot esportato manualmente.
Le eliminazioni e le correzioni dovrebbero toccare solo ciò che è cambiato
Eliminazioni, correzioni di caption, fix di label e aggiornamenti di governance sono routine nei dataset AI. Non dovrebbero costringere ogni lunga colonna vettoriale a passare attraverso lo stesso percorso di riscrittura.
Con Loon, i log di eliminazione possono inizialmente essere trattati come eliminazione logica. In seguito, la compattazione può ripulire i ColumnGroups interessati senza riscrivere dati non correlati. Se un breve campo di testo cambia, il layer di storage non dovrebbe dover riscrivere centinaia di gigabyte di vettori densi solo perché condividono la stessa riga logica.
Gli engine esterni diventano parte del workflow, non una via di fuga
Il cambiamento più ampio è che i motori esterni non vengono più trattati come sistemi al di fuori del database vettoriale.
Spark, Ray, job di valutazione, sistemi di labeling e pipeline di governance producono e modificano già gran parte dei dati. Il livello di storage dovrebbe consentire loro di collaborare attorno a un’unica fonte di verità invece di esportare, copiare e reimportare continuamente.
Questo è ciò che una versione di Manifest rende possibile. Offre al serving online, all’analisi offline, ai job di backfill e alla compattazione una vista condivisa del dataset.
Questi possono sembrare dettagli interni dello storage, ma influenzano la rapidità con cui i team possono iterare sui dataset AI. Ogni modifica del modello, backfill di feature, correzione di caption, filtro di qualità e ricostruzione dell’indice dipende dalla stessa domanda: "Il sistema può aggiornare il dataset senza spostare dati che non ha bisogno di spostare? "
Questo è il valore pratico del modello di storage.
Loon è disponibile in Milvus 3.0 beta e Zilliz Vector Lakebase
Loon è disponibile in Milvus 3.0 beta ed è anche parte del livello di storage in Zilliz Vector Lakebase, la prossima evoluzione di Zilliz Cloud. E questa release si concentra su tre aree fondamentali:
- Il Manifest. L’obiettivo è che scritture, backfill, eliminazioni, statistiche e aggiornamenti degli indici producano viste versionate del dataset che i reader possano aprire in modo consistente. Per i reader, questo significa che una query può aprire una versione specifica del Manifest e vedere una vista stabile del dataset. Per i writer, questo significa che nuovi file di dati, log di eliminazione, statistiche o file di indice possono essere preparati prima e poi resi visibili tramite un commit versionato.
- Il ColumnGroup e il supporto dei formati. Parquet supporta colonne scalari e compatibili con l’ecosistema. Vortex supporta pattern di accesso con forte presenza di vettori. Lance può essere integrato in modalità di sola lettura per la compatibilità con dataset Lance esistenti.
- L’Index on Lake. Statistiche scalari, indici di filtro e indici invertiti testuali possono partecipare alla pianificazione basata su Manifest per intervallo di righe. Gli indici vettoriali lake-native sono più complessi. HNSW e IVF hanno comportamenti diversi su object storage, e HNSW in particolare è sensibile all’accesso casuale e alla località della cache. Non può semplicemente riutilizzare un layout progettato per un SSD locale e aspettarsi lo stesso risultato.
C’è ancora lavoro da fare
- I percorsi di scrittura esterni sono importanti perché Spark e Ray dovrebbero poter produrre ColumnGroup e commit Manifest senza costringere ogni backfill a passare attraverso un ciclo di client SDK.
- L’interoperabilità con il lakehouse è importante perché molti team usano già cataloghi e motori di query come Iceberg, Delta Lake, Trino, DuckDB e Athena. I dati vettoriali dovrebbero poter partecipare a quell’ecosistema senza perdere prestazioni nella ricerca vettoriale.
- Il layout degli indici è importante perché gli indici a grafo e le strutture invertite hanno pattern di accesso diversi su object storage.
- La semantica dei large object è importante perché video grezzi, PDF, immagini e file audio richiedono gestione dei riferimenti, versioning e comportamento di eliminazione allineati al dataset vettoriale derivato.
Il comportamento esatto della release, le impostazioni predefinite e il percorso di migrazione dovrebbero seguire le relative note di rilascio di Milvus e Zilliz Cloud. La direzione dello storage, tuttavia, è chiara: i database vettoriali hanno bisogno di una base versionata e lake-native sotto il livello di serving.
Prova Loon con Zilliz Vector Lakebase
Se il tuo stack attuale separa serving online, analisi offline, backfill e workflow esterni di data lake in sistemi diversi, Zilliz Vector Lakebase merita un’occhiata. Puoi provarlo in Zilliz Cloud. Le nuove registrazioni con email di lavoro ricevono $100 di crediti gratuiti. Puoi anche parlare con noi del tuo caso d’uso.
Puoi anche seguire la release di Milvus 3.0 per vedere come Loon evolve nel motore open-source.
Zilliz Vector Lakebase riunisce:
- Servizio a livelli per diversi compromessi tra prestazioni in tempo reale e costi
- Ricerca on-demand per carichi di lavoro su larga scala o esplorativi senza calcolo sempre attivo
- Ricerca su data lake esterni, così puoi indicizzare e cercare direttamente sui dati lake esistenti
- Ricerca ad ampio spettro su vettori, testo, JSON e dati geospaziali, con recupero ibrido e reranking
- Storage unificato lake-native basato su Vortex, un formato aperto progettato per letture casuali più rapide e a costo inferiore su dati con forte presenza di vettori
Continua a leggere

How to Install and Run OpenClaw (Previously Clawdbot/Moltbot) on Mac
Turn your Mac into an AI gateway for WhatsApp, Telegram, Discord, iMessage, and more — in under 5 minutes.

How to Build RAG with Milvus, QwQ-32B and Ollama
Hands-on tutorial on how to create a streamlined, powerful RAG pipeline that balances efficiency, accuracy, and scalability using the QwQ-32B and Milvus.

Legal Document Analysis: Harnessing Zilliz Cloud's Semantic Search and RAG for Legal Insights
Enhance legal document analysis with Zilliz Cloud’s Semantic Search and RAG. Improve accuracy, efficiency, and scalability for contracts, case law, and compliance.


