Ricerca semantica con NumPy
Costruirà un sistema di ricerca semantica in puro Python usando NumPy per calcolare la similarità coseno tra l'embedding di una query e una raccolta di embeddings di documenti.
Ricerca semantica con NumPy è 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.
Ricerca semantica senza database
La ricerca semantica individua i documenti più pertinenti per una query in base al significato, anziché alla sovrapposizione delle parole chiave. L'implementazione più semplice utilizza NumPy per calcolare la similarità coseno tra l'embedding di una query e tutti gli embedding dei documenti in memoria, senza bisogno di un database esterno.
Questo approccio funziona bene con un massimo di alcune decine di migliaia di documenti ed è ideale per la prototipazione, prima di investire in un database vettoriale.
Creazione del corpus di documenti
Iniziate raccogliendo i documenti e generando un embedding per ciascuno tramite l'API OpenAI. Memorizzate gli embedding in un array NumPy 2D, in cui ogni riga rappresenta il vettore di un documento. Mantenete un elenco parallelo dei testi dei documenti, così da poter recuperare il contenuto originale dopo aver individuato le corrispondenze migliori.
import numpy as np
from openai import OpenAI
client = OpenAI()
documents = [
'Python is a high-level programming language.',
'NumPy provides fast numerical computing for Python.',
'Embeddings represent text as dense vectors.',
'Cosine similarity measures angle between vectors.',
'RAG combines retrieval with language generation.'
]
response = client.embeddings.create(
model='text-embedding-3-small',
input=documents
)
corpus_embeddings = np.array([item.embedding for item in response.data])
print(f'Corpus shape: {corpus_embeddings.shape}') # (5, 1536)Generazione dell'embedding della query dell'utente
Quando un utente invia una query di ricerca, generate il relativo embedding utilizzando lo stesso modello usato per generare gli embedding dei documenti. La combinazione di modelli diversi, ad esempio text-embedding-3-small per i documenti e text-embedding-3-large per le query, produce vettori appartenenti a spazi diversi e restituisce punteggi di similarità privi di significato.
from openai import OpenAI
import numpy as np
client = OpenAI()
query = 'How do I compute similarity between text?'
response = client.embeddings.create(
model='text-embedding-3-small', # must match corpus model
input=query
)
query_embedding = np.array(response.data[0].embedding)
print(f'Query vector shape: {query_embedding.shape}') # (1536,)Calcolo della similarità coseno con NumPy
Per trovare in un'unica operazione la similarità tra la query e ogni documento, calcolate il prodotto scalare tra il vettore della query e la matrice dei vettori dei documenti. Poiché entrambi gli embedding OpenAI sono normalizzati secondo la norma L2, questo equivale a calcolare la similarità coseno per tutti i documenti contemporaneamente: O(n * d), dove n è il numero di documenti e d è la dimensione.
import numpy as np
# corpus_embeddings: (n_docs, 1536)
# query_embedding: (1536,)
def semantic_search(query_vec, corpus_vecs):
# Matrix-vector dot product: shape (n_docs,)
similarities = corpus_vecs @ query_vec
return similarities
# Example call (assuming pre-computed embeddings)
# sims = semantic_search(query_embedding, corpus_embeddings)
# print(sims) # array of similarity scores, one per documentOrdinamento e recupero dei risultati migliori
Utilizzate np.argsort per ordinare i documenti in base al punteggio di similarità in ordine decrescente, quindi selezionate gli indici dei primi k risultati. Otterrete così gli indici dei documenti più pertinenti, che potrete usare per recuperare il testo originale dall'elenco parallelo.
import numpy as np
def get_top_k(query_vec, corpus_vecs, documents, k=3):
similarities = corpus_vecs @ query_vec
# argsort gives ascending order; [::-1] reverses to descending
ranked_indices = np.argsort(similarities)[::-1]
top_k_indices = ranked_indices[:k]
return [
{'text': documents[i], 'score': float(similarities[i])}
for i in top_k_indices
]
# results = get_top_k(query_embedding, corpus_embeddings, documents, k=3)
# for r in results:
# print(f'{r["score"]:.4f}: {r["text"]}')Esempio completo di ricerca semantica
Riassumendo: generate l'embedding del corpus e della query, calcolate le similarità e restituite i risultati ordinati. Questo schema completo è il nucleo di ogni fase di recupero RAG, anche quando NumPy viene sostituito internamente da un database vettoriale.
import numpy as np
from openai import OpenAI
client = OpenAI()
docs = [
'Embeddings map text to numerical vectors.',
'Python lists store ordered collections.',
'Cosine similarity compares vector directions.',
'RAG retrieves documents to ground LLM answers.',
'Dictionaries store key-value pairs in Python.'
]
corpus_resp = client.embeddings.create(model='text-embedding-3-small', input=docs)
corpus = np.array([d.embedding for d in corpus_resp.data])
query = 'finding similar text using angles'
q_resp = client.embeddings.create(model='text-embedding-3-small', input=query)
q_vec = np.array(q_resp.data[0].embedding)
scores = corpus @ q_vec
for i in np.argsort(scores)[::-1][:3]:
print(f'{scores[i]:.3f}: {docs[i]}')Soglia del punteggio
Non tutti i risultati tra i primi k sono realmente pertinenti: a volte anche la corrispondenza migliore è semanticamente poco adatta. Aggiungete una soglia del punteggio per filtrare i risultati con similarità bassa. Una soglia tipica per la similarità coseno è compresa tra 0,70 e 0,80, ma dovreste calibrarla in base al vostro dominio specifico, utilizzando query reali.
import numpy as np
def search_with_threshold(query_vec, corpus_vecs, documents, k=5, threshold=0.75):
similarities = corpus_vecs @ query_vec
ranked = np.argsort(similarities)[::-1][:k]
results = []
for i in ranked:
if similarities[i] >= threshold:
results.append({'text': documents[i], 'score': float(similarities[i])})
return results
# Only returns documents above the minimum similarity thresholdPrestazioni della ricerca con NumPy
La ricerca della similarità con NumPy ha una complessità temporale O(n * d) per query, dove n è il numero di documenti e d è la dimensione dell'embedding. Per embedding di 1536 dimensioni:
- 10.000 documenti: ~5 ms per query su una CPU moderna
- 100.000 documenti: ~50 ms per query
- 1.000.000 di documenti: ~500 ms: un tempo eccessivo; passate a un database vettoriale
NumPy è eccellente per la prototipazione, ma non include funzioni integrate per la ricerca approssimata, il filtraggio o la persistenza.
Salvataggio degli embedding su disco
Ricalcolare gli embedding a ogni esecuzione comporta uno spreco di chiamate API e denaro. Salvate su disco gli embedding del corpus e i testi dei documenti, così da doverli rigenerare solo quando il corpus cambia.
np.save memorizza in modo efficiente la matrice degli embedding e potete salvare l'elenco dei documenti in formato JSON. All'avvio, caricate entrambi i file invece di chiamare l'API.
import numpy as np
import json
# Save
np.save('/tmp/corpus_embeddings.npy', corpus_embeddings)
with open('/tmp/corpus_docs.json', 'w') as f:
json.dump(documents, f)
# Load
corpus_embeddings = np.load('/tmp/corpus_embeddings.npy')
with open('/tmp/corpus_docs.json') as f:
documents = json.load(f)
print(f'Loaded {len(documents)} docs, shape {corpus_embeddings.shape}')Gestione incrementale dei nuovi documenti
Quando arrivano nuovi documenti, non è necessario generare nuovamente gli embedding dell'intero corpus. Generate gli embedding solo dei nuovi documenti e utilizzate np.vstack per aggiungere i relativi vettori alla matrice esistente. Ricordate di aggiungere i nuovi testi all'elenco dei documenti nello stesso ordine.
import numpy as np
from openai import OpenAI
client = OpenAI()
# Assume these exist from a previous session:
# corpus_embeddings: (n, 1536)
# documents: list of strings
new_docs = ['New document about vector search.']
resp = client.embeddings.create(model='text-embedding-3-small', input=new_docs)
new_vecs = np.array([item.embedding for item in resp.data])
corpus_embeddings = np.vstack([corpus_embeddings, new_vecs])
documents.extend(new_docs)
print(f'Corpus now has {len(documents)} documents')Limiti della ricerca in memoria
La ricerca semantica con NumPy presenta limiti significativi rispetto a un database vettoriale progettato appositamente:
- Nessuna persistenza: tutto risiede nella RAM e viene perso al riavvio
- Nessun filtraggio dei metadati: non potete filtrare i risultati per data, categoria o autore
- Solo scansione lineare: non è disponibile un indice per la ricerca approssimata dei vicini
- Nessun accesso simultaneo: non è adatta a implementazioni di produzione multiutente
Questi limiti motivano l'utilizzo di un database vettoriale dedicato per i sistemi RAG di produzione.
Verifica rapida
Verificate la vostra comprensione dei concetti di AI Engineering trattati in questa lezione.
Riepilogo della lezione
In questa lezione avete imparato che i prodotti scalari tra matrici ed embedding normalizzati secondo la norma L2 calcolano la similarità coseno per l'intero corpus in un'unica operazione, che np.argsort con inversione dell'ordine recupera i k documenti più simili e che la ricerca con NumPy è ideale per i prototipi, ma non offre persistenza né filtraggio dei metadati. Nella prossima lezione utilizzeremo il clustering e UMAP per scoprire la struttura degli argomenti in una raccolta di embedding.
Domande Frequenti
La lezione «Ricerca semantica con NumPy» è gratuita?
Sì — il testo completo di «Ricerca semantica con NumPy» è 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 «Ricerca semantica con NumPy»?
Costruirà un sistema di ricerca semantica in puro Python usando NumPy per calcolare la similarità coseno tra l'embedding di una query e una raccolta di embeddings di 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 «Ricerca semantica con NumPy»?
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
- Che cosa sono gli embeddings vettoriali?
- Generare embeddings con OpenAI
- Ricerca semantica con NumPy
- Clustering e visualizzazione degli embeddings