Elasticsearch: Setting Up Search for Web Applications

Imagine your online store grows to 500,000 products, and searching by name via ILIKE % in PostgreSQL stutters with every keystroke. Customers leave, conversion drops—each second of delay reduces it by 7%. We've faced this situation more than once. In one project, searching across 1 million products

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1287
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1245
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    983
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    1034
  • image_website-sbh_0.webp
    Website development for SBH Partners
    1108
  • image_website-_0.webp
    Website development for Red Pear
    555

Imagine your online store grows to 500,000 products, and searching by name via ILIKE % in PostgreSQL stutters with every keystroke. Customers leave, conversion drops—each second of delay reduces it by 7%. We've faced this situation more than once. In one project, searching across 1 million products took 8 seconds; after migrating to Elasticsearch, it was 80 ms. Conversion grew by 15%. Over time, we've configured Elasticsearch for 30+ projects—from catalogs to marketplaces. Our experience shows that proper index and analyzer configuration reduces search time by 10x.

Why Elasticsearch Instead of Full-Text Search in PostgreSQL?

PostgreSQL can do full-text search via tsvector, but it struggles with heavy loads, complex morphology, and facets. Elasticsearch outperforms PostgreSQL by 10-20x in speed under real workloads. Compare:

Criteria PostgreSQL (ILIKE/tsvector) Elasticsearch
Search speed for 1 million records 200-500 ms 10-50 ms
Russian morphology Basic via dictionaries Stemming, synonyms, custom analyzers
Faceted filtering Limited Powerful aggregations
Autocomplete Hacks with trigrams Edge n-gram, Suggester
Geo search Via PostGIS, slow Native, fast

Plus, Elasticsearch's distributed search handles billions of documents.

How We Configure Elasticsearch for Your Task

We don't set up Elasticsearch "out of the box." We always analyze your data structure and typical queries. Here's a real example: for an electronics catalog with 200,000 items, we created an index with two analyzers: Russian (stemming + stop words) and autocomplete (edge n-gram). The setup took 4 days. In another project, we needed a custom char_filter to clean product names of special characters.

Installing Elasticsearch 8.x

# Add repository wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | gpg --dearmor -o /usr/share/keyrings/elasticsearch-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/elasticsearch-keyring.gpg] https://artifacts.elastic.co/packages/8.x/apt stable main" > /etc/apt/sources.list.d/elastic-8.x.list apt update && apt install -y elasticsearch # Save superuser password from installation output systemctl enable elasticsearch && systemctl start elasticsearch 

Minimal config for single-node dev:

# /etc/elasticsearch/elasticsearch.yml cluster.name: myapp-search node.name: node-1 path.data: /var/lib/elasticsearch path.logs: /var/log/elasticsearch network.host: 127.0.0.1 discovery.type: single-node xpack.security.enabled: true xpack.security.http.ssl.enabled: false # for dev; in prod — enable 

We allocate heap as: no more than 50% RAM, no more than 32 GB (due to compressed OOPs). 4 GB is enough to start.

Index Mapping

Mapping defines the index schema. An incorrect mapping cannot be fixed without reindexing. Read more at Elasticsearch mapping:

PUT /products { "settings": { "number_of_shards": 2, "number_of_replicas": 1, "analysis": { "analyzer": { "russian_analyzer": { "type": "custom", "tokenizer": "standard", "filter": ["lowercase", "russian_stop", "russian_stemmer"] }, "autocomplete_analyzer": { "type": "custom", "tokenizer": "standard", "filter": ["lowercase", "edge_ngram_filter"] }, "autocomplete_search": { "type": "custom", "tokenizer": "standard", "filter": ["lowercase"] } }, "filter": { "russian_stop": { "type": "stop", "stopwords": "_russian_" }, "russian_stemmer": { "type": "stemmer", "language": "russian" }, "edge_ngram_filter": { "type": "edge_ngram", "min_gram": 2, "max_gram": 20 } } } }, "mappings": { "properties": { "id": { "type": "keyword" }, "name": { "type": "text", "analyzer": "russian_analyzer", "fields": { "autocomplete": { "type": "text", "analyzer": "autocomplete_analyzer", "search_analyzer": "autocomplete_search" }, "keyword": { "type": "keyword" } } }, "description": { "type": "text", "analyzer": "russian_analyzer" }, "category": { "type": "keyword" }, "brand": { "type": "keyword" }, "price": { "type": "scaled_float", "scaling_factor": 100 }, "in_stock": { "type": "boolean" }, "attributes": { "type": "object", "dynamic": true }, "location": { "type": "geo_point" }, "created_at": { "type": "date" } } } } 

Search Query with Facets

POST /products/_search { "query": { "bool": { "must": [ { "multi_match": { "query": "wireless headphones", "fields": ["name^3", "description", "name.autocomplete^2"], "type": "best_fields", "fuzziness": "AUTO" } } ], "filter": [ { "term": { "in_stock": true } }, { "range": { "price": { "gte": 1000, "lte": 10000 } } }, { "terms": { "category": ["audio", "headphones"] } } ] } }, "aggs": { "categories": { "terms": { "field": "category", "size": 20 } }, "brands": { "terms": { "field": "brand", "size": 30 } }, "price_ranges": { "range": { "field": "price", "ranges": [ { "to": 1000 }, { "from": 1000, "to": 5000 }, { "from": 5000, "to": 15000 }, { "from": 15000 } ] } } }, "highlight": { "fields": { "name": {}, "description": { "fragment_size": 150 } } }, "from": 0, "size": 24, "sort": [{ "_score": "desc" }, { "created_at": "desc" }] } 

How to Configure Autocomplete?

Autocomplete is implemented via an edge n-gram analyzer, as shown in the mapping. Edge n-gram creates tokens from 2 to 20 characters. Users get suggestions after entering 2-3 characters—improving UX. The name.autocomplete field indexes the start of each word. In search queries, use match_phrase_prefix or multi_match on this field.

Estimated Timelines

Timelines depend on complexity. Basic setup with one index and integration takes 3-5 days. If you need autocomplete, facets, and PostgreSQL synchronization, add another 3-5 days. A 3-node cluster with monitoring takes 1-2 weeks. Costs are calculated individually. Below is an approximate timeline by stage:

Stage Duration
Analysis and design 1-2 days
Installation and index setup 1-2 days
Integration with application 2-3 days
Testing and optimization 1-2 days
Deployment and monitoring 1 day

Syncing Data from PostgreSQL

For synchronization, we use logical replication via Debezium + Kafka in production scenarios. For a start, periodic reindexing via cron is sufficient. Below is an example in TypeScript:

// sync/product-indexer.ts import { Client } from '@elastic/elasticsearch' import { Pool } from 'pg' const es = new Client({ node: 'http://localhost:9200', auth: { username: 'elastic', password: process.env.ES_PASSWORD! } }) const pg = new Pool({ connectionString: process.env.DATABASE_URL }) export async function indexProduct(id: string) { const { rows } = await pg.query(` SELECT p.*, c.name AS category_name, json_agg(json_build_object('key', a.key, 'value', a.value)) AS attributes FROM products p LEFT JOIN categories c ON c.id = p.category_id LEFT JOIN product_attributes a ON a.product_id = p.id WHERE p.id = $1 GROUP BY p.id, c.name `, [id]) if (!rows.length) { await es.delete({ index: 'products', id }) return } const p = rows[0] await es.index({ index: 'products', id: p.id, document: { id: p.id, name: p.name, description: p.description, category: p.category_name, price: p.price, in_stock: p.stock_quantity > 0, attributes: Object.fromEntries(p.attributes?.map((a: any) => [a.key, a.value]) ?? []), created_at: p.created_at } }) } export async function reindexAll() { const { rows } = await pg.query('SELECT id FROM products WHERE deleted_at IS NULL') const chunks = chunk(rows.map(r => r.id), 100) for (const ids of chunks) { await Promise.all(ids.map(indexProduct)) console.log(`Indexed ${ids.length} products`) } } 

Step-by-Step Setup Plan

  1. Analysis—study data structure, typical queries, speed requirements (1-2 days).
  2. Design—develop mapping, analyzers, sync scheme (1-2 days).
  3. Implementation—install Elasticsearch, configure index, write integration (3-5 days).
  4. Testing—check search relevance, facets, speed (1-2 days).
  5. Deployment—deploy to production, configure monitoring (1 day).

What's Included

After completion, you receive:

  • Configured Elasticsearch cluster (single node or cluster) with access
  • Indices with custom analyzers for Russian language
  • Integration with your application via REST API
  • Data synchronization scripts (e.g., from PostgreSQL)
  • Operation documentation
  • Training for your team

We provide a 3-month guarantee: if search doesn't work as expected, we fix it for free.

Cluster Monitoring

We recommend monitoring cluster health via _cluster/health and enabling slowlog for search queries. Use Elastic Metricbeat to collect metrics—this helps detect degradation in time.

Common Configuration Mistakes
  • Incorrect mapping—dynamic mapping leads to unexpected field types, breaking facets. Always define schema explicitly.
  • Too small heap—insufficient memory causes frequent GC pauses and performance drop. Allocate at least 50% RAM, but no more than 32 GB.
  • No slowlog—without it, you won't see slow queries. Enable slowlog in config: index.search.slowlog.threshold.query.warn: 2s.
  • Ignoring replicas—for fault tolerance, at least 1 replica is needed. Set number_of_replicas: 1.

Start the Work

If your search is slow or can't handle the load, contact us. We'll evaluate your system and propose a solution. Order a turnkey Elasticsearch setup—get a free consultation. Don't delay: every second of delay costs you customers. Request an audit today.