Recherche sémantique avec NumPy
Construisez un système de recherche sémantique en Python pur utilisant NumPy pour calculer la similarité cosinus entre l’embedding d’une requête et une collection d’embeddings de documents.
Recherche sémantique avec NumPy est une leçon AI Engineering Academy gratuite sur CoddyKit. Ceci est la leçon 3 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage AI Engineering Academy, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours AI Engineering Academy comprend 4 leçons au total.
Certaines parties de cette leçon n'ont pas encore été traduites et s'affichent en anglais.
Semantic Search Without a Database
Semantic search finds the most relevant documents for a query based on meaning rather than keyword overlap. The simplest implementation uses NumPy to compute cosine similarity between a query embedding and all document embeddings in memory — no external database required.
This approach works well for up to tens of thousands of documents and is ideal for prototyping before investing in a vector database.
Building the Document Corpus
Start by collecting your documents and generating one embedding per document using the OpenAI API. Store the embeddings as a 2D NumPy array where each row is one document vector. Keep a parallel list of document texts so you can retrieve the original content after finding the best matches.
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)Embedding the User Query
When a user submits a search query, embed it using the same model that was used to embed the documents. Mixing models — for example, using text-embedding-3-small for documents and text-embedding-3-large for queries — will produce vectors in different spaces and give meaningless similarity scores.
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,)Computing Cosine Similarity with NumPy
To find the similarity between the query and every document in one operation, compute the dot product of the query vector with the matrix of document vectors. Since both OpenAI embeddings are L2-normalized, this equals cosine similarity for all documents at once — O(n * d) where n is the number of documents and d is the dimension.
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 documentRanking and Retrieving Top-K Results
Use np.argsort to rank documents by similarity score in descending order, then slice the top-k indices. This gives you the indices of the most relevant documents, which you use to look up the original text from your parallel list.
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"]}')Full Semantic Search Example
Putting it all together: embed the corpus, embed the query, compute similarities, and return ranked results. This complete pattern is the core of every RAG retrieval step, even when a vector database replaces NumPy under the hood.
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]}')Score Thresholding
Not all top-k results are actually relevant — sometimes the best match is still a poor semantic fit. Add a score threshold to filter out low-similarity results. A typical threshold is 0.70–0.80 for cosine similarity, but you should calibrate this against your specific domain using real queries.
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 thresholdPerformance Characteristics of NumPy Search
NumPy similarity search has time complexity O(n * d) per query, where n is the number of documents and d is the embedding dimension. For 1536-dimensional embeddings:
- 10,000 docs: ~5ms per query on a modern CPU
- 100,000 docs: ~50ms per query
- 1,000,000 docs: ~500ms — too slow, switch to a vector database
NumPy is excellent for prototyping but has no approximate search, filtering, or persistence built in.
Persisting Embeddings to Disk
Recomputing embeddings on every run wastes API calls and money. Save your corpus embeddings and document texts to disk so you only re-embed when the corpus changes.
np.save stores the embedding matrix efficiently, and you can save the document list as JSON. On startup, load both files instead of calling the 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}')Handling New Documents Incrementally
When new documents arrive, you do not need to re-embed the entire corpus. Embed only the new documents and use np.vstack to append their vectors to the existing matrix. Remember to append the new texts to your document list in the same order.
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')Limitations of In-Memory Search
NumPy semantic search has significant limitations compared to a purpose-built vector database:
- No persistence — everything lives in RAM and is lost on restart
- No metadata filtering — you cannot filter results by date, category, or author
- Linear scan only — no approximate nearest neighbor indexing
- No concurrent access — not suitable for multi-user production deployments
These limitations motivate using a dedicated vector database for production RAG systems.
Quick Check
Test your understanding of AI Engineering concepts from this lesson.
Lesson Recap
In this lesson you learned: matrix dot products against L2-normalized embeddings compute cosine similarity for the entire corpus in one operation, np.argsort with reversal retrieves the top-k most similar documents, and NumPy search is ideal for prototypes but lacks persistence and metadata filtering. Next up we use clustering and UMAP to discover topic structure in an embedding collection.
Questions Fréquemment Posées
La leçon « Recherche sémantique avec NumPy » est-elle gratuite ?
Oui — le texte complet de « Recherche sémantique avec NumPy » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours AI Engineering Academy, passe à CoddyKit PRO. Le cours AI Engineering Academy comprend 4 leçons au total.
Qu'est-ce que j'apprendrai dans « Recherche sémantique avec NumPy » ?
Construisez un système de recherche sémantique en Python pur utilisant NumPy pour calculer la similarité cosinus entre l’embedding d’une requête et une collection d’embeddings de documents. Tu pratiques AI Engineering Academy avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.
Dois-je avoir de l'expérience pour commencer AI Engineering Academy ?
Aucune expérience préalable n'est requise. AI Engineering Academy sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 3 sur 4.
Combien de temps prend la leçon « Recherche sémantique avec NumPy » ?
La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.
Peux-tu écrire et exécuter du code dans cette leçon AI Engineering Academy ?
Oui. Chaque leçon AI Engineering Academy inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.
Toutes les leçons de ce cours
- Que sont les embeddings vectoriels ?
- Générer des embeddings avec OpenAI
- Recherche sémantique avec NumPy
- Regrouper et visualiser des embeddings