Ten przewodnik pokazuje, jak zintegrować Elasticsearch z Laravel.
📋 Spis treści
- Wprowadzenie
- Instalacja Elasticsearch
- Konfiguracja połączenia
- Tworzenie indeksu
- Implementacja wyszukiwania
- Testowanie
- Podsumowanie
- Kod źródłowy
1. Dlaczego Elasticsearch?
Eloquent Laravel jest świetny dla większości zapytań, ale gdy potrzebujesz wyszukiwania pełnotekstowego, agregacji lub wyszukiwania geograficznego, Elasticsearch jest najlepszym rozwiązaniem. Czysta integracja z Laravel zapewnia, że Twój kod pozostanie łatwy w utrzymaniu i testowaniu.
2. Instalacja i konfiguracja
Najpierw zainstaluj oficjalnego klienta PHP dla Elasticsearch:
composer require elasticsearch/elasticsearch
Następnie dodaj Elasticsearch do pliku docker-compose.yml
:
services:
...
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:9.0.1
container_name: laravel_elasticsearch
environment:
- discovery.type=single-node
- ES_JAVA_OPTS=-Xms1g -Xmx1g
- xpack.security.enabled=false
- xpack.monitoring.collection.enabled=true
ports:
- "9200:9200"
volumes:
- esdata:/usr/share/elasticsearch/data
networks:
- laravel-network
voluems:
...
esdata:
driver: local
Następnie utwórz plik konfiguracyjny dla Elasticsearch:
touch config/elasticsearch.php
<?php
return [
'host' => env('ELASTICSEARCH_HOST', 'http://elasticsearch:9200'),
];
3. Warstwa serwisu: Opakowanie klienta Elasticsearch
Aby zachować czysty i testowalny kod, zamknij całą logikę Elasticsearch w dedykowanym serwisie.
app/Services/ElasticsearchService.php
<?php
namespace App\Services;
use Elastic\Elasticsearch\ClientBuilder;
class ElasticsearchService
{
public function client()
{
return ClientBuilder::create()
->setHosts([config('elasticsearch.host')])
->build();
}
public function createArticlesIndex(): void
{
$params = [
'index' => 'articles',
'body' => [
'settings' => [
'analysis' => [
'analyzer' => [
'autocomplete' => [
'tokenizer' => 'autocomplete',
'filter' => ['lowercase'],
],
],
'tokenizer' => [
'autocomplete' => [
'type' => 'edge_ngram',
'min_gram' => 1,
'max_gram' => 20,
'token_chars' => ['letter', 'digit'],
],
],
],
],
'mappings' => [
'properties' => [
'title' => [
'type' => 'text',
'analyzer' => 'autocomplete',
'search_analyzer' => 'standard',
],
'tags' => [
'type' => 'keyword',
],
'user' => [
'type' => 'keyword',
],
'location' => [
'type' => 'geo_point',
],
'city_name' => [
'type' => 'keyword',
],
],
],
],
];
if ($this->client()->indices()->exists(['index' => 'articles'])->asBool()) {
$this->client()->indices()->delete(['index' => 'articles']);
}
$this->client()->indices()->create($params);
}
public function index(array $params)
{
return $this->client()->index($params);
}
}
4. Obiekt transferu danych (DTO) dla filtrów wyszukiwania
Użycie DTO (z spatie/laravel-data) utrzymuje logikę wyszukiwania w czystości i zapewnia bezpieczeństwo typów.
app/DTO/ArticleFilterData.php
<?php
namespace App\DTO;
use Spatie\LaravelData\Data;
class ArticleFilterData extends Data
{
public function __construct(
public ?string $q = '',
public ?string $tag = null,
public ?string $city = null,
public ?int $radius = null,
public ?float $lat = null,
public ?float $lon = null,
public ?int $page = 1,
public ?int $size = 20,
) {}
}
5. Serwis wyszukiwania: Logika biznesowa dla wyszukiwania artykułów
Ten serwis buduje zapytanie Elasticsearch na podstawie DTO i zwraca ustrukturyzowane wyniki.
app/Services/ArticleSearchService.php
<?php
namespace App\Services;
use App\DTO\ArticleFilterData;
use Illuminate\Support\Arr;
readonly class ArticleSearchService
{
public function __construct(private ElasticsearchService $es) {}
public function search(ArticleFilterData $filters): array
{
$queryBody = [
'index' => 'articles',
'from' => ($filters->page - 1) * $filters->size,
'size' => $filters->size,
'body' => [
'query' => [
'bool' => [
'must' => $this->buildMustQueries($filters),
'filter' => $this->buildFilterQueries($filters),
],
],
],
];
$results = $this->es->client()->search($queryBody);
return [
'articles' => collect($results['hits']['hits'])->pluck('_source')->all(),
'total' => $results['hits']['total']['value'] ?? 0,
'filters' => $filters->toArray(),
];
}
private function buildMustQueries(ArticleFilterData $filters): array
{
$must = [];
if ($filters->q) {
$must[] = mb_strlen($filters->q) <= 2
? ['wildcard' => ['title' => "*{$filters->q}*"]]
: [
'multi_match' => [
'query' => $filters->q,
'fields' => ['title^2', 'tags'],
'fuzziness' => 'auto',
'operator' => 'and',
'minimum_should_match' => '100%',
],
];
}
if ($filters->tag) {
$must[] = ['term' => ['tags' => $filters->tag]];
}
return $must;
}
private function buildFilterQueries(ArticleFilterData $filters): array
{
$filter = [];
if ($filters->city) {
$filter[] = ['term' => ['city_name' => $filters->city]];
}
if ($filters->lat && $filters->lon && $filters->radius > 0) {
$filter = array_filter($filter, fn($f) => !Arr::has($f, 'term.city_name'));
$filter[] = [
'geo_distance' => [
'distance' => "{$filters->radius}km",
'location' => [
'lat' => $filters->lat,
'lon' => $filters->lon,
],
],
];
}
return $filter;
}
}
6. Komenda do tworzenia indeksu
app/Console/Commands/ReindexArticlesElasticsearch.php
<?php
namespace App\Console\Commands;
use App\Jobs\ReindexArticles;
use App\Services\ElasticsearchService;
use Elastic\Elasticsearch\Exception\AuthenticationException;
use Elastic\Elasticsearch\Exception\ClientResponseException;
use Elastic\Elasticsearch\Exception\MissingParameterException;
use Elastic\Elasticsearch\Exception\ServerResponseException;
use Illuminate\Console\Command;
class ReindexArticlesElasticsearch extends Command
{
protected $signature = 'es:reindex-articles';
protected $description = 'Tworzy indeks artykułów z mapowaniem i reindeksuje artykuły do Elasticsearch';
/**
* @throws AuthenticationException
* @throws ClientResponseException
* @throws ServerResponseException
* @throws MissingParameterException
*/
public function handle(ElasticsearchService $es): void
{
$this->info('Tworzenie indeksu articles z mapowaniem...');
$es->createArticlesIndex();
$this->info('Reindeksowanie artykułów...');
ReindexArticles::dispatchSync();
$this->info('Gotowe!');
}
}
7. Zadanie do reindeksacji artykułów
app/Jobs/ReindexArticles.php
<?php
namespace App\Jobs;
use App\Models\Article;
use App\Models\User;
use App\Services\ElasticsearchService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ReindexArticles implements ShouldQueue
{
use Queueable;
public function handle(ElasticsearchService $es): void
{
Article::with(['user', 'tags'])->chunk(100, function ($articles) use ($es) {
foreach ($articles as $article) {
/** @var User $user */
$user = $article->user;
$es->index([
'index' => 'articles',
'id' => $article->id,
'body' => [
'title' => $article->title,
'slug' => $article->slug,
'content' => $article->content,
'user' => $user->name,
'tags' => $article->tags->pluck('name')->toArray(),
'location' => $article->location ?? null,
'city_name' => $article->city_name ?? null,
],
]);
}
});
}
}
8. Obserwator dla modelu Article
app/Observers/ArticleObserver.php
<?php
namespace App\Observers;
use App\Jobs\ReindexArticles;
use App\Models\Article;
use App\Services\ElasticsearchService;
class ArticleObserver
{
public function deleted(Article $article): void
{
$es = app(ElasticsearchService::class)->client();
try {
$es->delete([
'index' => 'articles',
'id' => $article->id,
]);
} catch (\Exception $e) {
\Log::info($e->getMessage());
}
}
public function saved(Article $article)
{
ReindexArticles::dispatch();
}
}
9. Podsumowanie
Masz teraz system wyszukiwania gotowy do produkcji z:
- Wyszukiwaniem w czasie rzeczywistym
- Zaawansowanym filtrowaniem i sortowaniem
- Optymalizacją wydajności
- Obsługą błędów
- Czystym, łatwym w utrzymaniu kodem
To podejście utrzymuje Twój kod Laravel w czystości, testowalności i gotowości do produkcji.
Śledź mnie na LinkedIn po więcej wskazówek o Laravel i DevOps!
Chcesz dowiedzieć się więcej o implementacji wyszukiwania w Laravel? Daj znać w komentarzach poniżej!
Kod źródłowy
Pełną implementację i więcej przykładów znajdziesz w repozytorium GitHub.