AI Engineering Academy · Lezione

Indicizzazione: incorporare e memorizzare i chunk

Incorporerà ogni chunk usando l'API degli embeddings di OpenAI ed eseguirà l'upsert dei vettori risultanti con i metadati in un vector store, creando un indice consultabile dei documenti.

Lezione 3 di 413 passaggi

Indicizzazione: incorporare e memorizzare i chunk è una lezione AI Engineering Academy gratuita su CoddyKit. Questa è la lezione 3 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento AI Engineering Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso AI Engineering Academy include 4 lezioni in totale.

La fase di indicizzazione: panoramica

Dopo aver caricato e suddiviso i documenti in chunk, si raggiunge la fase di indicizzazione: la conversione dei chunk di testo in embedding vettoriali e la loro memorizzazione in un database vettoriale consultabile. Questo è l'ultimo passaggio offline prima di poter rispondere alle query. La qualità degli embedding e l'efficienza della strategia di archiviazione e indicizzazione determinano direttamente la velocità e l'accuratezza del sistema RAG al momento della query.

Generazione degli embedding tramite OpenAI API

L'approccio più comune consiste nel chiamare l'API degli embedding di OpenAI con il testo del chunk. Il modello text-embedding-3-small produce vettori di 1536 dimensioni e costa $0.02 per milione di token: un prezzo estremamente contenuto per la maggior parte dei carichi di lavoro. Invii più testi in una singola chiamata API (fino a 2048 input) per massimizzare il throughput. La risposta contiene un vettore di embedding per ogni testo di input, nello stesso ordine.

from openai import OpenAI

client = OpenAI()

def embed_batch(texts, model='text-embedding-3-small'):
    response = client.embeddings.create(
        model=model,
        input=texts  # up to 2048 texts per call
    )
    return [item.embedding for item in response.data]

# Embed one batch of 100 chunk texts
texts = [chunk['text'] for chunk in chunks[:100]]
vectors = embed_batch(texts)
print(f'Embedding dimension: {len(vectors[0])}')
print(f'Embedded {len(vectors)} chunks')

Elaborazione in batch per una maggiore efficienza

Quando indicizza migliaia di chunk, l'efficienza è importante. Elabori i chunk in batch da 100-500 per bilanciare throughput e utilizzo della memoria. Tenga traccia della posizione, così potrà riprendere l'elaborazione dopo un errore senza creare nuovamente gli embedding dei chunk già elaborati. Registri regolarmente l'avanzamento. Per 100.000 chunk elaborati in batch da 500, effettuerà 200 chiamate API: in genere l'operazione si completa in pochi minuti.

def embed_all_chunks(chunks, batch_size=200):
    embedded = []
    total = len(chunks)
    for i in range(0, total, batch_size):
        batch = chunks[i:i+batch_size]
        texts = [c['text'] for c in batch]
        vectors = embed_batch(texts)
        for chunk, vector in zip(batch, vectors):
            embedded.append({
                **chunk,
                'embedding': vector
            })
        if (i // batch_size) % 10 == 0:
            print(f'Progress: {min(i+batch_size, total)}/{total}')
    return embedded

Gestione dei limiti di frequenza durante l'indicizzazione

L'API degli embedding di OpenAI applica limiti di frequenza misurati in token al minuto (TPM). I processi di indicizzazione di grandi dimensioni raggiungono questi limiti e ricevono RateLimitError. Implementi un backoff esponenziale con jitter: quando si verifica un errore di limite di frequenza, attenda un breve intervallo casuale prima di riprovare, raddoppiando l'attesa a ogni errore successivo. In questo modo distribuisce i nuovi tentativi nel tempo e impedisce a tutti i worker paralleli di sovraccaricare l'API nello stesso momento.

import time
import random
from openai import RateLimitError

def embed_batch_with_retry(texts, max_retries=5):
    for attempt in range(max_retries):
        try:
            return embed_batch(texts)
        except RateLimitError:
            if attempt == max_retries - 1:
                raise
            wait = (2 ** attempt) + random.uniform(0, 1)
            print(f'Rate limited. Waiting {wait:.1f}s...')
            time.sleep(wait)
    return []

Inserimento o aggiornamento dei vettori in Pinecone

Dopo aver generato gli embedding, esegua l'upsert nel vector store. Upsert significa inserire nuovi vettori o aggiornare quelli esistenti se è già presente lo stesso ID: per progettazione, l'operazione è idempotente. In Pinecone, ogni record sottoposto a upsert contiene l'ID del vettore, i valori dell'embedding e un dizionario di metadati con i campi che desidera filtrare o visualizzare in seguito. Esegua l'upsert in batch di massimo 100 record per chiamata, per ottenere il throughput ottimale.

import pinecone

pc = pinecone.Pinecone(api_key='YOUR_KEY')
index = pc.Index('rag-index')

def upsert_to_pinecone(embedded_chunks, batch_size=100):
    for i in range(0, len(embedded_chunks), batch_size):
        batch = embedded_chunks[i:i+batch_size]
        vectors = [
            (
                chunk['id'],
                chunk['embedding'],
                {
                    'text': chunk['text'],
                    'source': chunk['metadata']['source'],
                    'page': chunk['metadata'].get('page', 0)
                }
            )
            for chunk in batch
        ]
        index.upsert(vectors=vectors)
        print(f'Upserted {min(i+batch_size, len(embedded_chunks))}/{len(embedded_chunks)}')

Archiviazione in pgvector

Con pgvector, inserisca gli embedding direttamente in una tabella PostgreSQL utilizzando SQL standard. Il tipo di dati vector accetta una lista Python di float serializzata come stringa. Dopo aver inserito tutte le righe, crei un indice HNSW per velocizzare le query di ricerca approssimata dei vicini più prossimi. L'indicizzazione di una tabella esistente con milioni di righe può richiedere diversi minuti; per questo, crei l'indice dopo l'inserimento massivo anziché prima.

import psycopg2
from psycopg2.extras import execute_values

def upsert_to_pgvector(conn, embedded_chunks):
    with conn.cursor() as cur:
        records = [
            (
                chunk['id'],
                chunk['text'],
                chunk['metadata']['source'],
                chunk['metadata'].get('page', 0),
                chunk['embedding']   # list of floats
            )
            for chunk in embedded_chunks
        ]
        execute_values(cur, '''
            INSERT INTO document_chunks (id, text, source, page, embedding)
            VALUES %s
            ON CONFLICT (id) DO UPDATE
            SET text = EXCLUDED.text, embedding = EXCLUDED.embedding
        ''', records)
    conn.commit()

Creazione dell'indice HNSW

HNSW (Hierarchical Navigable Small World) è il tipo di indice che consente una ricerca rapida e approssimata dei vicini più prossimi. A differenza della ricerca a forza bruta, che confronta il vettore della query con ogni vettore memorizzato, HNSW crea una struttura a grafo multilivello che riduce lo spazio di ricerca. Il parametro m controlla il numero di connessioni di ogni nodo (un valore più alto migliora il recall ma richiede più memoria), mentre ef_construction controlla la qualità dell'indice durante la fase di creazione.

-- Build HNSW index after bulk insertion
CREATE INDEX CONCURRENTLY ON document_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

-- Set ef_search at query time to trade recall vs speed
SET hnsw.ef_search = 100;

-- Verify index was created
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'document_chunks';

Progettazione dello schema dei metadati

I metadati memorizzati insieme a ogni vettore consentono un potente recupero filtrato. Progetti lo schema dei metadati prima dell'indicizzazione: l'aggiunta di nuovi campi in seguito richiede una nuova indicizzazione. Includa i campi su cui applicherà i filtri (department, doc_type, intervallo di date), quelli che visualizzerà nelle citazioni (title, page, author) e quelli utili per il debugging (chunk_index, total_chunks, indexed_at). Mantenga semplici i valori dei metadati: stringhe, numeri e valori booleani vengono indicizzati e filtrati in modo efficiente, mentre gli oggetti annidati no.

# Well-designed metadata schema
METADATA_SCHEMA = {
    # For filtering at retrieval time
    'department': 'HR',         # string
    'doc_type': 'policy',       # string
    'year': 2025,               # integer
    'is_active': True,          # boolean

    # For display in citations
    'title': 'Employee Handbook 2025',
    'author': 'HR Team',
    'page': 12,
    'source': 's3://docs/handbook_2025.pdf',

    # For debugging and updates
    'chunk_index': 3,
    'total_chunks': 24,
    'indexed_at': '2025-09-01T10:00:00Z'
}

Checkpoint per i processi di indicizzazione lunghi

L'indicizzazione di un corpus di grandi dimensioni può richiedere ore. Un arresto anomalo a metà processo farebbe perdere tutti i progressi. Implementi un file di checkpoint che registri quali chunk sono stati indicizzati correttamente. Al riavvio, salti i chunk già indicizzati e riprenda dal punto in cui si era interrotto. In questo modo il processo di indicizzazione è idempotente e può essere ripreso in sicurezza. Memorizzi il checkpoint come insieme di ID dei chunk elaborati in un file JSON o in una tabella del database.

import json
from pathlib import Path

CHECKPOINT_FILE = '/tmp/index_checkpoint.json'

def load_checkpoint():
    if Path(CHECKPOINT_FILE).exists():
        return set(json.loads(Path(CHECKPOINT_FILE).read_text()))
    return set()

def save_checkpoint(indexed_ids):
    Path(CHECKPOINT_FILE).write_text(json.dumps(list(indexed_ids)))

def index_with_checkpoint(chunks, index):
    done = load_checkpoint()
    remaining = [c for c in chunks if c['id'] not in done]
    print(f'Resuming: {len(done)} done, {len(remaining)} remaining')
    for chunk in remaining:
        upsert_to_pinecone([chunk], index)
        done.add(chunk['id'])
        save_checkpoint(done)

Verifica della completezza dell'indice

Dopo l'indicizzazione, verifichi che tutti i chunk siano stati inseriti nel vector store. Confronti il numero di chunk prodotti dal proprio splitter con il numero di vettori indicato dall'indice. Interroghi l'indice utilizzando il testo di un documento noto e confermi che il risultato atteso compaia tra i primi 5. Esegua alcune query note del proprio golden test set e verifichi che la precisione raggiunga il livello previsto. Non dia mai per scontato che l'indice sia completo senza averlo verificato.

def verify_index(index, chunks, sample_size=10):
    index_stats = index.describe_index_stats()
    total_vectors = index_stats.total_vector_count
    expected = len(chunks)
    print(f'Index vectors: {total_vectors}, Expected: {expected}')
    if total_vectors != expected:
        print('WARNING: mismatch — some chunks may not have been indexed')

    # Spot-check retrieval
    import random
    sample = random.sample(chunks, sample_size)
    for chunk in sample:
        vec = embed_batch([chunk['text']])[0]
        results = index.query(vector=vec, top_k=1, include_metadata=True)
        top_id = results.matches[0].id if results.matches else None
        if top_id != chunk['id']:
            print(f'WARNING: expected {chunk["id"]}, got {top_id}')

Alternative per gli embedding locali

Per i dati sensibili dal punto di vista della privacy che non possono lasciare la propria infrastruttura, utilizzi modelli di embedding ospitati localmente. La libreria sentence-transformers fornisce modelli di alta qualità come all-MiniLM-L6-v2 (384 dimensioni, 22 MB, molto veloce) e bge-large-en-v1.5 (1024 dimensioni, qualità superiore). Li esegua sulla CPU per carichi di lavoro moderati o sulla GPU per processi di indicizzazione di grandi dimensioni. I modelli locali eliminano i costi delle API e l'uscita dei dati dall'infrastruttura, ma richiedono la gestione dei file dei modelli e delle risorse di calcolo.

from sentence_transformers import SentenceTransformer

# Load once at startup
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')

def embed_locally(texts, batch_size=64):
    # encode() handles batching internally
    embeddings = model.encode(
        texts,
        batch_size=batch_size,
        show_progress_bar=True,
        convert_to_numpy=True
    )
    return embeddings.tolist()  # convert numpy array to Python list

vectors = embed_locally([c['text'] for c in chunks])

Verifica rapida

Verifichi la propria comprensione dei concetti di AI Engineering trattati in questa lezione.

Riepilogo della lezione

In questa lezione ha imparato a: generare embedding in batch con l'API OpenAI e gestire i limiti di frequenza con l'exponential backoff, eseguire l'upsert dei vettori in Pinecone e pgvector con i metadati, creare indici HNSW per una ricerca rapida e approssimata dei vicini più prossimi e applicare le best practice per la produzione, tra cui la ripresa basata su checkpoint, la progettazione dello schema dei metadati e la verifica della completezza dell'indice. Nel prossimo capitolo creeremo la pipeline di query che recupera i chunk e genera risposte basate sulle fonti.

Gratis per iniziare

Impara Python con un tutor IA — gratis

Scrivi ed esegui vero codice nel tuo browser, ricevi aiuto istantaneo da un tutor IA disponibile 24/7, e riprendi da dove hai lasciato sul web o nell'app.

Corsi
30
Lezioni
120

Domande Frequenti

La lezione «Indicizzazione: incorporare e memorizzare i chunk» è gratuita?

Sì — il testo completo di «Indicizzazione: incorporare e memorizzare i chunk» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso AI Engineering Academy, passa a CoddyKit PRO. Il corso AI Engineering Academy include 4 lezioni in totale.

Cosa imparerò in «Indicizzazione: incorporare e memorizzare i chunk»?

Incorporerà ogni chunk usando l'API degli embeddings di OpenAI ed eseguirà l'upsert dei vettori risultanti con i metadati in un vector store, creando un indice consultabile dei documenti. Eserciti AI Engineering Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare AI Engineering Academy?

Non è richiesta alcuna esperienza precedente. AI Engineering Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 3 di 4.

Quanto tempo richiede la lezione «Indicizzazione: incorporare e memorizzare i chunk»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione AI Engineering Academy?

Sì. Ogni lezione AI Engineering Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Caricamento dei documenti ed estrazione del testo
  2. Strategie di chunking: fisso, per frasi e ricorsivo
  3. Indicizzazione: incorporare e memorizzare i chunk
  4. Interrogare, recuperare e generare
← Torna a AI Engineering Academy