Keyword search je mŕtvy. Semantic search nachádza to, čo používatelia skutočne hľadajú.
Čo je semantic search?
Keyword search:
Query: "laptop" → Nájde len dokumenty obsahujúce "laptop"
Semantic search:
Query: "laptop" → Nájde aj "notebook", "MacBook", "počítač na prácu"
Vyhľadávanie na základe významu, nie presných slov.
Architektúra
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Indexing │ │ Query │ │ Results │
│ │ │ │ │ │
│ Documents │ │ User Query │ │ Ranked Docs │
│ ↓ │ │ ↓ │ │ ↑ │
│ Chunking │ │ Embedding │ │ Re-ranking │
│ ↓ │ │ ↓ │ │ ↑ │
│ Embedding │ │ Similarity │────►│ Top K │
│ ↓ │ │ Search │ │ │
│ Vector DB │◄────│ │ │ │
└─────────────┘ └──────────────┘ └─────────────┘
Step 1: Chunking
from langchain.text_splitter import RecursiveCharacterTextSplitter
def chunk_documents(documents: list[str]) -> list[str]:
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", ". ", " "]
)
chunks = []
for doc in documents:
chunks.extend(splitter.split_text(doc))
return chunks
# Prečo chunking?
# - Embedding modely majú limit tokenov
# - Menšie chunky = presnejšie výsledky
# - Overlap zachováva kontext
Step 2: Embedding
from openai import OpenAI
client = OpenAI()
def get_embeddings(texts: list[str]) -> list[list[float]]:
response = client.embeddings.create(
model="text-embedding-3-small",
input=texts
)
return [item.embedding for item in response.data]
# Batch processing
def embed_all_chunks(chunks: list[str], batch_size: int = 100):
embeddings = []
for i in range(0, len(chunks), batch_size):
batch = chunks[i:i + batch_size]
embeddings.extend(get_embeddings(batch))
return embeddings
Step 3: Vector Storage
import chromadb
# Inicializácia
client = chromadb.PersistentClient(path="./chroma_db")
collection = client.create_collection(
name="documents",
metadata={"hnsw:space": "cosine"}
)
# Indexovanie
def index_documents(chunks: list[str], embeddings: list[list[float]]):
collection.add(
documents=chunks,
embeddings=embeddings,
ids=[f"chunk_{i}" for i in range(len(chunks))],
metadatas=[{"source": "docs"} for _ in chunks]
)
Step 4: Search
def semantic_search(query: str, top_k: int = 5) -> list[dict]:
# Embed query
query_embedding = get_embeddings([query])[0]
# Search
results = collection.query(
query_embeddings=[query_embedding],
n_results=top_k,
include=["documents", "distances", "metadatas"]
)
return [
{
"text": doc,
"score": 1 - dist, # Convert distance to similarity
"metadata": meta
}
for doc, dist, meta in zip(
results["documents"][0],
results["distances"][0],
results["metadatas"][0]
)
]
Step 5: Hybrid Search
Kombinácia keyword + semantic:
from rank_bm25 import BM25Okapi
class HybridSearch:
def __init__(self, chunks: list[str]):
self.chunks = chunks
# BM25 for keyword
tokenized = [c.split() for c in chunks]
self.bm25 = BM25Okapi(tokenized)
# Vector for semantic
self.embeddings = get_embeddings(chunks)
def search(self, query: str, alpha: float = 0.5) -> list[dict]:
# Keyword scores
keyword_scores = self.bm25.get_scores(query.split())
# Semantic scores
query_emb = get_embeddings([query])[0]
semantic_scores = [
cosine_similarity(query_emb, emb)
for emb in self.embeddings
]
# Combine
combined = [
alpha * kw + (1 - alpha) * sem
for kw, sem in zip(
normalize(keyword_scores),
normalize(semantic_scores)
)
]
# Rank
ranked = sorted(
enumerate(combined),
key=lambda x: x[1],
reverse=True
)[:10]
return [{"text": self.chunks[i], "score": s} for i, s in ranked]
Step 6: Re-ranking
from cohere import Client
cohere = Client(api_key="xxx")
def rerank(query: str, documents: list[str], top_k: int = 5):
response = cohere.rerank(
model="rerank-english-v3.0",
query=query,
documents=documents,
top_n=top_k
)
return [
{
"text": documents[r.index],
"score": r.relevance_score
}
for r in response.results
]
Kompletný pipeline
class SemanticSearchEngine:
def __init__(self, documents: list[str]):
self.chunks = chunk_documents(documents)
self.embeddings = embed_all_chunks(self.chunks)
self.index_documents()
def search(self, query: str, top_k: int = 5) -> list[dict]:
# 1. Initial retrieval (fast, more results)
candidates = semantic_search(query, top_k=20)
# 2. Re-rank (slower, more accurate)
reranked = rerank(
query,
[c["text"] for c in candidates],
top_k=top_k
)
return reranked
Záver
Semantic search dramaticky zlepšuje používateľský zážitok. S modernými nástrojmi je implementácia otázkou hodín, nie mesiacov.