Quickwit — wyszukiwarka logów i traces oparta o storage obiektowy
Wyszukiwarka napisana w Ruście od zera pod S3: dane leżą na storage obiektowym, węzły są bezstanowe, a otwarcie fragmentu indeksu zajmuje 60 ms mimo latencji chmury. Rozbieramy architekturę splitów i hotcache, tryby wdrożenia, zgodność z API Elasticsearcha, retencję działającą na splitach zamiast dokumentów oraz konkretne koszty żądań na S3. Do tego uczciwy rachunek z tego, co znaczy dla projektu przejęcie przez Datadoga w styczniu 2025 i dwuletnia przerwa między stabilnymi wydaniami.
Elasticsearch do logów jest drogi nie dlatego, że jest złym oprogramowaniem. Jest drogi, bo trzyma indeksy na dyskach węzłów, które muszą działać bez przerwy — a te dyski muszą być szybkie, a pamięć węzła musi rosnąć razem z indeksem. Pięćset gigabajtów logów oznacza klaster, który kosztuje co miesiąc tyle samo niezależnie od tego, że dziewięćdziesiąt kilka procent tych danych nikt nigdy nie odczyta.
Konsekwencja jest znana każdemu, kto kiedyś szukał przyczyny awarii sprzed kwartału: retencja trzydziestu dni nie jest decyzją techniczną, tylko finansową. Logi znikają nie dlatego, że przestały być potrzebne, ale dlatego, że ich trzymanie w gorącym indeksie przestało się opłacać.
Quickwit (github.com/quickwit-oss/quickwit) atakuje ten problem od strony architektury: to wyszukiwarka napisana w Ruście, zaprojektowana od zera pod storage obiektowy. Indeksy leżą na S3, węzły wyszukujące są bezstanowe i nie trzymają żadnych danych, a mimo wysokiej latencji storage'u obiektowego wyszukiwanie jest subsekundowe. Retencja przestaje kosztować jak klaster i zaczyna kosztować jak S3.
Poniżej: jak to działa pod spodem (mechanizm jest ciekawy i wart zrozumienia nawet bez planów wdrożenia), jak to uruchomić, jak podłączyć do tego logi z kontenerów i traces z OpenTelemetry — oraz co dla przyszłości projektu znaczy fakt, że od stycznia 2025 jego właścicielem jest Datadog.
Stan projektu i kwestia właściciela
Zacznijmy od liczb z API GitHuba na 15 września 2026:
- 11 595 gwiazdek i 593 forki,
- repozytorium założone 13 kwietnia 2021, kod w Ruście, licencja Apache-2.0,
- ostatni commit z 8 września 2026, ruch w repozytorium wyraźny — ponad sto commitów w oknie, które sprawdziliśmy,
- 809 otwartych zgłoszeń,
- najnowsze wydanie
v0.9.0z 25 lipca 2026, a poprzednie stabilne —v0.8.2z 17 czerwca 2024.
Ta ostatnia para dat to ponad dwa lata bez stabilnego wydania i nie da się o niej nie napisać. Wyjaśnienie jest znane: 9 stycznia 2025 Quickwit został przejęty przez Datadoga (ogłoszenie zespołu Quickwita, wpis Datadoga). Przy tej okazji licencja zmieniła się z AGPL na Apache-2.0, a zespół zadeklarował skupienie się na integracji z produktem Datadoga przy jednoczesnym wydaniu dużej aktualizacji dla społeczności. Wersja 0.9.0 jest właśnie tą aktualizacją.
Projekt nie jest porzucony — commity lecą, wydanie 0.9.0 jest realne i obszerne, a licencja Apache-2.0 jest dla komercyjnego użytkownika korzystniejsza niż poprzednia AGPL. Ale rytm wydawniczy i priorytety należą teraz do firmy, która sprzedaje płatny produkt w tej samej kategorii. Przy decyzji o oparciu na tym narzędziu obserwowalności na najbliższe trzy lata to jest czynnik do policzenia, nie do zignorowania.
Drobiazg tej samej natury: README na gałęzi main wciąż ogłasza „We just released Quickwit 0.8!". Nic to nie psuje technicznie, ale mówi coś o uwadze poświęcanej dziś komunikacji z społecznością.
Jak to działa: splity i hotcache
Cała przewaga Quickwita siedzi w jednym pomyśle, więc warto go zrozumieć. Indeks jest podzielony na splity — małe, niezależne indeksy identyfikowane UUID-em. Każdy split leży na storage obiektowym jako zestaw plików, a razem z nimi Quickwit zapisuje plik hotcache.
Hotcache jest kluczem do wszystkiego: dzięki niemu otwarcie splitu leżącego na Amazon S3 zajmuje mniej niż 60 milisekund. To z kolei pozwala na rzecz, która w klasycznej architekturze wyszukiwarki jest niemożliwa — całkowitą bezstanowość węzłów wyszukujących. Węzeł nie musi nic wiedzieć o indeksach. Dodanie albo usunięcie węzła zajmuje sekundy i nie wymaga przenoszenia danych.
Wewnątrz splitu dokumenty są zapisane w trzech różnych strukturach, a to, które z nich powstają dla danego pola, konfiguruje się w mapowaniu indeksu:
- indeks odwrócony — do szybkiego wyszukiwania pełnotekstowego,
fast field— magazyn kolumnowy, odpowiednik doc values z Lucene'a. Wymagany do liczenia agregatów i pozwalający na część zaawansowanych filtrów,doc store— magazyn wierszowy, z którego bierze się treść dopasowanych dokumentów.
Metadane splitu zawierają jego stan oraz — i to jest drugi kluczowy element — minimalną i maksymalną wartość pola znacznika czasu. Zapytanie z filtrem czasowym pozwala więc odciąć nierelewantne splity, nie otwierając ich wcale. Przy logach, gdzie praktycznie każde zapytanie ma zakres czasowy, to jest sedno wydajności: szukanie w ostatniej godzinie z pięciu lat historii dotyka kilku splitów, nie wszystkich.
Pięć usług
Quickwit to jeden plik binarny (albo jeden obraz Dockera), którego rolę wybiera się opcją --service. Usług jest cztery podstawowe i jedna serwisowa:
- Indexer — pobiera dokumenty ze źródeł i buduje indeksy,
- Searcher — wykonuje zapytania z REST API,
- Metastore — przechowuje metadane indeksów w bazie zgodnej z PostgreSQL-em albo w pliku na storage'u obiektowym,
- Control plane — rozdziela i koordynuje zadania indeksowania,
- Janitor — wykonuje zadania cykliczne: odśmiecanie, zadania usuwania i egzekwowanie polityki retencji.
Każdy węzeł niezależnie od roli obsługuje REST API oraz interfejs wyszukiwania i administracji, a żądania, których nie potrafi obsłużyć, przekazuje do właściwego węzła. Klaster formuje się protokołem chitchat — członkostwo i detekcja awarii oparte na rekoncyliacji Scuttlebutt i detektorze phi-accrual, czyli pomysłach pożyczonych z Cassandry i DynamoDB.
Rozdział pracy przy zapytaniu jest prosty i elegancki: węzeł, który dostał zapytanie, zostaje węzłem korzeniowym na czas tego żądania. Pobiera metadane z metastore, ustala relewantne splity, rozdziela pracę na węzły liściowe i scala wyniki. Rozdział używa haszowania rendezvous, które daje stabilne przypisanie splitów do węzłów przy dołączaniu i odchodzeniu węzłów — a stabilność przypisania to warunek sensownego cachowania.
Control plane działa na trzech mechanizmach naraz: reaguje na zdarzenia z metastore (utworzenie, usunięcie, przełączenie źródła), co 3 sekundy sprawdza zgodność planu pożądanego z rzeczywistym, i raz na minutę przebudowuje plan od zera na podstawie najnowszego stanu metastore — na wypadek, gdyby przez problemy sieciowe przegapił jakieś zdarzenie. To ostatnie jest dobrym przykładem projektowania pod założenie, że sieć zawodzi.
Pierwsze uruchomienie
Najkrótsza droga to obraz Dockera:
mkdir qwdata
docker run --rm -v $(pwd)/qwdata:/quickwit/qwdata \
-p 127.0.0.1:7280:7280 \
quickwit/quickwit:0.9.0 runInterfejs wstaje na http://localhost:7280, a stan sprawdza się jednym żądaniem:
curl http://localhost:7280/api/v1/versionAlternatywnie instalator dobiera właściwe archiwum dla systemu i rozpakowuje je w katalogu roboczym:
curl -L https://raw.githubusercontent.com/quickwit-oss/quickwit/main/install.sh | sh
cd ./quickwit-v*/ && ./quickwit runIndeks konfiguruje się plikiem YAML. Przykład z dokumentacji, dla zbioru postów ze Stack Overflow, pokazuje wszystkie elementy, które trzeba rozumieć:
version: 0.8
index_id: stackoverflow
doc_mapping:
field_mappings:
- name: title
type: text
tokenizer: default
record: position
stored: true
- name: body
type: text
tokenizer: default
record: position
stored: true
- name: creationDate
type: datetime
fast: true
input_formats:
- rfc3339
fast_precision: seconds
timestamp_field: creationDate
search_settings:
default_search_fields: [title, body]
indexing_settings:
commit_timeout_secs: 10Trzy rzeczy z tego pliku decydują o zachowaniu całości. timestamp_field włącza opisane wyżej przycinanie splitów po czasie — bez niego każde zapytanie dotyka wszystkiego. default_search_fields określa pola przeszukiwane, gdy zapytanie nie wskazuje pola jawnie. A commit_timeout_secs ustala, jak szybko zaindeksowane dokumenty stają się widoczne — i jednocześnie, jak dużo małych splitów powstaje, o czym za chwilę przy kosztach.
Pola niezadeklarowane nie są tracone: tryb dynamiczny jest domyślny, więc nieznane pola nadal są indeksowane, z domyślnie włączonymi fast fields dla agregacji i tokenizerem raw dla tekstu. Quickwit obsługuje więc zarówno pracę bez schematu, jak i schemat ścisły — a przy logach z kilkunastu usług tryb dynamiczny to zwykle jedyne sensowne wyjście.
Tryby wdrożenia i skalowanie
Tryb standalone to jedno polecenie quickwit run ze wszystkimi usługami na jednym węźle. Tryb klastrowy wymaga wspólnego storage'u — S3, MinIO albo odpowiednika — i ma gotowy chart Helma.
Wydajność, którą podaje dokumentacja i którą warto zapamiętać jako punkt odniesienia: jeden indexer na małej instancji z 4 vCPU indeksuje 20–40 MB/s, czyli 1–3 TB na dobę. Dla większości firm oznacza to, że część indeksująca to jedna maszyna, a skalować trzeba wyłącznie searchery — i to jest scenariusz, który dokumentacja wprost rekomenduje jako punkt startowy.
Trzy ograniczenia trzeba znać przed projektowaniem klastra:
- Wysoka dostępność indeksowania jest tylko z Kafką. FAQ mówi to wprost: HA jest dostępne dla wyszukiwania, a dla indeksowania — wyłącznie ze źródłem Kafka. Indeksowanie jednego źródła na wielu indekserach też wymaga Kafki, przy czym natywne indeksowanie rozproszone doszło w wersji 0.9,
- Metastore plikowy nie znosi wielu instancji. Jest dobry do wdrożeń jednowęzłowych i małych, ale bezpieczny tylko wtedy, gdy da się zagwarantować, że w danym momencie działa dokładnie jeden. Przy poważnym obciążeniu dokumentacja zaleca PostgreSQL,
- Metastore klastrowy to dziś tylko PostgreSQL. Nie ma alternatywy do wyboru.
Wymiarowanie: konkretne liczby
Dokumentacja podaje wytyczne, które oszczędzają pierwszą rundę zgadywania. Dla indekserów:
- około 7,5 MB/s na rdzeń, 4 GB RAM na rdzeń, nigdy mniej niż 8 GB RAM na instancję,
- katalog danych na wolumenie co najmniej 120 GB — mieści się tam cache splitów (domyślnie 100 GB), kolejka ingestu (domyślnie 4 GiB) i budowane właśnie indeksy,
- preferowane lokalne SSD; dyski zdalne działają, jeśli dają około 20 MB/s zapisu na rdzeń przy użyciu ingest API albo 10 MB/s przy innych źródłach — dla wolumenów EBS to odpowiednio 320 albo 160 IOPS na rdzeń,
- pułapka: wykorzystanie więcej niż czterech rdzeni wymaga rozbicia obciążenia na wiele potoków indeksowania. Robi się to przez wiele indeksów albo partycjonowane źródło (Kafka, ingest API v2). Instancja z szesnastoma rdzeniami i jednym potokiem będzie się nudzić.
Dla searcherów, gdzie wydajność zależy głównie od charakteru zapytań (termy są tańsze od agregacji):
- 8 GB RAM na rdzeń przy wolnym storage'u o wysokiej latencji, takim jak S3; przy szybszym storage'u można zejść do 4 GB na rdzeń,
- nigdy mniej niż 4 GB RAM na instancję, a przy wielu równoległych agregacjach — więcej, bo każde żądanie może zużyć do 500 MB RAM na węzeł,
- searchery nie używają dysku, dopóki nie włączy się jawnie cache'u splitów. To one są tą częścią, którą skaluje się w górę i w dół pod obciążenie.
Pozostałe usługi są lekkie: control plane, metastore i janitor potrzebują po jednym rdzeniu i 2 GB RAM. Trzy zastrzeżenia: control plane i janitor muszą być dokładnie po jednym w klastrze; metastore plikowy również jeden, natomiast metastore na PostgreSQL-u może działać w wielu instancjach dla wysokiej dostępności; a janitor korzystający z Delete API należy wymiarować jak indexer, bo usuwanie to realna praca na splitach. Sam PostgreSQL na metastore to dla większości przypadków jeden rdzeń i 4 GB RAM. Do eksperymentów i małych proof of concept wszystko na jednym węźle: 2 rdzenie i 8 GB RAM.
Zgodność z Elasticsearchem, czyli droga migracji
To jest element, który decyduje o tym, czy wdrożenie jest tygodniem pracy czy kwartałem. Quickwit implementuje znaczący podzbiór API Elasticsearcha i OpenSearcha:
- zgodne z ES API ingestu, w tym częściowa obsługa
_bulk— czyli istniejące log shippery (Vector, Fluent Bit, Syslog) migrują bez przepisywania, - najpopularniejsze endpointy wyszukiwania, query DSL i agregacje,
- możliwość udawania wersji: jeśli klient odmawia połączenia z powodu brakujących nagłówków, opcja
extra_headersw konfiguracji węzła pozwala podać się za dowolną zgodną wersję Elasticsearcha albo OpenSearcha. Rozwiązanie nieelegackie i bardzo praktyczne.
Warto przy tym mieć realistyczne oczekiwania: to jest podzbiór. Przed migracją trzeba sprawdzić na liście wspieranych endpointów i agregacji, czy to, czego używamy, jest w środku — bo część konstrukcji nie jest.
Skąd biorą się dane
Quickwit natywnie mówi OpenTelemetry Protocol i wystawia endpoint gRPC (domyślnie port 7281) przyjmujący logi i traces z kolektora OTEL:
exporters:
otlp:
endpoint: quickwit-host:7281
tls:
insecure: true
service:
pipelines:
logs:
exporters:
- otlpPoza tym działają agenty oparte o HTTP: przetestowane są Vector i Fluent Bit, FluentD jest zapowiadany. Wyjątek, na który trzeba uważać: Logstash nie działa przez wyjście elasticsearch — da się go podłączyć wyłącznie wyjściem HTTP w formacie json.
Do indeksowania ciągłego z kolejek Quickwit łączy się bezpośrednio z Kafką, Pulsarem i Kinesis, z gwarancją exactly-once, a w wersji 0.9 doszło źródło SQS. Do jednorazowego wgrania danych jest ingest z pliku.
Traces i Grafana
Dla rozproszonego śledzenia Quickwit jest natywny dla Jaegera — można go podłączyć jako backend składowania, a wersja 0.9 dodała obsługę Jaegera v2. Do Grafany jest osobne źródło danych, więc panele z logami i traces buduje się tam, gdzie zespół już patrzy.
Metryk natomiast nie ma — README trzyma je na mapie drogowej. W drzewie kodu jest prototypowa praca nad modelem danych opartym o Parquet i DataFusion, ale nota wydawnicza 0.9.0 mówi jednoznacznie: to jest wczesny prototyp, nie jest wbudowany w binaria ani obrazy 0.9.0 i nie jest wspieraną funkcją tej wersji. Do metryk potrzebne jest więc osobne narzędzie.
Logi z kontenerów w praktyce
Najkonkretniejszy scenariusz dla wdrożenia kontenerowego: kolektor OpenTelemetry z odbiornikiem filelog czyta pliki logów Dockera, wyciąga metadane kontenera i wysyła całość do Quickwita przez gRPC.
receivers:
filelog:
include:
- /var/lib/docker/containers/*/*-json.log
operators:
- id: parser-docker
type: json_parser
timestamp:
layout: '%Y-%m-%dT%H:%M:%S.%LZ'
parse_from: attributes.time
- type: remove
field: attributes.time
- from: attributes.log
to: body
type: move
processors:
batch:
timeout: 5s
exporters:
otlp/qw:
endpoint: quickwit:7281
tls:
insecure: true
service:
pipelines:
logs:
receivers: [filelog]
processors: [batch]
exporters: [otlp/qw]Pełna wersja z dokumentacji dokłada operator regex_parser wyciągający z tagu Dockera nazwę kontenera, nazwę obrazu i identyfikator, a potem przenosi je do atrybutów zasobu (docker.container.name, docker.image.name, docker.container.id). Warto to zrobić od razu — bez tych atrybutów logi z dwudziestu kontenerów są jedną nierozróżnialną masą, a filtrowanie po nazwie usługi jest pierwszą rzeczą, po którą sięga się przy incydencie.
Ścieżka /var/lib/docker/containers/*/*-json.log zależy od systemu i sterownika logowania, więc to jest pierwsza rzecz do sprawdzenia, gdy nic nie przychodzi. tls: insecure: true jest do przyjęcia wewnątrz jednej sieci Dockera i nie jest do przyjęcia, gdy kolektor i Quickwit stoją na różnych maszynach.
Jak się w tym szuka
Quickwit ma własny, prosty język zapytań zbudowany wokół klauzul w formie pole:warunek. Nazwę pola można pominąć — wtedy używane są pola z default_search_fields. Typy klauzul:
- term —
app_name:tantivy,process_id:1234, - prefiks termu —
app_name:tant*, - zbiór termów —
severity:IN [error warn], czyli najkrótsza droga do „pokaż błędy i ostrzeżenia", - fraza —
full_name:"john doe", i jej wariant prefiksowytitle:"how to m"*, - istnienie —
error:*dopasuje dokumenty, w których pole w ogóle występuje. Przy logach bez schematu to bardzo użyteczne, - zakres —
duration:[0 TO 1000}, z nawiasami domkniętymi i otwartymi.
Klauzule łączy się operatorami AND i OR, grupuje nawiasami, a negacja to NOT albo prefiks -. Dwie reguły pierwszeństwa warto zapamiętać, bo cicho zmieniają znaczenie zapytania: brak operatora oznacza AND, a AND wiąże silniej niż OR — czyli type:rose AND color:red OR type:violet AND color:blue znaczy to samo co wersja z nawiasami. Negacja ma pierwszeństwo nad wszystkim, więc -a AND b to (-a) AND b, a nie -(a AND b).
Retencja i usuwanie danych
Retencja konfiguruje się w pliku indeksu i jest zaskakująco prosta:
retention:
period: 90 days
schedule: dailyWażny szczegół implementacyjny: retencja usuwa splity, nie pojedyncze dokumenty. Split leci wtedy, gdy now() - split.time_range.end >= retention_policy.period. Granica czasowa jest więc przybliżona z dokładnością do rozpiętości splitu, co przy logach nie ma znaczenia, a przy wymogu „usuwamy dokładnie po 90 dniach" — ma. schedule przyjmuje wyrażenie cron albo formę czytelną (hourly, daily, weekly), a period składa się z rozpiętości czasowych w rodzaju 2 days 3h 24min.
Osobno istnieje Delete API do usuwania dokumentów spełniających zapytanie i dokumentacja od razu określa jego przeznaczenie: zgodność z RODO, używane oszczędnie. „Oszczędnie" jest tu doprecyzowane — zalecane kilka zapytań na godzinę albo na dobę. Cztery rzeczy, o których trzeba wiedzieć:
- Usuwanie jest zadaniem długotrwałym. Zadanie usuwania wykonuje się na wszystkich splitach utworzonych przed jego zgłoszeniem i przy zapytaniu dopasowującym dokumenty w wielu splitach może trwać godziny,
- Działa tylko na splitach „dojrzałych", czyli takich, które nie będą już scalane. Dojrzałość zależy od polityki scalania i parametru
maturation_period. W najgorszym razie usunięcie zgłoszone w chwilit0poczeka dot0 + maturation_period, aż niedojrzałe splity dojrzeją, - Postępu nie da się dziś monitorować. Dokumentacja przyznaje to wprost i wskazuje otwarte zgłoszenie. Przy żądaniu usunięcia danych z RODO oznacza to, że potwierdzenie wykonania trzeba sobie zorganizować samemu,
- Każde zadanie ma inkrementalny identyfikator (
opstamp), a operacje są wsadowane per split — jeśli split maopstamp = n, a ostatnie zadanien + 10, dziesięć zapytań usuwających wykona się na nim naraz.
Ile to realnie kosztuje na S3
Dokumentacja podaje tu liczby, a nie ogólniki, i to jest najlepszy rozdział do przeczytania przed decyzją. Ceny żądań w AWS S3: 0,0004 USD za 1000 żądań GET i 0,005 USD za 1000 żądań PUT.
Zapis. Przy indeksowaniu Quickwit wysyła nowe splity na S3 i progresywnie scala je, aż osiągną 10 milionów dokumentów — wtedy split staje się „dojrzały", ma zwykle od 1 do 10 GB i wymaga około dwóch żądań PUT (jedno na 5 GB). Przy domyślnych parametrach (commit_timeout_secs 60 sekund, merge_factor 10) i wolumenie miliona dokumentów na minutę koszt żądań PUT to mniej niż 1 USD miesięcznie.
Odczyt. Tu jest wzór, którym warto policzyć własny przypadek:
liczba żądań = liczba_splitów
* ((liczba_pól_szukanych * liczba_termów * 3)
+ liczba_pól_z_fieldnorms
+ 1 (fast field znacznika czasu, jeśli jest))
+ liczba_zwróconych_dokumentówWzór zakłada, że hotcache jest już w cache'u — ładuje się przy pierwszym zapytaniu dla każdego splitu. Gdy pozycje nie są włączone, na term wypadają tylko dwa żądania GET zamiast trzech. Dokumentacja stawia przy tym jasne ostrzeżenie: koszty tych żądań potrafią szybko narosnąć przy dużej liczbie splitów albo przy więcej niż 10 zapytaniach na sekundę.
Praktyczny wniosek jest taki: Quickwit jest tani przy profilu „dużo danych, mało zapytań" — czyli dokładnie przy logach i traces, gdzie zapisujemy stale, a czytamy przy incydencie. Przy profilu „wyszukiwarka w produkcie z setkami zapytań na sekundę" ta architektura pracuje przeciwko nam i trzeba wybrać inne narzędzie.
Dwie rady infrastrukturalne z tej samej strony dokumentacji: trzymaj Quickwita w tej samej sieci co storage, bo dostawcy liczą sobie za transfer poza swoją sieć, a zapytania z zewnątrz dokładają latencji; i wybieraj instancje o wysokiej przepustowości sieciowej — dokumentacja wskazuje c5n.2xlarge jako najlepszy stosunek jakości do ceny w ich testach.
Co przyniosła wersja 0.9.0
Skoro to pierwsze stabilne wydanie od dwóch lat, warto wiedzieć, co jest w środku:
- Ingest V2 domyślnie — nowa ścieżka indeksowania obsługuje teraz istniejący endpoint
/api/v1/{index}/ingest, - Odciążanie wyszukiwania liściowego na AWS Lambda — searchery mogą oddać pracę liściową funkcjom Lambda. Dla obciążeń nierównomiernych, gdzie zapytania przychodzą rzadko i w skokach, to potencjalnie duża oszczędność,
- Jaeger v2, endpointy zgodne z Trino, agregacje kompozytowe i źródło SQS,
- Opcjonalna weryfikacja mTLS na REST API i osobny serwer HTTP na kontrole zdrowia,
- szereg poprawek w control plane, storage'u, planowaniu wyszukiwań, wydajności i bezpieczeństwie.
Do tego jedno ostrzeżenie, które nota wydawnicza wyróżnia i którego zignorowanie kończy się źle: przed aktualizacją z 0.8.x zrób kopię metastore. Wersja 0.9 migruje go przy pierwszym zapisie, a wycofanie zmiany wymaga przywrócenia tej kopii.
Kiedy to ma sens w naszym stacku
Uczciwie: Quickwit nie jest narzędziem, które wchodzi do typowego projektu w Laravelu. Nie zastąpi wyszukiwania w produkcie — do tego są Meilisearch, Typesense albo pełnotekstowe indeksy w Postgresie, i wszystkie trzy są prostsze. Quickwit jest narzędziem obserwowalności i ma sens dokładnie wtedy, gdy spełnione są trzy warunki naraz:
- logów i traces jest dużo — setki gigabajtów albo więcej,
- retencja liczy się w miesiącach, a nie w dniach, bo tego wymaga zgodność albo diagnostyka,
- zapytań jest niewiele i pojawiają się przy incydentach, a nie w ciągłym ruchu użytkowników.
Jeśli te trzy warunki są spełnione, układ wygląda tak: logi z kontenerów zbiera kolektor OpenTelemetry albo Vector i wysyła je na endpoint OTLP Quickwita; traces z aplikacji idą przez SDK OpenTelemetry tą samą drogą; indeksy leżą na S3 albo MinIO; frontem jest Grafana przez dedykowane źródło danych, a dla traces alternatywnie Jaeger. Indexer to jedna maszyna, searcherów tyle, ile potrzeba pod zapytania.
Alternatywy, które trzeba rozważyć w tej samej rozmowie: Grafana Loki indeksuje tylko etykiety, więc jest tańsza i słabsza w wyszukiwaniu pełnotekstowym; Elasticsearch albo OpenSearch daje najszerszy ekosystem i najwyższy rachunek; Datadog nie wymaga utrzymywania niczego i kosztuje najwięcej — a przy okazji jest dziś właścicielem Quickwita, co samo w sobie jest komentarzem do tego, gdzie ten rynek się układa.
Pułapki
- Właścicielem projektu jest Datadog, a między 0.8.2 i 0.9.0 minęły ponad dwa lata. Commity są aktywne i licencja jest permisywna, ale tempo i kierunek rozwoju zależą od priorytetów firmy sprzedającej konkurencyjny produkt komercyjny,
- Nie ma metryk. Prace nad modelem Parquet są prototypem poza wydaniem, więc do metryk potrzebne jest osobne narzędzie,
- HA indeksowania tylko z Kafką. Bez Kafki mamy jeden indexer i to jest pojedynczy punkt awarii ścieżki zapisu,
- Metastore: albo plik dla jednej instancji, albo PostgreSQL. Trzeciej opcji nie ma, a plikowy metastore przy dwóch działających instancjach to uszkodzone metadane,
- Delete API jest drogie i nieobserwowalne, a retencja działa na splitach. Przy twardych wymogach „usuń dokładnie te dane do daty X" trzeba to zaprojektować świadomie i samemu potwierdzać wykonanie,
- Koszty żądań GET rosną z liczbą splitów i z QPS. Powyżej dziesięciu zapytań na sekundę dokumentacja sugeruje kontakt z zespołem — czyli że to już nie jest scenariusz standardowy,
- Zgodność z Elasticsearchem jest częściowa. Przed migracją sprawdź na liście, czy używane endpointy i agregacje są wspierane,
- Transfer danych między sieciami kosztuje. Quickwit i storage powinny być u tego samego dostawcy,
- 809 otwartych zgłoszeń i dokumentacja miejscami nieaktualna względem kodu (README reklamuje wersję 0.8).
Podsumowanie
Quickwit rozwiązuje realny i drogi problem: przechowywanie logów w formie, w której nadal da się je przeszukać, bez płacenia za klaster proporcjonalny do wolumenu historii. Architektura jest przemyślana i uczciwie ciekawa — hotcache pozwalający otworzyć split na S3 w 60 ms i bezstanowe searchery to rozwiązanie, które warto rozumieć niezależnie od decyzji wdrożeniowej. Co z tego wynika przy planowaniu:
- Policz kontekst właścicielski. Datadog przejął projekt 9 stycznia 2025, licencja zmieniła się wtedy z AGPL na Apache-2.0, a stabilne wydanie 0.9.0 wyszło po ponadwuletniej przerwie. To nie dyskwalifikuje narzędzia, ale przy horyzoncie trzyletnim jest to czynnik ryzyka,
- Profil obciążenia decyduje. Dużo danych i mało zapytań — Quickwit świeci. Setki zapytań na sekundę — architektura pracuje przeciwko nam,
- Ustaw
timestamp_field. Bez niego nie ma przycinania splitów po czasie, czyli nie ma głównego mechanizmu wydajności, - Zacznij od jednego indeksera — 20–40 MB/s na 4 vCPU to 1–3 TB na dobę. Skaluj searchery, nie indeksery,
- Rozbij indeksowanie na wiele potoków, jeśli indexer ma więcej niż cztery rdzenie — inaczej ich nie wykorzysta,
- Logi z kontenerów zbieraj kolektorem OTEL z odbiornikiem
filelogi od razu wyciągaj nazwę kontenera oraz obrazu do atrybutów zasobu, - Na produkcję weź PostgreSQL jako metastore, a plikowy zostaw dla wdrożeń jednowęzłowych,
- Zaplanuj HA indeksowania przez Kafkę albo świadomie zaakceptuj jeden indexer,
- Migruj przez zgodność z Elasticsearchem — log shippery zostają, zmienia się backend; sprawdź wcześniej listę wspieranych endpointów,
- Retencję ustaw w konfiguracji indeksu, a Delete API traktuj wyłącznie jako narzędzie do RODO, z własnym potwierdzaniem wykonania,
- Trzymaj Quickwita u tego samego dostawcy co storage i wybieraj instancje o wysokiej przepustowości sieci,
- Przed aktualizacją z 0.8.x zrób kopię metastore. Migracja przy pierwszym zapisie jest nieodwracalna bez tej kopii,
- Do metryk weź inne narzędzie — Quickwit ich nie ma i w 0.9.0 nie zapowiada.
Licencja: Quickwit jest rozpowszechniany na Apache License 2.0 — licencji permisywnej, bez copyleftu, z jawnym udzieleniem licencji patentowej i bez warunków przy komercyjnym użyciu poza zachowaniem noty o prawach autorskich oraz wskazaniem istotnych zmian w plikach objętych licencją. W praktyce oznacza to pełną swobodę: własna instancja obsługująca logi naszych aplikacji i aplikacji klientów, forki z własnymi poprawkami bez obowiązku publikacji, a nawet oferowanie na tym usługi hostowanej. Warto wiedzieć, że nie zawsze tak było — projekt startował na AGPL, a zmiana na Apache-2.0 nastąpiła w kontekście przejęcia przez Datadoga. Dla komercyjnego użytkownika to zmiana jednoznacznie na lepsze i jeden z niewielu przypadków, w których przejęcie projektu open source poluzowało licencję, zamiast ją zacieśnić.