Szymon Borowski
Extended\Mind::Thesis()
Umysł nie kończy się na granicy czaszki — rozciąga się na narzędzia, notatki i środowisko. — Clark & Chalmers, 1998

Wdrożenie Meilisearch – od Docker Compose do Kubernetes

Szymon Borowski ·

Meilisearch to szybki silnik full-text search napisany w Rust, wyróżniający się bardzo niskim czasem odpowiedzi i wbudowaną tolerancją na literówki. Poniżej opisuję krok po kroku, jak został wdrożony w projekcie portfolio.

Docker Compose – środowisko developerskie

Do blog/docker-compose.yml dodany został serwis blog-meilisearch:

blog-meilisearch:
  image: getmeili/meilisearch:v1.11
  environment:
    MEILI_MASTER_KEY: ${MEILISEARCH_MASTER_KEY}
    MEILI_ENV: development
  volumes:
    - blog_meilisearch_data:/meili_data
  ports:
    - "127.0.0.1:7700:7700"
  healthcheck:
    test: ["CMD-SHELL", "wget --spider -q http://localhost:7700/health"]
    interval: 10s
    timeout: 5s
    retries: 5
    start_period: 30s
  networks:
    - blog-internal

Serwis jest podłączony wyłącznie do sieci blog-internal — nie jest eksponowany w sieci microservices ani web. Port 7700 jest bindowany tylko na 127.0.0.1, więc jest dostępny lokalnie wyłącznie dla potrzeb deweloperskich (np. przez panel Meilisearch).

Serwisy blog-app i blog-consumer otrzymały zmienne środowiskowe wskazujące na Meilisearch oraz warunek service_healthy w depends_on, gwarantujący, że aplikacja nie uruchomi się przed gotowością silnika:

MEILISEARCH_HOST: http://blog-meilisearch:7700
MEILISEARCH_KEY: ${MEILISEARCH_MASTER_KEY}
SCOUT_DRIVER: meilisearch

Docker Compose – produkcja (docker-compose.prod.yml)

W produkcyjnym pliku docker-compose.prod.yml serwis wygląda analogicznie, z tą różnicą, że MEILI_ENV jest ustawiony na production (co wyłącza panel administracyjny Meilisearch i wymusza użycie klucza master). Port nie jest eksponowany na hoście — komunikacja odbywa się wyłącznie wewnątrz sieci blog-internal. Dane indeksu są przechowywane w nazwanym wolumenie blog_meilisearch_data.

Kubernetes

W warstwie produkcyjnej (ArgoCD + K8s) Meilisearch działa jako StatefulSet w namespace portfolio. Kluczowe elementy konfiguracji:

  • StatefulSet z jedną repliką — gwarantuje stabilną tożsamość podu i stałe podpięcie wolumenu.
  • Headless ClusterIP Service (clusterIP: None) na porcie 7700 — umożliwia adresowanie podu po DNS (blog-meilisearch.portfolio.svc.cluster.local) bez load balancera.
  • PersistentVolumeClaim o pojemności 1 Gi (accessModes: ReadWriteOnce) generowany przez volumeClaimTemplates — dane indeksu przeżywają restart podu.
  • Klucz master key pochodzi z Kubernetes Secret (blog-secret, klucz MEILISEARCH_MASTER_KEY), nie jest hardkodowany w manifeście.
  • Liveness probe i readiness probe na endpoincie /health — Kubernetes odtwarza pod przy awarii i nie kieruje ruchu do niegotowego kontenera.
env:
  - name: MEILI_ENV
    value: production
  - name: MEILI_MASTER_KEY
    valueFrom:
      secretKeyRef:
        name: blog-secret
        key: MEILISEARCH_MASTER_KEY
resources:
  requests:
    cpu: 50m
    memory: 128Mi
  limits:
    memory: 256Mi

Zmiany w serwisie blog

Zależności

Do composer.json trafiły dwie paczki:

  • laravel/scout — oficjalny pakiet wyszukiwania Laravel; dostarcza trait Searchable i interfejs do różnych silników.
  • meilisearch/meilisearch-php — oficjalne PHP SDK Meilisearch, używane przez Scout pod spodem.

Trait Searchable i metody modelu Post

Model Post implementuje trait Searchable z Laravel Scout. Trzy metody kontrolują sposób indeksowania:

toSearchableArray() — buduje dokument przesyłany do Meilisearch. Tłumaczenia (PL + EN) są spłaszczane do jednej struktury płaskiej, co pozwala na wyszukiwanie w obu językach jednocześnie:

public function toSearchableArray(): array
{
    $this->loadMissing(['translations', 'categories', 'tags']);
    return [
        'id'           => $this->id,
        'slug'         => $this->slug,
        'title'        => $this->translations->pluck('title')->join(' | '),
        'excerpt'      => $this->translations->map(fn ($t) => Str::limit(strip_tags($t->excerpt ?? ''), 300))->join(' | '),
        'content'      => Str::limit(strip_tags($this->translations->pluck('content')->join(' ')), 3000),
        'categories'   => $this->categories->pluck('name')->toArray(),
        'tags'         => $this->tags->pluck('name')->toArray(),
        'cover_image'  => $this->cover_image,
        'published_at' => $this->published_at?->timestamp,
    ];
}

shouldBeSearchable() — gate kontrolujący, które posty wchodzą do indeksu. Do Meilisearch trafiają wyłącznie posty opublikowane, z published_at <= now() i bez soft delete:

public function shouldBeSearchable(): bool
{
    return $this->status === 'published'
        && $this->published_at !== null
        && $this->published_at <= now()
        && is_null($this->deleted_at);
}

makeAllSearchableUsing() — eager-loading relacji przy masowym imporcie, aby uniknąć N+1 przy php artisan scout:import.

Konfiguracja scout.php

W config/scout.php zdefiniowane zostały ustawienia per-indeks dla posts:

  • searchableAttributes: title, excerpt, content, categories, tags — kolejność wpływa na ranking wyników.
  • filterableAttributes: published_at — umożliwia filtrowanie po dacie po stronie Meilisearch.
  • sortableAttributes: published_at.
  • typoTolerance: 1 literówka dla słów ≥5 znaków, 2 literówki dla słów ≥9 znaków.
  • rankingRules: domyślny zestaw Meilisearch (words → typo → proximity → attribute → sort → exactness).

Asynchroniczne aktualizacje indeksu

Laravel Scout obsługuje kolejkowanie operacji indeksowania przez zmienną środowiskową SCOUT_QUEUE=true. Po jej ustawieniu dodanie lub usunięcie dokumentu z indeksu jest odkładane do kolejki Laravel (np. Redis), a nie wykonywane synchronicznie w trakcie żądania HTTP. To szczególnie istotne przy zapisywaniu postów przez panel admina — odpowiedź HTTP nie musi czekać na zakończenie operacji w Meilisearch.

Gdy post zmienia status (np. z draft na published), PostObserver nasłuchuje na zdarzenie updated i — przez mechanizm Scout — automatycznie kolejkuje re-indeksację. Zmiana statusu publikuje też zdarzenie do RabbitMQ (wymiana blog), informując inne serwisy o nowym poście.

Pełną re-indeksację wszystkich postów można wymusić poleceniem:

php artisan scout:import "App\Models\Post"

SearchController

Endpoint GET /api/search?q=… odpytuje jednocześnie trzy indeksy: posts (limit 5, z podświetlaniem <mark>), categories (limit 3) i tags (limit 5). W razie niedostępności Meilisearch kontroler łapie wyjątek i zwraca status 503 z pustymi kolekcjami wyników, dzięki czemu frontend nie traci działania.

Polubienia
Zaloguj — Zaloguj się, aby dodać komentarz.

Komentarze

Brak komentarzy