Elasticsearch: Setting Up Search for Web Applications

Your catalog is growing, and name-based search via PostgreSQL starts to lag — customers leave without waiting for results. We configure Elasticsearch for your task: analyze data structure, design indexes and analyzers, and sync with PostgreSQL. We deliver turnkey — from audit to implementation and ongoing support — so search runs fast and reliably.

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

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1359
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1330
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1077
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1113
  • Website development for SBH Partners
    Website development for SBH Partners
    1201
  • Website development for Red Pear
    Website development for Red Pear
    619

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—request a consultation. Don't delay: every second of delay costs you customers. Request an audit today.