# EmbeddingGemma 2 w wyszukiwaniu po polsku: pomiar z 7 października 2026

Zbiór danych do artykułu w serwisie ekspertodsztucznejinteligencji.pl. Licencja plików własnych (skrypty, wyniki, rankingi,
pytania, ta metoda): CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/), z uznaniem autorstwa „Robert Marczyński,
ekspertodsztucznejinteligencji.pl”. Akapity w `korpus/fragmenty.json` pochodzą z tekstów tego serwisu, których autorem jest
Robert Marczyński, i są tu udostępnione na tej samej licencji.

## Pytanie

EmbeddingGemma 2 to otwarty model embeddingów od Google (premiera 6 października 2026, licencja Apache 2.0, 740 mln
parametrów, z czego 270 mln obsługuje tekst; wektory 768-wymiarowe z możliwością obcięcia do 512, 256 i 128; kontekst 8192
tokeny). Karta modelu mówi o ponad 100 językach, ale listy nie podaje i polskiego nie wymienia. Sprawdzono, czy polskie pytanie
zadane temu modelowi, uruchomionemu lokalnie na laptopie, prowadzi do właściwego tekstu w zbiorze polskich dokumentów.
Wynik porównano z wyszukiwaniem po słowach (BM25) i z mniejszym, starszym modelem wielojęzycznym (multilingual-e5-small).
Pomiar dotyczy wyłącznie tekstu; obrazów, nagrań ani wideo nie mierzono.

## Metoda

Miary, zestawy pytań i systemy zostały ustalone przed pierwszym uruchomieniem skryptu. Niczego nie strojono na pytaniach.
Pomiar ma trzy przebiegi (opis niżej); wyniki główne pochodzą z przebiegu 2, który poprawia błąd wejścia z przebiegu 1.

- Korpus: 90 tekstów serwisu (38 wpisów na blogu, 43 odpowiedzi, 9 stron usług) w stanie z 7 października 2026 (commit
  `47d00ae`). Treść po frontmatterze podzielono na akapity pustymi liniami; pominięto nagłówki, tabele, bloki kodu i akapity
  krótsze niż 15 słów; linki zamieniono na sam tekst, znaczniki HTML usunięto. Daje to 1633 akapity (od 88 do 1801 znaków,
  mediana 346; od 7 do 45 na tekst, mediana 17). Skrypt `przygotuj_korpus.py` odtwarza oba pliki korpusu bajt w bajt
  (sumy SHA-256 w `wyniki.json`).
- Pytania FAQ (423): wszystkie pytania z sekcji FAQ tych tekstów (od 3 do 8 na tekst). Trafny dokument to tekst, z którego
  pytanie pochodzi; odpowiedzi FAQ nie są częścią korpusu. Pytania bywają zrozumiałe tylko w kontekście tekstu (na przykład
  „Czy to nowe badanie z 2026 roku?”), co obniża wynik każdego systemu. Jedno pytanie („Ile trwa wdrożenie AI w firmie?”)
  występuje w dwóch tekstach, z różnym trafnym dokumentem, i liczy się dwa razy.
- Pytania potoczne (60): spisane ręcznie przed pomiarem, w stylu zapytań do wyszukiwarki, bez powtarzania tytułów
  („ile bierze konsultant AI”, „pracownicy używają ChatGPT bez wiedzy szefa”); 50 z 60 dzieli jednak z tytułem trafnego tekstu
  co najmniej jedno słowo dłuższe niż trzy litery (w pytaniach FAQ 226 z 423). Dla 12 z nich trafne są dwa lub trzy teksty.
  Plik `korpus/pytania-trudne.json`.
- Systemy: BM25 (biblioteka rank_bm25 0.2.2, BM25Okapi, tokeny `\w+` po zamianie na małe litery, bez odmiany i bez listy
  stopu, dokument = tytuł + akapit); multilingual-e5-small (`intfloat/multilingual-e5-small`, rewizja `614241f6`, 384
  wymiary, plik wag 471 MB, czyli około 118 mln parametrów, licencja MIT; pytanie `query: {pytanie}`, dokument
  `passage: {tytuł}. {akapit}`); EmbeddingGemma 2 (`google/embeddinggemma-2`, rewizja `914f7f89`, 768 wymiarów, plik wag
  1,49 GB; pytanie z promptem `SearchQuery` z konfiguracji modelu, czyli `task: search result | query: {pytanie}`, dokument
  w formacie z karty modelu `title: {tytuł} | text: {akapit}` bez dodatkowego promptu); EmbeddingGemma 2 w 256 wymiarach
  (te same wektory obcięte do pierwszych 256 współrzędnych i ponownie znormalizowane, bez ponownego kodowania). Oba modele
  załadowano przez sentence-transformers w domyślnej konfiguracji: EmbeddingGemma 2 w precyzji bfloat16 ze wszystkimi trzema
  enkoderami (tekst, obraz, dźwięk; 744 mln parametrów w pamięci), wektory rzutowane na float32.
- Ranking na poziomie dokumentu: liczy się podobieństwo pytania do każdego akapitu (iloczyn skalarny znormalizowanych
  wektorów, czyli cosinus; dla BM25 wynik BM25), a tekst dostaje najwyższą wartość spośród swoich akapitów.
- Miary: Hit@5 (odsetek pytań z co najmniej jednym trafnym tekstem w pierwszej piątce), Recall@5 (udział trafnych tekstów
  znalezionych w pierwszej piątce, uśredniony po pytaniach) i MRR@10 (odwrotność pozycji pierwszego trafnego tekstu, zero
  poza pierwszą dziesiątką).
- Czas i pamięć: czas ładowania modelu z lokalnej kopii (samego pobrania wag nie mierzono), czas kodowania 1633 akapitów,
  średni czas zapytania (dla modeli czas kodowania wszystkich pytań partiami po 32 podzielony przez ich liczbę, bez iloczynu z indeksem; dla BM25 mediana czasu liczenia wyniku jednego pytania), szczytowe zużycie pamięci procesu (`ru_maxrss`;
  wartość w trakcie procesu nie maleje, a e5-small był liczony wcześniej w tym samym procesie).
- Środowisko: Mac z Apple M5 Pro i 48 GB pamięci, macOS 26.6.2, Python 3.14.7, sentence-transformers 6.1.0, transformers
  5.19.0, torch 2.14.1 (MPS), numpy 1.26.4.
- Przebieg 1 (7 października 2026, 17:13; `wyniki-przebieg1.json`, `pomiar-przebieg1.py`): akapity dla EmbeddingGemma 2 były
  kodowane z `prompt_name="Document"` (w konfiguracji modelu `title: none | text: `) i jednocześnie z ręcznym formatem
  `title: {tytuł} | text: {akapit}`, czyli z podwójnym przedrostkiem, przed którym karta modelu ostrzega; e5-small dostawał sam
  akapit, bez tytułu. Błąd wykryto przy przeglądzie skryptu przed publikacją. Kontrolne powtórzenie przebiegu 1 tego samego dnia,
  z dodanymi sprawdzeniami, że wektory są skończone, dało identyczne wartości wszystkich miar. Ostrzeżenia numpy „divide by zero
  encountered in matmul” w logu pochodzą z biblioteki Accelerate na macOS i pojawiają się także dla losowych macierzy float32.
- Przebieg 2 (7 października 2026, 18:25; `wyniki.json`, `wyniki-per-pytanie.jsonl`, `pomiar.py`): poprawione wejście jak w
  opisie systemów; BM25 bez zmian. Z tego przebiegu pochodzą wyniki główne.
- Przebieg 3 (7 października 2026, 23:42; `rankingi-pelne.jsonl`, `pomiar-rankingi.py`): ten sam kod co w przebiegu 2, uzupełniony o zapis
  dziesięciu pierwszych tekstów i ich podobieństw dla każdego pytania (do przeliczenia MRR@10). Skrypt porównuje miary z
  `wyniki.json`: wszystkie identyczne. Czasów z tego przebiegu nie raportuję.
- Wersje: `requirements.txt` z przypiętymi pakietami. Skrypty przebiegów 2 i 3 przypinają rewizje obu modeli argumentem `revision=`; argument
  dopisano 8 października 2026, po przebiegach. Żaden plik modeli w lokalnym cache Hugging Face nie był modyfikowany po 17:15
  7 października (daty modyfikacji plików), więc przebiegi 2 i 3 szły na tych samych plikach co przebieg 1.

## Wynik (przebieg 2)

| System | FAQ Hit@5 | FAQ MRR@10 | Potoczne Hit@5 | Potoczne Recall@5 | Potoczne MRR@10 |
|---|---|---|---|---|---|
| BM25 | 74,7% | 0,587 | 91,7% | 87,2% | 0,800 |
| multilingual-e5-small (384 wym.) | 89,8% | 0,759 | 96,7% | 93,9% | 0,894 |
| EmbeddingGemma 2 (768 wym.) | 89,6% | 0,752 | 96,7% | 94,2% | 0,856 |
| EmbeddingGemma 2 (256 wym.) | 88,7% | 0,741 | 96,7% | 94,2% | 0,863 |

Dla pytań FAQ Recall@5 równa się Hit@5 (jeden trafny tekst na pytanie). n = 423 (FAQ) i 60 (potoczne). Trafienia (w kolejności z tabeli): FAQ 316,
380, 379 i 375 z 423; potoczne 55, 58, 58 i 58 z 60. Właściwy tekst na pierwszym miejscu: FAQ 198, 276, 266 i 268; potoczne
43, 51, 46 i 47. Pary niezgodne na FAQ: EmbeddingGemma 2 (768) trafił, a BM25 nie, 79 razy, odwrotnie 16 razy, oba pudło 28;
wobec e5-small 20 i 21, oba pudło 23; 768 wobec 256 wymiarów 11 i 7. Na 12 pytaniach potocznych z więcej niż jednym trafnym
tekstem Recall@5: BM25 61,1%, e5-small 77,8%, EmbeddingGemma 2 (768 i 256) 79,2%. Dokładny test dwumianowy na parach
niezgodnych: EmbeddingGemma 2 (768) wobec BM25 na FAQ p < 0,0001, wobec e5-small p = 1,0, 768 wobec 256 wymiarów p = 0,48;
na pytaniach potocznych wobec BM25 p = 0,375.

| System | Ładowanie | Kodowanie 1633 akapitów | Zapytanie (średnio) | Szczyt pamięci procesu |
|---|---|---|---|---|
| BM25 | brak | 0,04 s | 1,0 ms (mediana) | 66 MB |
| multilingual-e5-small | 4,7 s | 4,6 s (352 akapity/s) | 0,7 ms | 1117 MB |
| EmbeddingGemma 2, 768 wym. | 938 s (zob. niżej) | 31,0 s (52,6 akapitu/s) | 2,7 ms | 1798 MB |
| EmbeddingGemma 2, 256 wym. | jak wyżej | jak wyżej | 2,5 ms (to samo kodowanie) | jak wyżej |

Ładowanie EmbeddingGemma 2 trwało 7,7 s w przebiegu 1, 5,6 s w kontrolnym i 938 s w przebiegu 2. Ten ostatni czas to artefakt:
dziennik zasilania macOS (`pmset -g log`) pokazuje uśpienie komputera o 18:26:03 i wybudzenie o 18:41:28, czyli 925 s w trakcie
ładowania; w cache Hugging Face nic się wtedy nie zmieniło, a ostrzeżenie o nieuwierzytelnionych żądaniach występuje w logach
wszystkich trzech przebiegów, więc opóźnienia nie tłumaczy. Ładowanie bez sieci z lokalnej kopii (`HF_HUB_OFFLINE=1`) trwa około 2 s.
Indeks 1633 wektorów w float32
zajmuje 5,0 MB przy 768 wymiarach, 1,7 MB przy 256 i 2,5 MB dla e5-small (384).

## Wynik przebiegu 1 (dla porównania)

| System | FAQ Hit@5 | FAQ MRR@10 | Potoczne Hit@5 | Potoczne Recall@5 | Potoczne MRR@10 |
|---|---|---|---|---|---|
| BM25 | 74,7% | 0,587 | 91,7% | 87,2% | 0,800 |
| multilingual-e5-small, bez tytułu | 74,9% | 0,599 | 83,3% | 81,4% | 0,591 |
| EmbeddingGemma 2 (768), podwójny przedrostek | 90,3% | 0,757 | 96,7% | 95,0% | 0,876 |
| EmbeddingGemma 2 (256), podwójny przedrostek | 88,2% | 0,747 | 95,0% | 93,3% | 0,873 |

Czasy przebiegu 1: EmbeddingGemma 2 ładowanie 7,7 s, kodowanie 41,4 s (39,5 akapitu/s), zapytanie 6,8 ms (768) i 3,6 ms (256),
szczyt pamięci 1827 MB; e5-small 4,5 s, 4,4 s, 0,6 ms, 1122 MB. Wniosek z porównania przebiegów: podwójny przedrostek prawie nie
zmienił wyniku EmbeddingGemma 2, a brak tytułu obniżył wynik e5-small o 14,9 punktu procentowego na FAQ i o 13,4 na pytaniach potocznych.

## Ograniczenia

- Korpus to teksty jednego autora o spójnym słownictwie, bez skanów, e-maili i szumu typowego dla archiwów firmowych. Wynik
  nie przenosi się na dowolne dokumenty.
- Pytania FAQ napisał autor tekstów, więc dzielą z nimi słownictwo; część ma sens tylko w kontekście artykułu. Etykieta jest
  jedna na pytanie, a serwis ma teksty o zbliżonych tematach, więc trafienie w pokrewny tekst liczy się jako pudło.
- Różnice między systemami ocenia dokładny test dwumianowy na parach niezgodnych (pytanie trafione przez jeden
  system, a nie przez drugi), a przedziały ufności liczono metodą Wilsona na poziomie 95%. Pytania nie są próbą losową, a pytania z tego samego
  tekstu są od siebie zależne, więc p-wartości i przedziały opisują ten zbiór, nie populację pytań.
- Pytań potocznych jest 60; przy 58 trafieniach z 60 przedział sięga od 88,6% do 99,1%, a różnice między systemami na tym
  zestawie nie wykraczają poza tę niepewność.
- BM25 użyto w najprostszej postaci, bez odmiany polskich słów (stemmingu) i bez listy stopu; mocniejsza wersja leksykalna
  mogłaby wypaść lepiej, czego nie mierzono.
- Zmierzono tylko odnajdywanie tekstu, bez podziału na mniejsze fragmenty, bez rerankingu i bez generowania odpowiedzi.
- Format wejścia dobrano według kart modeli dopiero w przebiegu 2; przebieg 1 pokazuje, jak bardzo wynik mniejszego modelu
  zależy od obecności tytułu. Innych formatów (sam akapit dla EmbeddingGemma 2, tytuł bez separatora) nie mierzono.
- Czasy dotyczą tego komputera i tej wersji bibliotek; ładowanie w przebiegu 2 wydłużyło się przez uśpienie komputera.
- Zmierzono wyłącznie tekst. Enkodery obrazu i dźwięku były załadowane, ale nieużywane; zdolności wielomodalnych nie mierzono.

## Pliki

- `README.md`: ta metoda.
- `przygotuj_korpus.py`: buduje `korpus/fragmenty.json` i `korpus/pytania-faq.json` z plików Markdown serwisu.
- `pomiar.py`: przebieg 2; uruchamia cztery systemy i zapisuje `wyniki.json` oraz `wyniki-per-pytanie.jsonl`.
- `pomiar-przebieg1.py`: wersja skryptu z przebiegu 1 (z błędem wejścia opisanym wyżej).
- `pomiar-rankingi.py`: wersja z przebiegu 3; zapisuje `rankingi-pelne.jsonl` i sprawdza zgodność miar z `wyniki.json`.
- `rankingi-pelne.jsonl`: 1932 rekordy jak wyżej, dodatkowo pole `top10` z dziesięcioma pierwszymi tekstami i podobieństwem.
- `requirements.txt`: przypięte wersje pakietów Python.
- `wyniki.json`: przebieg 2; podsumowanie miar, czasów, pamięci, promptów, wersji bibliotek i sum kontrolnych korpusu.
- `wyniki-przebieg1.json`: to samo dla przebiegu 1.
- `wyniki-per-pytanie.jsonl`: przebieg 2; 1932 rekordy (483 pytania × 4 systemy): pytanie, trafne teksty, pięć pierwszych
  tekstów w rankingu, Hit@5, Recall@5, MRR@10.
- `korpus/fragmenty.json`: 1633 akapity z polami `id`, `doc`, `title`, `text`.
- `korpus/pytania-faq.json`: 423 pytania z polami `q`, `doc`, `a` (odpowiedź FAQ, nieużywana w pomiarze).
- `korpus/pytania-trudne.json`: 60 pytań potocznych z polami `q` i `doc` (lista trafnych tekstów).

## Jak powtórzyć

```
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
python przygotuj_korpus.py <katalog src/content> korpus   # albo użyj dołączonych plików korpusu
python pomiar.py                 # miary, czasy, wyniki-per-pytanie.jsonl
python pomiar-rankingi.py        # dodatkowo pełne rankingi; sprawdza zgodność miar z wyniki.json
```

Pierwsze uruchomienie pobiera wagi obu modeli z Hugging Face (około 2 GB). Na komputerze bez MPS skrypt użyje CPU i
będzie wolniejszy; miary jakości nie powinny się zmienić.
