Реалізація пошуку з виправленням помилок для веб-застосунку

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація пошуку з виправленням помилок для веб-застосунку
Складна
~2-3 робочих дні
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація пошуку з исправленням опечаток для веб-додатків

Користувачи роблять помилки: "навушниик", "бесплодные", "samsng". Пошук без fuzzy matching повертає порожній результат, втрачаючи конверсію. Реалізуємо пошук з исправленням опечаток трьома способами — залежно від масштабу та вимог.

Метрика відстані: Levenshtein vs Damerau-Levenshtein

Відстань Левенштейна: мінімальна кількість вставок, видалень, замін для перетворення одного рядка в інший.

Дамерау-Левенштейн додає транспозицію (перестановку сусідніх символів): "наушинки" → "наушники" — це 1 транспозиція, а не 2 операції. Для пошуку краще.

PostgreSQL: pg_trgm

pg_trgm — розширення PostgreSQL для similarity-пошуку на основі триграм. Працює без зовнішніх сервісів.

CREATE EXTENSION IF NOT EXISTS pg_trgm;

-- Індекс для similarity search
CREATE INDEX idx_products_title_trgm ON products USING GIN (title gin_trgm_ops);
CREATE INDEX idx_products_description_trgm ON products USING GIN (description gin_trgm_ops);

-- Пошук із порогом схожості
SET pg_trgm.similarity_threshold = 0.3;

SELECT id, title, similarity(title, 'наушниик') AS sim
FROM products
WHERE title % 'наушниик'         -- оператор similarity
ORDER BY sim DESC
LIMIT 10;

-- Або комбінуємо FTS + fuzzy:
SELECT
    p.id,
    p.title,
    p.price,
    greatest(
        similarity(p.title, 'беспродные наушники'),
        ts_rank(p.search_vector, plainto_tsquery('russian', 'беспродные наушники'))
    ) AS relevance
FROM products p
WHERE
    p.title % 'беспродные наушники'
    OR p.search_vector @@ plainto_tsquery('russian', 'беспродные')
ORDER BY relevance DESC
LIMIT 20;

% — оператор схожості, використовує GIN-індекс. Без індексу деградує до seq scan.

Настройка порога: 0.3 — ліберально (багато шуму), 0.5 — строго (мало опечаток). Для коротких запитів (1–2 слова) поріг повинен бути нижчий.

Meilisearch: movimento пошуку з исправленням

Meilisearch написан на Rust, підтримує typo tolerance з коробки, простий у настройці.

# Docker
docker run -p 7700:7700 getmeili/meilisearch:latest

# Або через бінарник
curl -L https://install.meilisearch.com | sh
./meilisearch --master-key="your-master-key"

Настройка індексу:

import meilisearch

client = meilisearch.Client('http://localhost:7700', 'your-master-key')
index = client.index('products')

# Настройки пошуку
index.update_settings({
    'searchableAttributes': ['title', 'brand', 'description', 'tags'],
    'filterableAttributes': ['category_id', 'status', 'price', 'brand'],
    'sortableAttributes': ['price', 'created_at', 'popularity'],
    'rankingRules': [
        'words',
        'typo',
        'proximity',
        'attribute',
        'sort',
        'exactness',
    ],
    'typoTolerance': {
        'enabled': True,
        'minWordSizeForTypos': {
            'oneTypo': 5,     # слова >= 5 символів допускають 1 помилку
            'twoTypos': 9,    # слова >= 9 символів допускають 2 помилки
        },
        'disableOnWords': ['iPhone', 'iPad', 'MacBook'],  # бренди без fuzzy
        'disableOnAttributes': ['sku', 'barcode'],
    },
    'pagination': {
        'maxTotalHits': 10000,
    },
})

Індексування даних:

import asyncio
from typing import Any

async def sync_products_to_meilisearch(products: list[dict[str, Any]]) -> None:
    """Батчева синхронізація продуктів."""
    documents = [
        {
            'id': p['id'],
            'title': p['title'],
            'brand': p.get('brand', ''),
            'description': p.get('description', ''),
            'category_id': p['category_id'],
            'price': float(p['price']),
            'status': p['status'],
            'tags': [t['name'] for t in p.get('tags', [])],
            'created_at': p['created_at'].timestamp(),
            'popularity': p.get('view_count', 0),
        }
        for p in products
        if p['status'] == 'published'
    ]

    # Meilisearch приймає батчи до 100MB
    batch_size = 1000
    for i in range(0, len(documents), batch_size):
        batch = documents[i:i + batch_size]
        task = index.add_documents(batch)
        index.wait_for_task(task.task_uid)

Пошук:

from dataclasses import dataclass

@dataclass
class SearchParams:
    query: str
    category_id: int | None = None
    price_min: float | None = None
    price_max: float | None = None
    page: int = 1
    hits_per_page: int = 20
    sort: str = 'relevance'  # relevance | price:asc | price:desc | created_at:desc

def build_filter(params: SearchParams) -> str | None:
    filters = ['status = "published"']

    if params.category_id:
        filters.append(f'category_id = {params.category_id}')
    if params.price_min is not None:
        filters.append(f'price >= {params.price_min}')
    if params.price_max is not None:
        filters.append(f'price <= {params.price_max}')

    return ' AND '.join(filters) if filters else None

def search_products(params: SearchParams) -> dict:
    sort_map = {
        'price:asc':   ['price:asc'],
        'price:desc':  ['price:desc'],
        'created_at:desc': ['created_at:desc'],
        'relevance':   [],  # стандартні ranking rules
    }

    results = index.search(params.query, {
        'filter':       build_filter(params),
        'sort':         sort_map.get(params.sort, []),
        'page':         params.page,
        'hitsPerPage':  params.hits_per_page,
        'attributesToHighlight': ['title', 'description'],
        'highlightPreTag':  '<mark>',
        'highlightPostTag': '</mark>',
        'attributesToCrop': {'description': 200},
        'showMatchesPosition': False,
    })

    return results

Elasticsearch: Fuzzy запит

Якщо Elasticsearch уже використовується для FTS — fuzzy вбудований:

{
  "query": {
    "bool": {
      "should": [
        {
          "multi_match": {
            "query": "наушниик",
            "fields": ["title^3", "brand^2", "description"],
            "fuzziness": "AUTO",
            "prefix_length": 2,
            "max_expansions": 50
          }
        },
        {
          "match_phrase": {
            "title": {
              "query": "наушниик",
              "slop": 2
            }
          }
        }
      ]
    }
  }
}

prefix_length: 2 — перші 2 символи повинні збігатися точно. Зменшує кількість помилкових спрацьовувань та прискорює запит.

AUTO fuzzy: 0 помилок для ≤2 символів, 1 для 3–5 символів, 2 для 6+ символів.

Вибір підходу

Для малого каталогу (до 100k записів) з PostgreSQL — pg_trgm достатньо. Для великого каталогу з фасетами, фільтрами та вимогою <10ms — Meilisearch. Для аналітичної платформи з агрегаціями — Elasticsearch.

Графіки

pg_trgm (розширення, індекси, запити, настройка порога): 1 день. Meilisearch (deploy, настройка індексу, синхронізація, API): 2–3 дні. Fuzzy в існуючому Elasticsearch кластері: 1 день.