Головна▸Статті▸Пошук та інформаційний пошук

Розуміння запитів

ML для розуміння пошукових запитів - класифікація інтенту, парсинг запитів, екстракція сутностей та розширення запитів.

mysimulator teamОновлено 2026-01≈ 7 хв читання▶ Open the simulation

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

Де використовується

Розуміння запитів забезпечує пошук скрізь:

Пайплайн розуміння запитів

Етап Призначення Приклади
Токенізація Розбиття на одиниці "iphone 13" → ["iphone", "13"]
Нормалізація Стандартизація "iPhone" → "iphone", "13pro" → "13 pro"
Виправлення помилок Виправлення друкарок "ipohne" → "iphone"
Класифікація інтенту Який тип запиту? Навігаційний, інформаційний, транзакційний
Екстракція сутностей Ідентифікація сутностей "iphone 13" → {brand: Apple, product: iPhone, model: 13}
Розширення запиту Додавання пов'язаних термінів "laptop" → "laptop OR notebook OR computer"
Переписування запиту Трансформація запиту "cheap flights NYC" → "budget flights to New York City"

Що ви робите на практиці

  1. Проаналізуйте логи запитів - Що насправді шукають користувачі?
  2. Побудуйте таксономію - Інтенти, сутності, атрибути
  3. Зберіть тренувальні дані - Анотуйте запити мітками
  4. Реалізуйте пайплайн - Модульні, дебажні етапи
  5. Обробіть edge cases - Порожні запити, дуже довгі запити
  6. Оцініть end-to-end - Метрики якості пошуку
  7. Моніторте в продакшені - Відстежуйте збої та дрейф
  8. Ітеруйте безперервно - Цикл фідбеку користувачів

Спочатку базова лінія

Почніть з правил та словників перед ML:

import re
from collections import defaultdict

class QueryUnderstandingBaseline:
    """Rule-based розуміння запитів."""

    def __init__(self):
        self.brand_dict = {'apple', 'samsung', 'google', 'sony'}
        self.category_dict = {'phone', 'laptop', 'tablet', 'headphones'}
        self.attribute_patterns = {
            'color': r'\b(red|blue|black|white|green|gold|silver)\b',
            'size': r'\b(\d+)\s*(gb|tb|inch|")\b',
            'price': r'\b(cheap|expensive|budget|premium)\b'
        }
        self.spelling_corrections = {
            'ipohne': 'iphone',
            'samsugn': 'samsung',
            'labtop': 'laptop'
        }

    def process(self, query):
        """Повний пайплайн розуміння запитів."""
        # Нормалізація
        query = self.normalize(query)

        # Виправлення помилок
        query = self.spell_correct(query)

        # Екстракція сутностей
        entities = self.extract_entities(query)

        # Класифікація інтенту
        intent = self.classify_intent(query, entities)

        return {
            'original_query': query,
            'normalized_query': query,
            'entities': entities,
            'intent': intent
        }

    def normalize(self, query):
        """Нормалізація тексту запиту."""
        query = query.lower().strip()
        query = re.sub(r'\s+', ' ', query)
        query = re.sub(r'[^\w\s]', ' ', query)
        return query

    def spell_correct(self, query):
        """Просте виправлення помилок за словником."""
        words = query.split()
        corrected = []

        for word in words:
            if word in self.spelling_corrections:
                corrected.append(self.spelling_corrections[word])
            else:
                corrected.append(word)

        return ' '.join(corrected)

    def extract_entities(self, query):
        """Екстракція відомих сутностей з запиту."""
        entities = defaultdict(list)
        words = set(query.split())

        # Виявлення бренду
        for brand in self.brand_dict:
            if brand in words or brand in query:
                entities['brand'].append(brand)

        # Виявлення категорії
        for category in self.category_dict:
            if category in words:
                entities['category'].append(category)

        # Патерни атрибутів
        for attr_type, pattern in self.attribute_patterns.items():
            matches = re.findall(pattern, query)
            if matches:
                entities[attr_type].extend(matches)

        return dict(entities)

    def classify_intent(self, query, entities):
        """Класифікація інтенту запиту."""
        # Навігаційний: шукає конкретний бренд/продукт
        if 'brand' in entities and len(query.split()) <= 3:
            return 'navigational'

        # Транзакційний: сигнали покупки
        buy_signals = ['buy', 'price', 'cheap', 'order', 'purchase', 'купити', 'ціна']
        if any(signal in query for signal in buy_signals):
            return 'transactional'

        # Інформаційний: питання, порівняння
        info_signals = ['how', 'what', 'which', 'best', 'vs', 'compare', 'review', 'як', 'що', 'який']
        if any(signal in query for signal in info_signals):
            return 'informational'

        return 'unknown'

Класифікація інтенту з ML

import torch
from transformers import BertTokenizer, BertForSequenceClassification
from sklearn.model_selection import train_test_split

class IntentClassifier:
    """BERT-based класифікатор інтентів."""

    def __init__(self, model_name='bert-base-uncased', num_intents=5):
        self.tokenizer = BertTokenizer.from_pretrained(model_name)
        self.model = BertForSequenceClassification.from_pretrained(
            model_name,
            num_labels=num_intents
        )
        self.intent_labels = [
            'navigational', 'informational', 'transactional',
            'local', 'other'
        ]

    def predict(self, queries):
        """Прогнозування інтентів для запитів."""
        inputs = self.tokenizer(
            queries,
            padding=True,
            truncation=True,
            max_length=64,
            return_tensors='pt'
        )

        with torch.no_grad():
            outputs = self.model(**inputs)
            predictions = torch.argmax(outputs.logits, dim=1)

        return [self.intent_labels[p] for p in predictions]

    def train(self, queries, labels, epochs=3):
        """Fine-tune на парах запит-інтент."""
        # Конвертація міток в індекси
        label_indices = [self.intent_labels.index(l) for l in labels]

        # Токенізація
        inputs = self.tokenizer(
            queries,
            padding=True,
            truncation=True,
            max_length=64,
            return_tensors='pt'
        )
        inputs['labels'] = torch.tensor(label_indices)

        # Тренування
        optimizer = torch.optim.AdamW(self.model.parameters(), lr=2e-5)

        self.model.train()
        for epoch in range(epochs):
            optimizer.zero_grad()
            outputs = self.model(**inputs)
            loss = outputs.loss
            loss.backward()
            optimizer.step()
            print(f'Epoch {epoch+1}: Loss {loss.item():.4f}')

class QueryEntityExtractor:
    """NER-based екстракція сутностей з запитів."""

    def __init__(self):
        from transformers import pipeline
        self.ner = pipeline('ner', aggregation_strategy='simple')

        # Маппінг сутностей для пошуку
        self.entity_mapping = {
            'ORG': 'brand',
            'PRODUCT': 'product',
            'LOC': 'location',
            'CARDINAL': 'quantity'
        }

    def extract(self, query):
        """Екстракція сутностей з запиту."""
        entities = self.ner(query)

        result = defaultdict(list)
        for entity in entities:
            entity_type = self.entity_mapping.get(
                entity['entity_group'], 
                entity['entity_group'].lower()
            )
            result[entity_type].append({
                'text': entity['word'],
                'score': entity['score']
            })

        return dict(result)

Розширення запитів

Покращення recall через розширення запитів:

from gensim.models import KeyedVectors
import numpy as np

class QueryExpander:
    """Розширення запитів пов'язаними термінами."""

    def __init__(self, embeddings_path=None):
        if embeddings_path:
            self.embeddings = KeyedVectors.load(embeddings_path)
        else:
            self.embeddings = None

        # Ручні синоніми для критичних термінів
        self.synonyms = {
            'laptop': ['notebook', 'computer', 'pc'],
            'phone': ['mobile', 'smartphone', 'cell'],
            'cheap': ['budget', 'affordable', 'inexpensive'],
            'buy': ['purchase', 'order', 'get']
        }

    def expand(self, query, max_expansions=3):
        """Розширення запиту пов'язаними термінами."""
        words = query.lower().split()
        expansions = set()

        for word in words:
            # Перевірка ручних синонімів
            if word in self.synonyms:
                expansions.update(self.synonyms[word][:max_expansions])

            # Використання ембедінгів для семантичного розширення
            if self.embeddings and word in self.embeddings:
                similar = self.embeddings.most_similar(word, topn=max_expansions)
                for term, score in similar:
                    if score > 0.7:  # Поріг подібності
                        expansions.add(term)

        return list(expansions - set(words))

    def build_expanded_query(self, query, strategy='or'):
        """Побудова розширеного пошукового запиту."""
        original_terms = query.split()
        expansions = self.expand(query)

        if strategy == 'or':
            all_terms = original_terms + expansions
            return ' OR '.join(all_terms)
        elif strategy == 'boost':
            # Оригінальні терміни з вищим boost
            boosted = [f'{t}^2.0' for t in original_terms]
            return ' '.join(boosted + expansions)
        else:
            return query

class SpellingCorrector:
    """Контекстно-залежне виправлення помилок."""

    def __init__(self, vocabulary):
        self.vocabulary = set(vocabulary)
        self.word_freq = defaultdict(int)
        for word in vocabulary:
            self.word_freq[word] += 1

    def correct(self, query):
        """Виправлення помилок у запиті."""
        words = query.split()
        corrected = []

        for word in words:
            if word in self.vocabulary:
                corrected.append(word)
            else:
                # Пошук найближчого матчу
                candidates = self.get_candidates(word)
                if candidates:
                    best = max(candidates, key=lambda w: self.word_freq[w])
                    corrected.append(best)
                else:
                    corrected.append(word)

        return ' '.join(corrected)

    def get_candidates(self, word, max_distance=2):
        """Отримання кандидатів виправлення в межах edit distance."""
        candidates = []

        for vocab_word in self.vocabulary:
            if abs(len(vocab_word) - len(word)) <= max_distance:
                distance = self.edit_distance(word, vocab_word)
                if distance <= max_distance:
                    candidates.append(vocab_word)

        return candidates

    def edit_distance(self, s1, s2):
        """Розрахунок відстані Левенштейна."""
        if len(s1) < len(s2):
            return self.edit_distance(s2, s1)

        if len(s2) == 0:
            return len(s1)

        previous_row = range(len(s2) + 1)
        for i, c1 in enumerate(s1):
            current_row = [i + 1]
            for j, c2 in enumerate(s2):
                insertions = previous_row[j + 1] + 1
                deletions = current_row[j] + 1
                substitutions = previous_row[j] + (c1 != c2)
                current_row.append(min(insertions, deletions, substitutions))
            previous_row = current_row

        return previous_row[-1]

Метрики оцінки

Метрика Призначення Вимірювання
Intent accuracy Коректне прогнозування інтенту Accuracy, F1 per intent
Entity F1 Якість екстракції сутностей Precision, Recall, F1
Spelling accuracy Якість виправлень % коректних виправлень
Search relevance End-to-end якість NDCG, MRR з/без QU
Latency Час відповіді p50, p99 латентність

Типові помилки та чек-лист

Чек-лист для продакшену

Перед деплоєм: - [ ] Аналіз покриття на логах запитів - [ ] A/B тест фреймворк готовий - [ ] Бюджет латентності встановлено - [ ] Fallback поведінка визначена - [ ] Логування для дебагу

Після деплою: - [ ] Моніторинг розподілу інтентів - [ ] Відстеження rates виправлень - [ ] Вимірювання впливу на якість пошуку - [ ] Огляд failure cases - [ ] Безперервні оновлення моделі

Інструменти та стек

Категорія Інструменти
NLP spaCy, Stanza, transformers
Spelling SymSpell, hunspell, pyspellchecker
Ембедінги FastText, Word2Vec, sentence-transformers
Пошук Elasticsearch, Solr, Vespa
Оцінка BEIR, ranx

Приклад міні-кейсу

Сценарій: Розуміння запитів для пошуку продуктів e-commerce.

Пайплайн: 1. Нормалізація: lowercase, видалення спецсимволів 2. Виправлення помилок: "ipone 14 blak" → "iphone 14 black" 3. Екстракція сутностей: {brand: apple, product: iphone, model: 14, color: black} 4. Класифікація інтенту: transactional 5. Розширення: додати "smartphone", "mobile"

Результати: - 15% покращення NDCG пошуку - 23% зменшення null результатів - Виправлення помилок ловить 8% запитів - Середня латентність: 12ms

Пов'язані сторінки

Додаткові матеріали

Спробуйте наживо

Дослідіть пов'язану модель на практиці: відкрийте Word Embeddings Explorer - NLP Vector Space Visualization і змінюйте параметри під час роботи. Усе лишається у вашому браузері.

▶ Відкрити симуляцію Word Embeddings Explorer - NLP Vector Space Visualization

Що ви знайшли?

Додати кроки відтворення (опційно)