Przejdź do treści
Praktyczne poradniki dla programistów
PORADNIK • PROGRAMISTA JAVA

Bazy wektorowe w aplikacjach Java: jak połączyć pgvector i Spring AI?

Rate this post

Gdy zwykłe wyszukiwanie po słowach przestaje wystarczać

Użytkownik pyta inaczej, niż napisano w dokumentacji

W bazie wiedzy znajduje się artykuł zatytułowany „Resetowanie hasła do konta”, a użytkownik wpisuje: „nie mogę odzyskać dostępu do konta”. Klasyczna wyszukiwarka oparta wyłącznie na dopasowaniu słów może uznać taki wynik za mało trafny, ponieważ fraza „odzyskać dostęp” nie zawiera słowa „hasło”. Użytkownik jednak oczekuje odpowiedzi właśnie z tej instrukcji.

Ten problem pojawia się często w dokumentacji technicznej, serwisach pomocy, regulaminach, katalogach produktów i systemach wewnętrznych. Autorzy treści posługują się jednym słownictwem, a odbiorcy drugim. Jedni piszą o „unieważnieniu tokenu”, inni pytają, „jak wylogować urządzenie”. Jedni opisują „uwierzytelnianie dwuskładnikowe”, inni szukają instrukcji pod hasłem „kod z telefonu nie działa”.

Wyszukiwanie semantyczne rozwiązuje tę rozbieżność częściowo, ponieważ porównuje nie tylko dokładne słowa, ale także znaczenie tekstu. Do tego celu potrzebuje embeddingów, czyli liczbowych reprezentacji tekstu. Zdanie, akapit lub fragment dokumentu zostaje zamieniony na wektor o ustalonej liczbie wymiarów. Podobne znaczeniowo treści trafiają bliżej siebie w przestrzeni wektorowej, nawet jeśli używają innych słów.

Od słów kluczowych do embeddingów

W klasycznym wyszukiwaniu pełnotekstowym zapytanie „reset hasła” będzie premiować dokumenty, które zawierają dokładnie te słowa lub ich odmiany. W wyszukiwaniu wektorowym aplikacja generuje embedding zapytania, a następnie szuka dokumentów o embeddingach najbardziej zbliżonych do tego wektora. Wynikiem nie musi być identyczne sformułowanie, lecz treść podobna intencją.

Nie oznacza to, że wyszukiwanie leksykalne przestaje być użyteczne. PostgreSQL oferuje bardzo dobre mechanizmy full-text search, a frazy dokładne są ważne między innymi dla kodów błędów, numerów faktur, identyfikatorów produktów, nazw klas Java czy komend CLI. W wielu systemach najlepsze rezultaty zapewnia podejście hybrydowe: najpierw ograniczenie wyników po filtrach lub słowach obowiązkowych, a następnie sortowanie według podobieństwa wektorowego.

Embedding nie jest odpowiedzią generowaną przez model językowy. To dane pomocnicze, dzięki którym aplikacja może odnaleźć właściwy kontekst. Dopiero potem model czatowy może użyć znalezionych fragmentów, aby sformułować odpowiedź w mechanizmie Retrieval-Augmented Generation, czyli RAG.

Dlaczego pgvector jest rozsądnym wyborem dla aplikacji Java

pgvector to rozszerzenie PostgreSQL umożliwiające przechowywanie wektorów oraz wykonywanie zapytań opartych na odległości i podobieństwie. Dla zespołu, który już korzysta z PostgreSQL, jest to często prostsza droga niż wdrażanie osobnej bazy wektorowej, osobnego klastra, kolejnego mechanizmu kopii zapasowych i nowego modelu uprawnień.

Takie rozwiązanie pozwala przechowywać tekst źródłowy, metadane, relacje biznesowe i embeddingi w tym samym ekosystemie. Można korzystać z transakcji, znanych narzędzi do migracji schematu, monitoringu PostgreSQL oraz polityk dostępu już stosowanych w organizacji. Nie każda aplikacja potrzebuje wyspecjalizowanego silnika wektorowego, zwłaszcza gdy indeks obejmuje dokumentację, katalog produktów albo bazę procedur. [4]

Mini-wniosek: baza wektorowa nie zastępuje modelu językowego ani dobrze przygotowanych danych. Jej zadaniem jest szybkie dostarczenie trafnego kontekstu, na podstawie którego aplikacja może zwrócić listę dokumentów albo zbudować odpowiedź RAG.

Krok 1. Ustal przypadek użycia i granice rozwiązania

Kiedy połączenie pgvector i Spring AI ma sens

Połączenie PostgreSQL, pgvector i Spring AI jest szczególnie praktyczne wtedy, gdy aplikacja Java potrzebuje wyszukiwać podobne treści w zbiorze dokumentów i jednocześnie korzysta już z ekosystemu Spring Boot. Typowe źródła danych to dokumentacja techniczna, artykuły pomocy, instrukcje operacyjne, regulaminy, opisy produktów, repozytoria wiedzy wewnętrznej oraz materiały publikowane przez CMS.

Dobrze sprawdzają się dane, których treść jest względnie stabilna i można ją indeksować partiami lub po zdarzeniu aktualizacji. Przykładowo: po publikacji nowej wersji instrukcji system może przetworzyć dokument, podzielić go na fragmenty, wygenerować embeddingi i zapisać je w PostgreSQL. Następnie użytkownicy mogą zadawać pytania naturalnym językiem bez ręcznego budowania wielu wariantów słów kluczowych.

Rozwiązanie wymaga ostrożności przy danych bardzo dynamicznych. Jeżeli odpowiedź ma zależeć od aktualnego stanu zamówienia, salda, dostępności magazynowej lub bieżących parametrów klienta, embedding nie może być jedynym źródłem prawdy. Model powinien otrzymać aktualne dane z API lub bazy transakcyjnej, a wektory mogą co najwyżej pomóc odnaleźć właściwą instrukcję albo klasyfikację problemu.

RAG, wyszukiwarka semantyczna czy rekomendacje

Przed konfiguracją pierwszego indeksu trzeba zdecydować, jaki wynik ma otrzymać użytkownik. Pod hasłem „baza wektorowa Spring AI” mogą kryć się trzy różne produkty, które wymagają innego sposobu oceny jakości.

Przypadek użyciaRezultat dla użytkownikaNajważniejsze kryterium
Wyszukiwanie semantyczneLista najlepiej dopasowanych dokumentów lub fragmentówTrafność wyników i możliwość przejścia do źródła
RAG w Spring BootOdpowiedź tekstowa zbudowana na podstawie znalezionego kontekstuRzetelność odpowiedzi, cytowanie źródeł i brak halucynacji
Rekomendacje podobnych treściPowiązane artykuły, produkty, procedury lub zgłoszeniaUżyteczność podobieństwa dla konkretnej czynności

Wyszukiwarka semantyczna jest zwykle najprostszym pierwszym etapem. Aplikacja zwraca kilka fragmentów oraz linki do źródeł, a człowiek sam wybiera właściwy materiał. RAG jest wygodniejszy dla użytkownika, ale podnosi wymagania: trzeba ograniczyć model do dostarczonego kontekstu, jasno komunikować brak danych i dołączać identyfikatory źródeł.

Rekomendacje mogą wykorzystywać podobny mechanizm wektorowy, lecz nie muszą angażować modelu czatowego. Po otwarciu procedury „Konfiguracja SSO” aplikacja może znaleźć treści o podobnym zakresie, takie jak „Mapowanie ról” czy „Obsługa certyfikatów”. To nadal podobieństwo wektorowe, ale bez generowania odpowiedzi.

Kryteria sukcesu ustalone przed kodowaniem

Bez konkretnych kryteriów łatwo uznać wdrożenie za udane tylko dlatego, że aplikacja zwraca jakiekolwiek wyniki. Lepiej przygotować zestaw realnych pytań użytkowników i dla każdego wskazać oczekiwane źródło. Wystarczy kilkadziesiąt pytań, o ile obejmują różne działy, języki, typy dokumentów oraz zapytania sformułowane potocznie.

  • Jak często właściwy dokument pojawia się w pierwszych kilku wynikach?
  • Czy filtr po produkcie, organizacji, języku lub wersji dokumentacji działa przed wyszukiwaniem?
  • Czy użytkownik może sprawdzić źródło odpowiedzi?
  • Czy wyszukiwarka nie ujawnia materiałów, do których użytkownik nie ma uprawnień?
  • Czy czas odpowiedzi pozostaje akceptowalny przy docelowej liczbie dokumentów?
  • Jak system zachowa się, gdy nie znajdzie pewnego kontekstu?

Mini-wniosek: najpierw trzeba określić pytania użytkowników, źródła prawdy i zasady dostępu. Dopiero później sensownie wybiera się model embeddingowy, typ indeksu oraz liczbę zwracanych fragmentów.

Abstrakcyjna wizualizacja 3D połączeń sieci neuronowej w cyfrowej przestrzeni
Źródło: Pexels | Autor: Google DeepMind

Krok 2. Przygotuj PostgreSQL z rozszerzeniem pgvector

Wymagania środowiskowe i aktywacja rozszerzenia

PostgreSQL musi działać w wersji i środowisku, które obsługują rozszerzenie pgvector. Lokalnie wygodnym rozwiązaniem jest obraz kontenera zawierający PostgreSQL z zainstalowanym pgvector. W środowisku zarządzanym należy wcześniej sprawdzić, czy dostawca pozwala wykonać polecenie CREATE EXTENSION, jaką wersję pgvector udostępnia i czy wspiera wybrany typ indeksu.

CREATE EXTENSION IF NOT EXISTS vector;

Polecenie zwykle wykonuje właściciel bazy albo użytkownik mający odpowiednie uprawnienia administracyjne. Konto używane przez aplikację nie musi mieć prawa tworzenia rozszerzeń, ale powinno mieć dostęp do tabel, sekwencji i indeksów wymaganych przez mechanizm zapisu. Nie należy zakładać, że lokalna konfiguracja Dockera będzie identyczna z konfiguracją produkcyjnej usługi PostgreSQL.

Stan rozszerzeń można sprawdzić zapytaniem:

SELECT extname, extversion
FROM pg_extension
WHERE extname = 'vector';

Jeżeli aplikacja ma automatycznie inicjalizować tabelę dla PgVectorStore, konto bazodanowe musi również mieć uprawnienia do tworzenia obiektów w odpowiednim schemacie. W systemach produkcyjnych częściej stosuje się migracje Flyway lub Liquibase, dzięki czemu zmiany schematu są kontrolowane, powtarzalne i widoczne w historii wdrożeń.

Wymiar wektorów, tekst źródłowy i metadane

Kolumna typu vector powinna mieć wymiar zgodny z embeddingami zwracanymi przez wybrany model. Jeżeli model generuje wektory o 1536 wymiarach, definicja vector(1536) jest poprawna. Próba zapisania wektora o innej długości zakończy się błędem albo ujawni niespójność konfiguracji.

CREATE TABLE knowledge_chunk (
    id uuid PRIMARY KEY,
    source_id text NOT NULL,
    content text NOT NULL,
    metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
    embedding vector(1536) NOT NULL,
    created_at timestamptz NOT NULL DEFAULT now()
);

Sam embedding nie wystarcza do obsługi aplikacji. W tabeli albo w strukturze zarządzanej przez Spring AI powinny znaleźć się co najmniej: treść fragmentu, identyfikator dokumentu źródłowego, adres URL lub ścieżka, wersja dokumentu, data indeksowania i metadane. Metadane mogą zawierać między innymi nazwę produktu, język, dział, identyfikator organizacji, poziom dostępu oraz status publikacji.

To właśnie metadane pozwalają uniknąć sytuacji, w której użytkownik szukający instrukcji dla produktu A otrzymuje fragment dokumentacji produktu B. Są także podstawą do bezpiecznego ograniczania wyników w systemie wielodzierżawnym. Wektor określa podobieństwo, ale nie zastępuje filtra autoryzacyjnego.

Cosine distance, L2 i inner product

pgvector obsługuje kilka miar odległości. Najczęściej spotykane są odległość cosinusowa, odległość euklidesowa L2 oraz iloczyn wewnętrzny. Wyboru nie należy dokonywać przypadkowo, ponieważ model embeddingowy, zapytanie SQL i indeks muszą wykorzystywać zgodną strategię. [3]

MiaraTypowe zastosowanieUwagi implementacyjne
Cosine distanceWyszukiwanie semantyczne tekstówCzęsty wybór dla embeddingów tekstowych; wynik mniejszy oznacza bliższe dopasowanie
L2 distanceModele, dla których odległość euklidesowa jest zalecanaWymaga zgodności operatora i indeksu
Inner productModele optymalizowane pod iloczyn skalarnyMoże wymagać odpowiedniej normalizacji wektorów

Typowy problem pojawia się dopiero przy pierwszym większym indeksie: zapytanie działa, ale po zmianie operatora wyniki nagle wyglądają na przypadkowe. Przyczyną bywa indeks utworzony dla jednej miary, podczas gdy aplikacja sortuje wyniki według innej. Taka niezgodność może też sprawić, że PostgreSQL nie wykorzysta indeksu i przejdzie do kosztownego skanowania tabeli.

Dla odległości cosinusowej w pgvector stosuje się operator <=>. Zapytanie może wyglądać następująco:

SELECT id, source_id, content, metadata,
       embedding <=> :queryEmbedding AS distance
FROM knowledge_chunk
WHERE metadata ->> 'product' = :product
  AND metadata ->> 'language' = :language
ORDER BY embedding <=> :queryEmbedding
LIMIT 5;

Indeks musi odpowiadać temu samemu operatorowi. Przykładowo, indeks HNSW dla odległości cosinusowej można utworzyć tak:

CREATE INDEX knowledge_chunk_embedding_hnsw_idx
ON knowledge_chunk
USING hnsw (embedding vector_cosine_ops);

Alternatywą jest indeks IVFFlat, który wymaga odpowiedniego przygotowania danych i strojenia liczby list. HNSW często zapewnia dobrą jakość wyników przy wyszukiwaniu przybliżonym, ale zużywa więcej pamięci i może wolniej budować się podczas zapisu. IVFFlat bywa rozsądnym wyborem dla dużych, względnie stabilnych zbiorów, jednak przed utworzeniem indeksu tabela powinna zawierać reprezentatywną liczbę wektorów. W obu przypadkach decyzję najlepiej potwierdzić planem wykonania EXPLAIN ANALYZE oraz rzeczywistym zestawem pytań testowych.

Filtry po metadanych wymagają osobnego podejścia. Indeks wektorowy przyspiesza kolejność według podobieństwa, ale nie zastępuje indeksów dla tenant_id, statusu publikacji czy języka. Jeżeli niemal każde zapytanie ogranicza wyniki do jednej organizacji, pomocny może być zwykły indeks B-tree na odpowiedniej kolumnie albo świadomie zaprojektowana struktura metadanych. Mini-wniosek: skuteczne wyszukiwanie powstaje z połączenia podobieństwa wektorowego, filtrów biznesowych i zgodnych indeksów, a nie z samej kolumny vector.

Dobrze skonfigurowany pgvector i Spring AI pozwalają traktować PostgreSQL jako praktyczny fundament wyszukiwania semantycznego: dane pozostają blisko aplikacji, metadane kontrolują zakres wyników, a odpowiedź modelu może być oparta na konkretnych fragmentach źródłowych zamiast na domysłach.

Krok 3. Skonfiguruj Spring Boot, Spring AI i PgVectorStore

Po przygotowaniu PostgreSQL aplikacja Java potrzebuje trzech elementów: połączenia JDBC, modelu generującego embeddingi oraz implementacji VectorStore, która zapisze dokumenty i wykona wyszukiwanie. W Spring AI tę rolę pełni PgVectorStore. [2]

Dodaj zależności Maven

W projekcie Maven warto użyć BOM-a Spring AI, aby wersje starterów były ze sobą zgodne. Poniższa konfiguracja zakłada Spring Boot, PostgreSQL, Spring AI oraz model OpenAI używany do generowania embeddingów.

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
    </dependency>

    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

Jeżeli embeddingi mają pochodzić od innego dostawcy, należy zastąpić starter modelu odpowiednim modułem Spring AI. Najważniejsze jest to, aby EmbeddingModel zwracał wektory zgodne z wymiarem zapisanym w konfiguracji PgVectorStore i schemacie bazy.

Skonfiguruj połączenie z PostgreSQL

Podstawowa konfiguracja może znaleźć się w pliku application.yml. Dane dostępowe powinny pochodzić ze zmiennych środowiskowych, systemu zarządzania sekretami albo mechanizmu konfiguracji używanego w organizacji, a nie z repozytorium kodu.

Pomarańczowa abstrakcyjna sieć linii i punktów na ciemnym tle
Źródło: Pexels | Autor: U.Lucas Dubé-Cantin
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/knowledge_base
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}

  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      embedding:
        options:
          model: text-embedding-3-small

    vectorstore:
      pgvector:
        initialize-schema: false
        schema-name: public
        table-name: vector_store
        dimensions: 1536
        distance-type: COSINE_DISTANCE
        index-type: HNSW

Parametr initialize-schema jest wygodny lokalnie, ponieważ Spring AI może utworzyć wymagane obiekty automatycznie. W środowisku produkcyjnym bezpieczniej ustawić wartość false i przygotować tabelę oraz indeksy przez migracje Flyway lub Liquibase. Dzięki temu struktura bazy nie zmieni się nieoczekiwanie podczas uruchamiania nowej wersji aplikacji. [1]

Nazwy właściwości mogą nieznacznie różnić się zależnie od wersji Spring AI. Przed wdrożeniem warto sprawdzić dokumentację dokładnie tej wersji biblioteki, która znajduje się w pliku pom.xml, a nie przykłady z przypadkowego wpisu blogowego.

Sprawdź, czy Spring utworzył VectorStore

Po poprawnej autokonfiguracji można wstrzyknąć interfejs VectorStore do serwisu. Aplikacja nie musi wtedy wykonywać ręcznych zapytań SQL przy każdym wyszukiwaniu, choć SQL nadal przydaje się do diagnostyki, migracji i strojenia bazy.

package pl.example.knowledge;

import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;

@Service
public class KnowledgeSearchService {

    private final VectorStore vectorStore;

    public KnowledgeSearchService(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }
}

Najprostszy test uruchomieniowy polega na sprawdzeniu logów aplikacji i wykonaniu kontrolnego zapisu dokumentu. Jeżeli bean VectorStore nie powstaje, najczęstsze przyczyny są następujące:

  • brak sterownika PostgreSQL lub niepoprawny adres JDBC,
  • brak rozszerzenia vector w docelowej bazie,
  • niezgodny wymiar embeddingów i kolumny wektorowej,
  • brak klucza API albo błędna konfiguracja dostawcy embeddingów,
  • konto aplikacyjne nie ma praw do tabeli, schematu lub sekwencji,
  • konflikt wersji Spring Boot, Spring AI i użytych starterów.

Kontrolny zapis i odczyt

Na początku nie podłączaj od razu całego repozytorium dokumentów. Zapisz jeden krótki dokument, a następnie wykonaj wyszukiwanie. Taki test szybko potwierdza, że aplikacja może wygenerować embedding, zapisać go w PostgreSQL i pobrać wynik.

package pl.example.knowledge;

import java.util.List;
import java.util.Map;

import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Component;

@Component
public class VectorStoreSmokeTest {

    private final VectorStore vectorStore;

    public VectorStoreSmokeTest(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    public void run() {
        Document document = new Document(
                "Aby zresetować hasło, wybierz opcję „Nie pamiętam hasła” na ekranie logowania.",
                Map.of(
                        "sourceId", "password-reset-guide",
                        "product", "portal",
                        "language", "pl",
                        "published", true
                )
        );

        vectorStore.add(List.of(document));

        List<Document> results = vectorStore.similaritySearch(
                SearchRequest.query("nie mogę odzyskać dostępu do konta")
                        .withTopK(3)
        );

        results.forEach(result -> System.out.println(result.getText()));
    }
}

Po tym teście sprawdź bazę bezpośrednio. Powinien pojawić się zapis zawierający tekst dokumentu, jego metadane i wektor. Warto również upewnić się, że tabela używana przez Spring AI jest tą samą tabelą, którą obejmują migracje oraz polityki dostępu.

Ostrzeżenie: nie uruchamiaj takiego testu przy każdym starcie aplikacji produkcyjnej. Każdy zapis może tworzyć kolejny fragment, a ponowne indeksowanie identycznego materiału prowadzi do duplikatów i pogarsza jakość wyników.

Krok 4. Zbuduj pipeline indeksowania dokumentów

Właściwe indeksowanie to powtarzalny proces: pobranie źródła, oczyszczenie treści, podział na fragmenty, przypisanie metadanych, zapis do VectorStore i kontrola wyniku. Ten proces powinien być niezależny od obsługi pojedynczego zapytania użytkownika.

Ustal kolejność przetwarzania

  1. Pobierz dokument ze źródła, na przykład z pliku, CMS-a, systemu ticketowego lub repozytorium Git.
  2. Odrzuć materiały nieopublikowane, archiwalne albo niedostępne dla danej grupy użytkowników.
  3. Wydobądź tekst i usuń elementy nieprzydatne w wyszukiwaniu, takie jak menu, stopki, powielone nagłówki nawigacyjne i techniczne znaczniki.
  4. Podziel materiał na fragmenty zachowujące sens: sekcje procedury, akapity z nagłówkiem albo krótkie bloki instrukcji.
  5. Dodaj metadane źródłowe i uprawnieniowe do każdego fragmentu.
  6. Usuń wcześniejszą wersję fragmentów danego dokumentu.
  7. Zapisz nowe fragmenty w PgVectorStore.
  8. Zapisz status indeksowania, liczbę fragmentów, wersję źródła i czas wykonania.

Najważniejsza zasada brzmi: dokument należy aktualizować jako całość, a nie tylko dopisywać kolejne fragmenty. Jeśli artykuł zmienił treść, stare fragmenty muszą zniknąć z indeksu. W przeciwnym razie wyszukiwarka może zwracać nieaktualną procedurę obok nowej wersji.

Podziel dokument na fragmenty o praktycznej wielkości

Fragment nie powinien być ani całym wielostronicowym regulaminem, ani pojedynczym zdaniem pozbawionym kontekstu. Dobrą jednostką jest blok zawierający jedną procedurę, jeden wariant konfiguracji albo jedną odpowiedź na konkretne pytanie.

Spring AI udostępnia mechanizmy dzielenia dokumentów, między innymi TokenTextSplitter. W prostym pipeline można najpierw utworzyć dokument źródłowy, a później podzielić go na mniejsze jednostki.

package pl.example.knowledge;

import java.util.List;
import java.util.Map;

import org.springframework.ai.document.Document;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;

@Service
public class KnowledgeIndexer {

    private final VectorStore vectorStore;
    private final TokenTextSplitter textSplitter = new TokenTextSplitter();

    public KnowledgeIndexer(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    public void index(String sourceId, String content, String sourceUrl) {
        Document sourceDocument = new Document(
                content,
                Map.of(
                        "sourceId", sourceId,
                        "sourceUrl", sourceUrl,
                        "product", "portal",
                        "language", "pl",
                        "published", true,
                        "documentVersion", "2025-03"
                )
        );

        List<Document> chunks = textSplitter.apply(List.of(sourceDocument));

        vectorStore.add(chunks);
    }
}

Przed użyciem splittera na pełnej bazie wiedzy sprawdź ręcznie kilkanaście wynikowych fragmentów. Każdy z nich powinien zachowywać nazwę funkcji, warunki wstępne, numer kroku lub inne dane potrzebne do zrozumienia instrukcji. Jeśli fragment zaczyna się od „następnie kliknij przycisk”, ale nie wiadomo, w którym ekranie, podział jest zbyt agresywny.

Dodaj metadane do każdego fragmentu

Metadane muszą być kompletne już w chwili indeksowania. Nie zakładaj, że da się później odtworzyć uprawnienia lub wersję dokumentu wyłącznie na podstawie tekstu fragmentu.

{
  "sourceId": "sso-configuration",
  "sourceUrl": "https://docs.example.pl/security/sso",
  "title": "Konfiguracja SSO",
  "product": "portal",
  "language": "pl",
  "tenantId": "acme",
  "accessLevel": "admin",
  "published": true,
  "documentVersion": "3.4",
  "indexedAt": "2025-03-08T10:15:00Z"
}

W praktyce warto rozdzielić dwa identyfikatory:

  • sourceId — stabilny identyfikator dokumentu źródłowego, używany podczas aktualizacji i usuwania,
  • chunkId — identyfikator konkretnego fragmentu, przydatny do diagnostyki, cytowania i śledzenia jakości wyników.

Jeżeli dokument jest dostępny tylko dla administratorów organizacji Acme, każdy jego fragment musi otrzymać takie samo ograniczenie. Nie wystarczy oznaczyć wyłącznie rekordu dokumentu w zewnętrznym systemie CMS.

Obsłuż aktualizację i usuwanie źródeł

Pipeline powinien reagować na trzy zdarzenia: publikację nowego dokumentu, zmianę istniejącego dokumentu oraz usunięcie lub wycofanie publikacji. Najbezpieczniejszy wariant to usunięcie fragmentów danego sourceId, a następnie zapisanie pełnej aktualnej wersji.

Pytania od czytelników

Pytanie czytelnika

Czy pgvector zastępuje klasyczne wyszukiwanie pełnotekstowe?

Odpowiedź redakcji: Nie. Wyszukiwanie pełnotekstowe nadal dobrze sprawdza się przy kodach błędów, identyfikatorach i nazwach klas. W wielu aplikacjach warto połączyć je z wyszukiwaniem wektorowym.
Pytanie czytelnika

Czy embedding może być jedynym źródłem aktualnych danych o zamówieniu?

Odpowiedź redakcji: Nie. Przy dynamicznych danych, takich jak status zamówienia, saldo czy dostępność, aplikacja powinna pobierać aktualne informacje z API lub bazy transakcyjnej.
Pytanie czytelnika

Co trzeba sprawdzić przed wyborem wymiaru wektora?

Odpowiedź redakcji: Wymiar kolumny typu vector musi odpowiadać długości wektorów generowanych przez wybrany model embeddingowy. Niezgodność konfiguracji spowoduje problem przy zapisie.
Pytanie czytelnika

Czy aplikacja powinna automatycznie tworzyć rozszerzenie pgvector na produkcji?

Odpowiedź redakcji: Niekoniecznie. W środowiskach produkcyjnych rozszerzenie i zmiany schematu często kontroluje się za pomocą migracji Flyway lub Liquibase oraz odpowiednio ograniczonych uprawnień.

Źródła

Tematy:Trendy w programowaniu