Wyszukiwanie semantyczne z NumPy
Uczestnicy zbudują system wyszukiwania semantycznego w czystym Pythonie, używając NumPy do obliczania podobieństwa cosinusowego między embeddingiem zapytania a kolekcją embeddingów dokumentów.
Wyszukiwanie semantyczne z NumPy to bezpłatna lekcja AI Engineering Academy na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej AI Engineering Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.
Wyszukiwanie semantyczne bez bazy danych
Wyszukiwanie semantyczne znajduje najbardziej odpowiednie dokumenty dla zapytania na podstawie znaczenia, a nie zgodności słów kluczowych. Najprostsza implementacja wykorzystuje NumPy do obliczania podobieństwa cosinusowego między embeddingiem zapytania a wszystkimi embeddingami dokumentów przechowywanymi w pamięci — bez potrzeby korzystania z zewnętrznej bazy danych.
To podejście dobrze sprawdza się przy maksymalnie kilkudziesięciu tysiącach dokumentów i jest idealne do tworzenia prototypów przed zainwestowaniem w bazę wektorową.
Tworzenie korpusu dokumentów
Najpierw należy zebrać dokumenty i wygenerować po jednym embeddingu dla każdego dokumentu za pomocą API OpenAI. Embeddingi należy przechowywać jako dwuwymiarową tablicę NumPy, w której każdy wiersz reprezentuje wektor jednego dokumentu. Należy także zachować równoległą listę tekstów dokumentów, aby po znalezieniu najlepszych dopasowań można było pobrać oryginalną treść.
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)Generowanie embeddingu zapytania użytkownika
Gdy użytkownik przesyła zapytanie wyszukiwania, należy wygenerować dla niego embedding za pomocą tego samego modelu, którego użyto do wygenerowania embeddingów dokumentów. Łączenie modeli — na przykład użycie text-embedding-3-small dla dokumentów i text-embedding-3-large dla zapytań — spowoduje powstanie wektorów w różnych przestrzeniach i da bezsensowne wyniki podobieństwa.
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,)Obliczanie podobieństwa cosinusowego za pomocą NumPy
Aby znaleźć podobieństwo między zapytaniem a każdym dokumentem w ramach jednej operacji, należy obliczyć iloczyn skalarny wektora zapytania i macierzy wektorów dokumentów. Ponieważ oba typy embeddingów OpenAI są znormalizowane w normie L2, wynik ten jest równy podobieństwu cosinusowemu dla wszystkich dokumentów jednocześnie — O(n * d), gdzie n oznacza liczbę dokumentów, a d wymiar.
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 i pobieranie wyników Top-K
Należy użyć np.argsort, aby posortować dokumenty według wyniku podobieństwa w kolejności malejącej, a następnie wybrać indeksy pierwszych k elementów. Otrzymane indeksy wskazują najbardziej odpowiednie dokumenty i pozwalają pobrać ich oryginalne teksty z równoległej listy.
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"]}')Kompletny przykład wyszukiwania semantycznego
Łącząc wszystkie elementy: generujemy embeddingi korpusu i zapytania, obliczamy podobieństwa oraz zwracamy wyniki uporządkowane według rankingu. Ten kompletny schemat stanowi podstawę każdego kroku pobierania danych w systemie RAG, nawet gdy używana baza wektorowa zastępuje wewnętrznie NumPy.
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]}')Ustalanie progu wyniku
Nie wszystkie wyniki z pierwszych k pozycji są rzeczywiście odpowiednie — czasami nawet najlepsze dopasowanie jest słabe pod względem semantycznym. Należy dodać próg wyniku, aby odfiltrować wyniki o niskim podobieństwie. Typowy próg dla podobieństwa cosinusowego wynosi 0,70–0,80, ale należy go skalibrować na podstawie rzeczywistych zapytań z konkretnej domeny.
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 thresholdCharakterystyka wydajności wyszukiwania NumPy
Wyszukiwanie podobieństwa za pomocą NumPy ma złożoność czasową O(n * d) dla każdego zapytania, gdzie n oznacza liczbę dokumentów, a d wymiar embeddingu. Dla embeddingów o 1536 wymiarach:
- 10 000 dokumentów: ~5 ms na zapytanie na nowoczesnym procesorze
- 100 000 dokumentów: ~50 ms na zapytanie
- 1 000 000 dokumentów: ~500 ms — to zbyt wolno, należy przełączyć się na bazę wektorową
NumPy doskonale nadaje się do tworzenia prototypów, ale nie zapewnia wbudowanego wyszukiwania przybliżonego, filtrowania ani trwałego przechowywania danych.
Zapisywanie embeddingów na dysku
Ponowne obliczanie embeddingów przy każdym uruchomieniu powoduje niepotrzebne wywołania API i koszty. Należy zapisać embeddingi korpusu oraz teksty dokumentów na dysku, aby ponownie generować embeddingi tylko wtedy, gdy korpus ulegnie zmianie.
np.save wydajnie przechowuje macierz embeddingów, a listę dokumentów można zapisać jako JSON. Przy uruchomieniu należy wczytać oba pliki zamiast wywoływać 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}')Przyrostowe przetwarzanie nowych dokumentów
Gdy pojawią się nowe dokumenty, nie trzeba ponownie generować embeddingów dla całego korpusu. Wystarczy przetworzyć tylko nowe dokumenty i użyć np.vstack, aby dołączyć ich wektory do istniejącej macierzy. Należy pamiętać o dołączeniu nowych tekstów do listy dokumentów w tej samej kolejności.
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')Ograniczenia wyszukiwania w pamięci
Wyszukiwanie semantyczne za pomocą NumPy ma istotne ograniczenia w porównaniu ze specjalizowaną bazą wektorową:
- Brak trwałości danych — wszystko znajduje się w pamięci RAM i zostaje utracone po ponownym uruchomieniu
- Brak filtrowania metadanych — nie można filtrować wyników według daty, kategorii ani autora
- Wyłącznie skanowanie liniowe — brak indeksowania przybliżonych najbliższych sąsiadów
- Brak dostępu współbieżnego — rozwiązanie nie nadaje się do produkcyjnych wdrożeń dla wielu użytkowników
Ograniczenia te uzasadniają użycie dedykowanej bazy wektorowej w produkcyjnych systemach RAG.
Szybki test
Sprawdź swoją znajomość zagadnień inżynierii AI z tej lekcji.
Podsumowanie lekcji
W tej lekcji poznali Państwo następujące zagadnienia: iloczyny skalarne macierzy obliczone dla embeddingów znormalizowanych w normie L2 wyznaczają podobieństwo cosinusowe dla całego korpusu w ramach jednej operacji, np.argsort z odwróceniem kolejności zwraca k najbardziej podobnych dokumentów, a wyszukiwanie za pomocą NumPy jest idealne do prototypów, ale nie zapewnia trwałości danych ani filtrowania metadanych. Następnie użyjemy klasteryzacji i UMAP, aby odkryć strukturę tematyczną kolekcji embeddingów.
Często zadawane pytania
Czy lekcja „Wyszukiwanie semantyczne z NumPy” jest bezpłatna?
Tak — pełny tekst „Wyszukiwanie semantyczne z NumPy” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu AI Engineering Academy, przejdź na CoddyKit PRO. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.
Co nauczysz się w „Wyszukiwanie semantyczne z NumPy”?
Uczestnicy zbudują system wyszukiwania semantycznego w czystym Pythonie, używając NumPy do obliczania podobieństwa cosinusowego między embeddingiem zapytania a kolekcją embeddingów dokumentów. Ćwiczysz AI Engineering Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć AI Engineering Academy?
Nie wymagamy żadnego doświadczenia. AI Engineering Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.
Ile czasu zajmuje lekcja „Wyszukiwanie semantyczne z NumPy”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji AI Engineering Academy?
Tak. Każda lekcja AI Engineering Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Czym są embeddingi wektorowe?
- Generowanie embeddingów za pomocą OpenAI
- Wyszukiwanie semantyczne z NumPy
- Grupowanie i wizualizacja embeddingów