О нас Руководства Проекты Контакты
Админка
пожалуйста подождите

Введение

Elasticsearch предоставляет мощные возможности полнотекстового поиска, которые значительно превосходят простые запросы базы данных с LIKE. Построенный на Apache Lucene, он предлагает оценку релевантности, нечеткое сопоставление (fuzzy matching), агрегации и поиск почти в реальном времени. В этом руководстве рассматривается практическая реализация Elasticsearch для типовых требований к поиску.

Настройка

Установка через Docker

# docker-compose.yml
version: '3.8'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ports:
- "9200:9200"
volumes:
- esdata:/usr/share/elasticsearch/data
kibana:
image: docker.elastic.co/kibana/kibana:8.11.0
ports:
- "5601:5601"
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
volumes:
esdata:

Проверка установки

curl http://localhost:9200

Управление индексами

Создание индекса с mapping

curl -X PUT "localhost:9200/products" -H 'Content-Type: application/json' -d'
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0,
"analysis": {
"analyzer": {
"custom_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": ["lowercase", "asciifolding", "snowball"]
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "custom_analyzer",
"fields": {
"keyword": { "type": "keyword" }
}
},
"description": {
"type": "text",
"analyzer": "custom_analyzer"
},
"category": {
"type": "keyword"
},
"price": {
"type": "float"
},
"in_stock": {
"type": "boolean"
},
"created_at": {
"type": "date"
},
"tags": {
"type": "keyword"
}
}
}
}'

Типы полей

TypeUse Case
textПолнотекстовый поиск (analyzed)
keywordТочное совпадение, сортировка, агрегации
integer/long/floatЧисла
dateДаты
booleanTrue/false
nestedМассивы объектов

Операции с документами

Индексация документов

# Один документ
curl -X POST "localhost:9200/products/_doc/1" -H 'Content-Type: application/json' -d'
{
"name": "Wireless Bluetooth Headphones",
"description": "Premium noise-canceling headphones with 30-hour battery",
"category": "electronics",
"price": 199.99,
"in_stock": true,
"tags": ["audio", "wireless", "premium"],
"created_at": "2024-01-15"
}'
# Пакетная индексация
curl -X POST "localhost:9200/_bulk" -H 'Content-Type: application/json' -d'
{"index": {"_index": "products", "_id": "2"}}
{"name": "USB-C Charging Cable", "category": "accessories", "price": 19.99}
{"index": {"_index": "products", "_id": "3"}}
{"name": "Laptop Stand", "category": "accessories", "price": 49.99}
'

Обновление документов

# Частичное обновление
curl -X POST "localhost:9200/products/_update/1" -H 'Content-Type: application/json' -d'
{
"doc": {
"price": 179.99,
"in_stock": false
}
}'
# Обновление с помощью script
curl -X POST "localhost:9200/products/_update/1" -H 'Content-Type: application/json' -d'
{
"script": {
"source": "ctx._source.price -= params.discount",
"params": { "discount": 20 }
}
}'

Поисковые запросы

Базовый поиск

# Запрос match (analyzed)
curl -X GET "localhost:9200/products/_search" -H 'Content-Type: application/json' -d'
{
"query": {
"match": {
"name": "wireless headphones"
}
}
}'
# Multi-match (поиск по нескольким полям)
curl -X GET "localhost:9200/products/_search" -H 'Content-Type: application/json' -d'
{
"query": {
"multi_match": {
"query": "wireless audio",
"fields": ["name^2", "description", "tags"],
"type": "best_fields"
}
}
}'

Boolean-запросы

curl -X GET "localhost:9200/products/_search" -H 'Content-Type: application/json' -d'
{
"query": {
"bool": {
"must": [
{ "match": { "name": "headphones" } }
],
"filter": [
{ "term": { "category": "electronics" } },
{ "range": { "price": { "lte": 300 } } },
{ "term": { "in_stock": true } }
],
"should": [
{ "term": { "tags": "premium" } }
],
"must_not": [
{ "term": { "tags": "refurbished" } }
]
}
}
}'

Нечеткий поиск

# Обрабатывает опечатки
curl -X GET "localhost:9200/products/_search" -H 'Content-Type: application/json' -d'
{
"query": {
"fuzzy": {
"name": {
"value": "headpohnes",
"fuzziness": "AUTO"
}
}
}
}'
# Match с fuzziness
curl -X GET "localhost:9200/products/_search" -H 'Content-Type: application/json' -d'
{
"query": {
"match": {
"name": {
"query": "wireles headpohnes",
"fuzziness": "AUTO"
}
}
}
}'

Автодополнение по префиксу

curl -X GET "localhost:9200/products/_search" -H 'Content-Type: application/json' -d'
{
"query": {
"prefix": {
"name.keyword": "Wire"
}
}
}'
# Лучше: используйте completion suggester для автодополнения

Агрегации

Terms-агрегация

curl -X GET "localhost:9200/products/_search" -H 'Content-Type: application/json' -d'
{
"size": 0,
"aggs": {
"categories": {
"terms": {
"field": "category",
"size": 10
}
}
}
}'

Range-агрегация

curl -X GET "localhost:9200/products/_search" -H 'Content-Type: application/json' -d'
{
"size": 0,
"aggs": {
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{ "to": 50 },
{ "from": 50, "to": 100 },
{ "from": 100, "to": 200 },
{ "from": 200 }
]
}
}
}
}'

Nested-агрегации

curl -X GET "localhost:9200/products/_search" -H 'Content-Type: application/json' -d'
{
"size": 0,
"aggs": {
"categories": {
"terms": { "field": "category" },
"aggs": {
"avg_price": { "avg": { "field": "price" } },
"price_stats": { "stats": { "field": "price" } }
}
}
}
}'

Интеграция с PHP

composer require elasticsearch/elasticsearch
<?php
use Elasticsearch\ClientBuilder;
$client = ClientBuilder::create()
->setHosts(['localhost:9200'])
->build();
// Индексировать документ
$params = [
'index' => 'products',
'id' => '1',
'body' => [
'name' => 'Wireless Headphones',
'price' => 199.99,
'category' => 'electronics',
],
];
$client->index($params);
// Поиск
$params = [
'index' => 'products',
'body' => [
'query' => [
'bool' => [
'must' => [
['match' => ['name' => $searchQuery]],
],
'filter' => [
['term' => ['category' => $category]],
['range' => ['price' => ['lte' => $maxPrice]]],
],
],
],
'from' => $offset,
'size' => $limit,
'sort' => [
['_score' => 'desc'],
['created_at' => 'desc'],
],
],
];
$response = $client->search($params);
foreach ($response['hits']['hits'] as $hit) {
$product = $hit['_source'];
$score = $hit['_score'];
echo "{$product['name']} (score: {$score})\n";
}
// Всего результатов
$total = $response['hits']['total']['value'];

Интеграция с Node.js

const { Client } = require('@elastic/elasticsearch');
const client = new Client({ node: 'http://localhost:9200' });
// Индексировать документ
await client.index({
index: 'products',
id: '1',
document: {
name: 'Wireless Headphones',
price: 199.99,
category: 'electronics',
},
});
// Поиск
const result = await client.search({
index: 'products',
query: {
bool: {
must: [
{ match: { name: searchQuery } },
],
filter: [
{ term: { category: 'electronics' } },
{ range: { price: { lte: 300 } } },
],
},
},
from: 0,
size: 10,
sort: [
{ _score: 'desc' },
{ created_at: 'desc' },
],
});
result.hits.hits.forEach((hit) => {
console.log(hit._source.name, hit._score);
});

Синхронизация с базой данных

Синхронизация на основе событий

// После вставки/обновления в базе данных
class ProductObserver
{
private $elasticsearch;
public function saved(Product $product)
{
$this->elasticsearch->index([
'index' => 'products',
'id' => $product->id,
'body' => [
'name' => $product->name,
'description' => $product->description,
'price' => $product->price,
'category' => $product->category->name,
'updated_at' => $product->updated_at->toIso8601String(),
],
]);
}
public function deleted(Product $product)
{
$this->elasticsearch->delete([
'index' => 'products',
'id' => $product->id,
]);
}
}

Команда bulk reindex

// Команда Artisan
class ReindexProducts extends Command
{
protected $signature = 'search:reindex';
public function handle()
{
// Удалить и пересоздать индекс
$this->elasticsearch->indices()->delete(['index' => 'products']);
$this->createIndex();
// Пакетная индексация
$products = Product::with('category')->cursor();
$batch = [];
foreach ($products as $product) {
$batch[] = ['index' => ['_index' => 'products', '_id' => $product->id]];
$batch[] = $this->formatProduct($product);
if (count($batch) >= 1000) {
$this->elasticsearch->bulk(['body' => $batch]);
$batch = [];
}
}
if (!empty($batch)) {
$this->elasticsearch->bulk(['body' => $batch]);
}
}
}

Советы по производительности

Настройки индекса

{
"settings": {
"index": {
"refresh_interval": "30s",
"number_of_replicas": 1
}
}
}

Bulk-операции

// Используйте bulk API для нескольких документов
$params = ['body' => []];
foreach ($products as $product) {
$params['body'][] = ['index' => ['_index' => 'products', '_id' => $product->id]];
$params['body'][] = $product->toSearchArray();
if (count($params['body']) >= 2000) {
$client->bulk($params);
$params['body'] = [];
}
}
if (!empty($params['body'])) {
$client->bulk($params);
}

Оптимизация запросов

  1. Используйте filters для точных совпадений (кэшируются)
  2. Ограничивайте возвращаемые поля с помощью _source
  3. Используйте пагинацию через from/size
  4. Избегайте глубокой пагинации — вместо этого используйте search_after

Лучшие практики

  1. Тщательно проектируйте mappings — последующие изменения обходятся дорого
  2. Используйте подходящие analyzers для вашего языка
  3. Поддерживайте данные в синхронизации с вашей основной базой данных
  4. Мониторьте состояние кластера и свободное место на диске
  5. Используйте aliases для reindexing без простоя
  6. Тестируйте запросы на реалистичных объемах данных

Заключение

Elasticsearch предоставляет мощные возможности поиска, которые улучшают пользовательский опыт. Продуманно проектируйте mappings, используйте подходящие analyzers и поддерживайте данные синхронизированными с вашей основной базой данных. Шаблоны из этого руководства помогут вам реализовать эффективный полнотекстовый поиск в ваших приложениях.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
Зетка Интерактив