Користувачі пишуть неохайні запити. Вони роблять помилки, використовують неоднозначні терміни і рідко виражають точно те, що хочуть. Розуміння запитів трансформує сирий ввід користувача в структурований інтент, з яким пошукова система може працювати. Це міст між тим, що користувачі пишуть, і тим, що вони мають на увазі.
Де використовується
Розуміння запитів забезпечує пошук скрізь:
- Веб-пошук: Google, Bing аналіз запитів
- E-commerce: Пошук продуктів, фасетна навігація
- Корпоративний пошук: Пошук документів, бази знань
- Голосові асистенти: Розуміння природної мови
- Підтримка клієнтів: Роутинг інтентів, FAQ matching
- Юридичний/медичний: Обробка спеціалізованої термінології
Пайплайн розуміння запитів
| Етап | Призначення | Приклади |
|---|---|---|
| Токенізація | Розбиття на одиниці | "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" |
Що ви робите на практиці
- Проаналізуйте логи запитів - Що насправді шукають користувачі?
- Побудуйте таксономію - Інтенти, сутності, атрибути
- Зберіть тренувальні дані - Анотуйте запити мітками
- Реалізуйте пайплайн - Модульні, дебажні етапи
- Обробіть edge cases - Порожні запити, дуже довгі запити
- Оцініть end-to-end - Метрики якості пошуку
- Моніторте в продакшені - Відстежуйте збої та дрейф
- Ітеруйте безперервно - Цикл фідбеку користувачів
Спочатку базова лінія
Почніть з правил та словників перед 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 латентність |
Типові помилки та чек-лист
- [ ] Over-correction: Виправлення слів, що не мають помилок
- [ ] Неправильний інтент: Неоднозначні запити неправильно класифіковані
- [ ] Плутанина сутностей: Схожі назви сутностей змішані
- [ ] Шум розширення: Додавання нерелевантних термінів
- [ ] Long-tail запити: Рідкісні запити не обробляються
- [ ] Латентність: Пайплайн занадто повільний
- [ ] Змішування мов: Багатомовні запити
Чек-лист для продакшену
Перед деплоєм: - [ ] Аналіз покриття на логах запитів - [ ] 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
Пов'язані сторінки
- Індекс: ML Field Guide
- Семантичний пошук
- Learning to Rank
- Named Entity Recognition
Додаткові матеріали
- Query Understanding for Search Engines
- Understanding Searches Better Than Ever Before
- Elasticsearch Query DSL
Спробуйте наживо
Дослідіть пов'язану модель на практиці: відкрийте Word Embeddings Explorer - NLP Vector Space Visualization і змінюйте параметри під час роботи. Усе лишається у вашому браузері.
▶ Відкрити симуляцію Word Embeddings Explorer - NLP Vector Space Visualization