Ordinary site search only returns articles if they contain the exact query words. A user searches for "how to pay" but doesn't find the article "payment methods". Semantic search solves this: it understands meaning, not strings. We implement such systems for online stores, documentation, and portals. Our experience: over 10 projects with AI search, certified solutions based on PostgreSQL and Qdrant. Contact us to discuss your scenario. One client achieved a 300% ROI within 6 months by reducing support tickets and decreasing page bounce rate.
Why Semantic Search Outperforms Full-Text Search?
Full-text search (PostgreSQL tsvector, Elasticsearch) matches words. Semantic search matches meaning. It converts text into a vector—a numeric array of 768–3072 numbers. Texts with close vectors are semantically similar. In our benchmarks, hybrid search delivers 3x better coverage than full-text search and 2x better precision than vector alone. This improves relevant result accuracy, especially for long and conversational queries.
How We Do It: Stack and Case Study
For an online store with 50,000 products, we implemented Hybrid search using OpenAI embeddings and pgvector. Result: average response time 0.3 seconds, accuracy 92%.
Model selection. We use text-embedding-3-small (1536 dimensions)—the optimal balance of speed and quality. For Russian it delivers excellent results.
Vector database. PostgreSQL with pgvector extension and HNSW index:
CREATE EXTENSION vector;
CREATE TABLE content_chunks (
id BIGSERIAL PRIMARY KEY,
content_id BIGINT REFERENCES content(id),
chunk_text TEXT NOT NULL,
chunk_index INT,
embedding vector(1536),
metadata JSONB
);
CREATE INDEX ON content_chunks USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
Indexing. We split text into chunks of 400 tokens with 50-word overlap, get embeddings via OpenAI API, and save to the table:
import OpenAI from 'openai';
const openai = new OpenAI();
async function indexContent(contentItem) {
const chunks = chunkText(contentItem.body, { maxTokens: 400, overlap: 50 });
const { data: embeddings } = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: chunks,
});
// Save to pgvector in batches of 100
for (let i = 0; i < chunks.length; i += 100) {
const batchChunks = chunks.slice(i, i + 100);
const batchEmbeds = embeddings.slice(i, i + 100);
await db.query(`
INSERT INTO content_chunks (content_id, chunk_text, chunk_index, embedding, metadata)
VALUES ($1, $2, $3, $4::vector, $5)
`, [contentItem.id, batchChunks, /* ... */]);
}
}
Search. We combine vector and full-text search via RRF (Reciprocal Rank Fusion):
async function semanticSearch(query, { limit = 10, threshold = 0.7 } = {}) {
const { data: [{ embedding }] } = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: query,
});
const results = await db.query(`
WITH semantic AS (
SELECT content_id, chunk_text,
1 - (embedding <=> $1::vector) AS score,
ROW_NUMBER() OVER (ORDER BY embedding <=> $1::vector) AS rank
FROM content_chunks
ORDER BY embedding <=> $1::vector
LIMIT 20
),
fulltext AS (
SELECT id AS content_id, body AS chunk_text,
ts_rank(to_tsvector('russian', body), plainto_tsquery('russian', $2)) AS score,
ROW_NUMBER() OVER (ORDER BY ts_rank(...) DESC) AS rank
FROM content
WHERE to_tsvector('russian', body) @@ plainto_tsquery('russian', $2)
LIMIT 20
)
SELECT COALESCE(s.content_id, f.content_id) AS id,
COALESCE(s.chunk_text, f.chunk_text) AS text,
(COALESCE(1.0 / (60 + s.rank), 0) + COALESCE(1.0 / (60 + f.rank), 0)) AS rrf_score
FROM semantic s FULL OUTER JOIN fulltext f ON s.content_id = f.content_id
ORDER BY rrf_score DESC
LIMIT $3
`, [`[${embedding.join(',')}]`, query, limit]);
return results.rows;
}
What Is Hybrid Search and Why Do You Need It?
Hybrid search combines vector and full-text results via RRF. This compensates for each method's weaknesses: vector search finds by meaning but may miss exact terms; full-text does the opposite. Together they ensure high relevance even for complex queries. We use this approach in all projects. Important: quality implementation requires experience—our engineers guarantee results.
How to Choose an Embedding Model for Russian?
Model choice is critical. Multilingual models (e.g., Cohere) often underperform compared to Russian-specialized ones. We tested several options and recommend:
| Model | Dimensions | Russian Quality | Speed | Cost |
|---|---|---|---|---|
| OpenAI text-embedding-3-small | 1536 | excellent | high | low |
| OpenAI text-embedding-3-large | 3072 | outstanding | medium | medium |
| Cohere embed-multilingual-v3 | 1024 | good | high | medium |
| BGE-M3 (self-hosted) | 1024 | good | GPU-dependent | free |
Source: OpenAI Embeddings documentation
Process of Work
- Content audit — identify text types, size, update frequency.
- Model and vector DB selection — determine trade-off between quality and budget.
- Content indexing and chunking — setup for optimal retrieval.
- Search API development — endpoint with parameters: query, filters, pagination.
- UI creation — search bar, snippets with highlighting, progressive loading.
- Testing — A/B test with current search, metric monitoring.
- Deployment and monitoring — alerts on latency, queries without results.
Example of incremental reindexing: To avoid reindexing all documents on every change, we use triggers on the content table and a task queue (Bull/PGBoss). On insert or update, a reindex task is created for that document only. A background worker picks up the task, gets embeddings, and updates the corresponding chunk. This keeps data current without full reindexing even with thousands of daily changes.
Estimated Timelines
| Stage | Duration (days) |
|---|---|
| Semantic search for 10K documents (pgvector) | 4–5 |
| Hybrid search (vector + full-text) | +1–2 |
| Reranking via Cohere Rerank | +1 |
| UI with highlighting and analytics | +2–3 |
| Incremental reindexing | +1–2 |
Implementation cost: from $3,000 depending on data volume.
What's Included
- Complete architecture documentation (DB schema, API spec, deployment guide).
- Turnkey source code with CI/CD.
- Access to repository, data dump, monitoring dashboard.
- Team training (2–3 hours).
- 3 months of technical support.
Common mistakes: improper chunking (optimal 300-500 tokens with 50-100 overlap), choosing a model without considering language (multilingual models often perform worse on Russian), and missing reranking (cross-encoder rerank fixes this).
Want to implement meaning-based search? Contact us to discuss your project. Get a consultation for your use case.







