Strona główna / Artykuły / Notatki praktyczne: Bazy danych wektorowych do zastosowań produkcyjnych RAG: indeksowanie, wyszukiwanie hybrydowe

Notatki praktyczne: Bazy danych wektorowych do zastosowań produkcyjnych RAG: indeksowanie, wyszukiwanie hybrydowe

Szczegółowy przewodnik po Notatkach praktycznych: Bazy danych wektorowych do zastosowań produkcyjnych RAG: indeksowanie, wyszukiwanie hybrydowe: umowy, sprawdzania oraz gotowe elementy kodu dla zespołów wdrażających ten wzorzec.

7876 słów

Poniższe notatki przedstawiają praktyczny plan pracy nad tematem „Bazy danych wektorowych do zastosowań produkcyjnych RAG: indeksowanie, hybrydowe wyszukiwanie i skalowanie procesu odzyskiwania danych”. Nacisk kładziony jest na specyfikacje, sprawdzanie oraz miejsca na kod do wstawienia, a nie na motywacyjne aspekty. Podczas przechodzenia przez etap przeglądu, najpierw zapisz specyfikacje: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Trzymaj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny haseł oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury.

Baza danych wektorowych to algorytm wyszukiwania

Baza danych wektorowych funkcjonuje najlepiej, gdy traktowana jest jako mierzalna powierzchnia. Zanim rozszerzysz zakres, zapisz jeden idealny przypadek działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian. Zdokumentuj zarówno prawidłowy przebieg operacji, jak i ścieżkę przywracania stanu. Próby ponownych działań, kontrola przez ludzi oraz obsługa wiadomości błędowych stanowią część produktu, a nie elementy dodawane później w celu udoskonalenia. Oddziel zasadę dzielenia na fragmenty od zasady wyszukiwania. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości.

Dokładne wyszukiwanie najbliższego sąsiada: podstawa, której nie można stosować na dużą skalę

Etap wyszukiwania dokładnego najbliższego sąsiada działa najlepiej, gdy traktuje się go jako mierzalną powierzchnię. Zapisz jeden idealny przypadek, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań, zanim rozszerzysz zakres. Wolno preferować małe, testowalne jednostki zamiast rozbudowanych skryptów. Gdy jakiś krok zawiedzie, awaria powinna wskazywać na konkretną odpowiedzialność, a nie na skomplikowany łańcuch operacji. Oddziel zasadę dzielenia na fragmenty od zasady wyszukiwania. Zmiana jednej nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości.

import faiss
import numpy as np
from typing import Tuple


def build_exact_index(
    embeddings: np.ndarray,
    use_cosine: bool = True
) -> faiss.IndexFlatIP:
    """
    Build a FAISS flat index for exact nearest neighbour search.

    embeddings: (N, D) float32 array.
    use_cosine: If True, normalises a copy of the embeddings and uses inner
                product (equivalent to cosine similarity). The caller's array
                is not mutated.

    Returns a FAISS flat index. Benchmark latency against your corpus and
    latency SLO before deciding whether ANN indexing is necessary.
    """
    dimension = embeddings.shape[1]

    if use_cosine:
        # Copy before normalising to avoid mutating the caller's array.
        embeddings_copy = embeddings.astype(np.float32).copy()
        faiss.normalize_L2(embeddings_copy)
        index = faiss.IndexFlatIP(dimension)
        index.add(embeddings_copy)
        return index
    else:
        index = faiss.IndexFlatL2(dimension)
        index.add(embeddings.astype(np.float32).copy())
        return index


def search_exact(
    index: faiss.IndexFlatIP,
    query_vector: np.ndarray,
    top_k: int = 10
) -> Tuple[np.ndarray, np.ndarray]:
    """
    Search the flat index. Returns (distances, indices).
    query_vector must already be normalised if the index was built with
    normalised embeddings.
    """
    query = query_vector.reshape(1, -1).astype(np.float32)
    faiss.normalize_L2(query)
    distances, indices = index.search(query, top_k)
    return distances[0], indices[0]

HNSW: Dlaczego jest tak powszechne w produkcyjnych systemach wyszukiwania wektorowego

Faza HNSW Why It Is funkcjonuje najlepiej, gdy jest traktowana jako mierzalna powierzchnia. Zapisz jeden idealny przepis działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań, zanim rozszerzysz zakres. Traktuj tę fazę jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzucaj ciche, częściowe ukończenie zadań. Oddziel politykę dzielenia na fragmenty od polityki wyszukiwania. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości. Faza HNSW Why It Is funkcjonuje najlepiej, gdy jest traktowana jako mierzalna powierzchnia. Zapisz jeden idealny przepis działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań, zanim rozszerzysz zakres. Przechowuj konfigurację poza kodem aplikacji. Pliki środowiskowe, składysek z danymi poufnymi oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą audytować bez konieczności czytania całego grafu.

Jak budowany jest graf

W etapie „Jak wygląda graf” należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy udokumentować zarówno prawidłowy przebieg procesu, jak i ścieżkę naprawczą. Próby ponownych działań, kontrola przez ludzi oraz obsługa wiadomości błędnych stanowią część produktu, a nie elementy dopiero późniejszej optymalizacji. Należy podać fragmenty tekstu, które faktycznie stanowią podstawę odpowiedzi. Bez tych odniesień operatorzy nie będą w stanie odróżnić halucynacji od luki w indeksowaniu.

Parametry określające kompromis między dokładnością a opóźnieniem

Aby określić parametry etapu, należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić dany krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy preferować małe, testowalne jednostki zamiast rozbudowanych skryptów. Gdy dany krok zawiedzie, powód awarii powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowaną strukturę procesu. Należy podawać fragmenty tekstu, które faktycznie stanowią podstawę odpowiedzi. Bez tych odniesień operatorzy nie będą w stanie odróżnić fałszywych informacji od braków w indeksowaniu.

import faiss
import numpy as np
from typing import Tuple


def build_hnsw_index(
    embeddings: np.ndarray,
    m: int = 32,
    ef_construction: int = 200,
    ef_search: int = 100,
    use_cosine: bool = True
) -> faiss.IndexHNSWFlat:
    """
    Build a FAISS HNSW index for approximate nearest neighbour search.

    m: Graph connectivity parameter. Higher = better recall potential, more memory.
       Starting range for banking policy corpora: 16 to 32. Benchmark your corpus.
    ef_construction: Candidates explored during index build. Higher = better graph quality.
       One-time cost at index build; does not affect query latency.
    ef_search: Candidates explored at query time. Controls recall-latency trade-off.
       Can be changed without rebuilding. Starting range: 50 to 200.
    use_cosine: Normalise embeddings and use inner product (cosine similarity).

    Note: FAISS HNSW does not support GPU acceleration. For GPU-accelerated ANN,
    use IndexIVFPQ variants.
    """
    dimension = embeddings.shape[1]

    # Copy before normalising to avoid mutating the caller's array.
    embeddings_to_index = embeddings.astype(np.float32).copy()

    if use_cosine:
        faiss.normalize_L2(embeddings_to_index)
        index = faiss.IndexHNSWFlat(dimension, m, faiss.METRIC_INNER_PRODUCT)
    else:
        index = faiss.IndexHNSWFlat(dimension, m, faiss.METRIC_L2)

    index.hnsw.efConstruction = ef_construction
    index.hnsw.efSearch = ef_search
    index.add(embeddings_to_index)
    return index


def search_hnsw(
    index: faiss.IndexHNSWFlat,
    query_vector: np.ndarray,
    top_k: int = 10
) -> Tuple[np.ndarray, np.ndarray]:
    """
    Search the HNSW index. Returns (scores, indices).
    query_vector must be normalised if the index was built with normalised embeddings.
    """
    query = query_vector.reshape(1, -1).astype(np.float32)
    faiss.normalize_L2(query)
    scores, indices = index.search(query, top_k)
    return scores[0], indices[0]

Wymagania pamięciowe HNSW

W etapie wymagań pamięciowych HNSW należy zdefiniować dane wejściowe, osobę odpowiedzialną za ten krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nazwij wszystkie elementy, zdefiniuj kryteria sukcesu i odrzuć ciche, częściowe ukończenie zadania. Podaj fragmenty tekstu, które faktycznie stanowią podstawę odpowiedzi. Bez tych odniesień operatorzy nie będą w stanie odróżnić halucynacji od luki w indeksowaniu. W etapie wymagań pamięciowych HNSW należy zdefiniować dane wejściowe, osobę odpowiedzialną za ten krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Przechowuj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury.

h.

IVF: Odwrotny indeks plików dla implementacji o ograniczonych zasobach pamięci

Podczas prace nad etapem odwrotnego indeksu plików w IVF najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Zdokumentuj zarówno ścieżkę prawidłowego działania, jak i ścieżkę naprawczą. Próby ponowne, kontrola przez ludzi oraz obsługa wiadomości nieodebranych stanowią część produktu, a nie elementy dopiero późniejszej optymalizacji. Zmierz stopień odzyskiwania informacji na ustalonej grupie pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko naprawiają słabe możliwości wyszukiwania.

import faiss
import numpy as np
from typing import Tuple


def build_ivf_index(
    embeddings: np.ndarray,
    nlist: int = 1024,
    nprobe: int = 64,
    use_cosine: bool = True
) -> faiss.IndexIVFFlat:
    """
    Build a FAISS IVF flat index.

    nlist: Number of Voronoi cells. A common starting heuristic is sqrt(N),
           where N is corpus size. For 100K vectors: 300-1000. For 1M: 1024-4096.
           Validate empirically.
    nprobe: Number of cells searched at query time. Higher = better recall, slower.
            Set based on your recall benchmark results.
    use_cosine: Use inner product on normalised vectors.

    Requires training on a representative sample before adding vectors.
    """
    dimension = embeddings.shape[1]
    embeddings_to_index = embeddings.astype(np.float32).copy()

    if use_cosine:
        faiss.normalize_L2(embeddings_to_index)
        quantiser = faiss.IndexFlatIP(dimension)
        index = faiss.IndexIVFFlat(quantiser, dimension, nlist, faiss.METRIC_INNER_PRODUCT)
    else:
        quantiser = faiss.IndexFlatL2(dimension)
        index = faiss.IndexIVFFlat(quantiser, dimension, nlist)

    # Use a random representative sample for training. Using the first N records
    # risks training on a non-representative slice if the corpus is ordered by
    # date, jurisdiction, or document type.
    n_available = len(embeddings_to_index)
    desired_training_size = min(n_available, 40 * nlist)
    rng = np.random.default_rng(seed=42)
    training_indices = rng.choice(n_available, size=desired_training_size, replace=False)
    training_sample = embeddings_to_index[training_indices]
    index.train(training_sample)

    index.nprobe = nprobe
    index.add(embeddings_to_index)
    return index

IVF z kwantyzacją produktu

Gdy przechodzisz przez etap kwantyzacji produktów w ramach technologii IVF, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Wolno preferować małe, testowalne jednostki zamiast rozbudowanych skryptów. Gdy jakiś krok się nie powiedzie, błąd powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowany łańcuch operacji. Zmierz stopień odzyskiwania informacji na ustalonej grupie pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko naprawiają słabe możliwości wyszukiwania.

import faiss
import numpy as np
from typing import Tuple


def build_ivfpq_index(
    embeddings: np.ndarray,
    nlist: int = 1024,
    m_subvectors: int = 8,
    bits_per_code: int = 8,
    nprobe: int = 64
) -> Tuple[faiss.IndexIVFPQ, faiss.IndexFlatIP]:
    """
    Build a FAISS IVF-PQ index paired with a flat index for exact re-scoring.

    m_subvectors: Number of sub-vectors. Must divide dimension evenly.
                  For 1536 dimensions: m=8 (192 dims each), m=16 (96 dims each).
                  Select based on the storage-recall trade-off for your corpus.
    bits_per_code: Bits per sub-vector code. 8 bits = 256 centroids per sub-vector.
                   Lower bits = smaller code, larger recall degradation.

    Returns (pq_index, flat_index).
    Use pq_index to retrieve top-N candidates cheaply; use flat_index to re-score
    those candidates with full float32 precision.
    """
    dimension = embeddings.shape[1]
    assert dimension % m_subvectors == 0, (
        f"Dimension {dimension} must be divisible by m_subvectors {m_subvectors}"
    )

    norm_embeddings = embeddings.astype(np.float32).copy()
    faiss.normalize_L2(norm_embeddings)

    # Compressed IVF-PQ index for broad retrieval
    quantiser = faiss.IndexFlatIP(dimension)
    pq_index = faiss.IndexIVFPQ(
        quantiser, dimension, nlist, m_subvectors, bits_per_code,
        faiss.METRIC_INNER_PRODUCT
    )
    training_size = min(len(norm_embeddings), 50 * nlist)
    rng = np.random.default_rng(seed=42)
    training_indices = rng.choice(len(norm_embeddings), size=training_size, replace=False)
    pq_index.train(norm_embeddings[training_indices])
    pq_index.nprobe = nprobe
    pq_index.add(norm_embeddings)

    # Flat index for exact re-scoring of PQ candidates
    flat_index = faiss.IndexFlatIP(dimension)
    flat_index.add(norm_embeddings)

    return pq_index, flat_index


def two_stage_search(
    pq_index: faiss.IndexIVFPQ,
    flat_index: faiss.IndexFlatIP,
    query_vector: np.ndarray,
    top_k: int = 10,
    candidate_multiplier: int = 10
) -> Tuple[np.ndarray, np.ndarray]:
    """
    Two-stage retrieval: broad PQ candidate recall followed by exact flat re-scoring.

    Stage 1: IVF-PQ retrieves top_k * candidate_multiplier candidates cheaply.
    Stage 2: The flat index re-scores those candidates with full float32 precision.

    The flat index must have been built with the same normalised embeddings added
    in the same corpus order so that IVF-PQ indices align to flat index positions.

    candidate_multiplier: Higher values improve recall at higher latency cost.
    """
    query = query_vector.reshape(1, -1).astype(np.float32)
    faiss.normalize_L2(query)

    n_candidates = top_k * candidate_multiplier
    _, candidate_indices = pq_index.search(query, n_candidates)

    valid_mask = candidate_indices[0] >= 0
    valid_candidates = candidate_indices[0][valid_mask]

    if len(valid_candidates) == 0:
        return np.array([]), np.array([])

    # Reconstruct candidate vectors from the flat index and score them exactly.
    candidate_vectors = np.zeros(
        (len(valid_candidates), flat_index.d), dtype=np.float32
    )
    for i, idx in enumerate(valid_candidates):
        flat_index.reconstruct(int(idx), candidate_vectors[i])

    exact_scores = (candidate_vectors @ query.T).flatten()
    reranked_order = np.argsort(exact_scores)[::-1][:top_k]

    final_indices = valid_candidates[reranked_order]
    final_scores = exact_scores[reranked_order]
    return final_scores, final_indices

Porównanie baz danych wektorowych na rok 2026

Gdy pracujesz nad etapem porównania baz danych wektorowych, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzuć ciche, częściowe ukończenie zadania. Zmierz stopień odzyskiwania informacji na ustalonej serii pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko poprawiają słabą efektywność wyszukiwania. Gdy pracujesz nad etapem porównania baz danych wektorowych, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Przechowuj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury.

FAISS

Faza FAISS działa najlepiej, gdy traktuje się ją jako mierzalną powierzchnię. Zapisz jeden idealny przypadek, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań, zanim rozszerzysz zakres pracy. Zdokumentuj zarówno prawidłowy przebieg operacji, jak i ścieżkę przywracania do normalnego stanu. Próby ponownych działań, kontrola przez ludzi oraz obsługa wiadomości nieodebranych stanowią część produktu, a nie elementy dodawane później w celu udoskonalenia. Oddziel zasadę dzielenia na fragmenty od zasady wyszukiwania. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości.

pgvector

Faza pgvector działa najlepiej, gdy traktuje się ją jako mierzalną powierzchnię. Zapisz jeden idealny przykład działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres pracy. Wolno preferować małe, testowalne jednostki zamiast rozbudowanych skryptów. Gdy jakiś krok zawiedzie, awaria powinna wskazywać na konkretną odpowiedzialność, a nie na skomplikowany łańcuch operacji. Rozdziel politykę dzielenia na fragmenty od polityki pobierania danych. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości.

-- Enable the pgvector extension
CREATE EXTENSION IF NOT EXISTS vector;

-- Policy chunk table with vector and structured metadata
CREATE TABLE policy_chunks (
    chunk_id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    document_id       TEXT NOT NULL,
    document_version  TEXT NOT NULL,
    policy_id         TEXT,
    jurisdiction      TEXT,
    effective_date    DATE,
    section           TEXT,
    content_type      TEXT NOT NULL,
    chunk_text        TEXT NOT NULL,
    classification    TEXT NOT NULL DEFAULT 'INTERNAL',
    permitted_roles   TEXT[] NOT NULL DEFAULT '{}',
    embedding_model   TEXT NOT NULL,
    embedding         vector(1536),
    indexed_at        TIMESTAMPTZ DEFAULT NOW()
);

-- HNSW index for cosine similarity retrieval
CREATE INDEX ON policy_chunks
    USING hnsw (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 200);

-- Partial index for jurisdiction-scoped retrieval (common query pattern)
CREATE INDEX ON policy_chunks
    USING hnsw (embedding vector_cosine_ops)
    WHERE jurisdiction = 'EU';

-- Standard indexes for metadata filter columns
CREATE INDEX ON policy_chunks (policy_id);
CREATE INDEX ON policy_chunks (jurisdiction);
CREATE INDEX ON policy_chunks (classification);
CREATE INDEX ON policy_chunks (effective_date);
import psycopg2
import numpy as np
from typing import List, Dict, Optional


def search_policy_chunks(
    query_embedding: List[float],
    jurisdiction: Optional[str] = None,
    classification_ceiling: str = "INTERNAL",
    permitted_role: Optional[str] = None,
    top_k: int = 10,
    ef_search: int = 100,
    connection_string: str = "postgresql://user:password@localhost:5432/rag_db"
) -> List[Dict]:
    """
    Retrieve policy chunks from pgvector with jurisdiction and access filtering.

    ef_search: Controls the HNSW recall-latency trade-off for this session.
               Set per-session; does not require index rebuild.
    """
    conn = psycopg2.connect(connection_string)
    cur = conn.cursor()

    cur.execute(f"SET hnsw.ef_search = {ef_search};")

    classification_levels = {"PUBLIC": 0, "INTERNAL": 1, "CONFIDENTIAL": 2}
    max_level = classification_levels.get(classification_ceiling, 1)
    permitted_classifications = [
        k for k, v in classification_levels.items() if v <= max_level
    ]

    filters = ["classification = ANY(%s)"]
    params: List = [permitted_classifications]

    if jurisdiction:
        filters.append("jurisdiction = %s")
        params.append(jurisdiction)

    if permitted_role:
        filters.append("%s = ANY(permitted_roles) OR cardinality(permitted_roles) = 0")
        params.append(permitted_role)

    where_clause = " AND ".join(filters)
    embedding_str = "[" + ",".join(str(x) for x in query_embedding) + "]"

    query = f"""
        SELECT
            chunk_id,
            document_id,
            document_version,
            policy_id,
            jurisdiction,
            effective_date,
            section,
            content_type,
            chunk_text,
            1 - (embedding <=> %s::vector) AS cosine_similarity
        FROM policy_chunks
        WHERE {where_clause}
        ORDER BY embedding <=> %s::vector
        LIMIT %s;
    """

    params_with_embedding = [embedding_str] + params + [embedding_str, top_k]
    cur.execute(query, params_with_embedding)
    rows = cur.fetchall()

    columns = [
        "chunk_id", "document_id", "document_version", "policy_id",
        "jurisdiction", "effective_date", "section", "content_type",
        "chunk_text", "cosine_similarity"
    ]
    results = [dict(zip(columns, row)) for row in rows]

    cur.close()
    conn.close()
    return results

Qdrant

Faza Qdrant funkcjonuje najlepiej, gdy traktowana jest jako mierzalna powierzchnia. Zapisz jeden idealny przykład działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań, zanim rozszerzysz zakres. Traktuj tę fazę jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzucaj ciche, częściowe ukończenie zadań. Oddziel politykę dzielenia na fragmenty od polityki pobierania danych. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości. Faza Qdrant funkcjonuje najlepiej, gdy traktowana jest jako mierzalna powierzchnia. Zapisz jeden idealny przykład działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań, zanim rozszerzysz zakres. Przechowuj konfigurację poza kodem aplikacji. Pliki środowiskowe, składysek z danymi poufnymi oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą audytować bez konieczności czytania całego grafu.

from qdrant_client import QdrantClient
from qdrant_client.models import (
    VectorParams, Distance, HnswConfigDiff,
    PointStruct, Filter, FieldCondition, MatchValue, MatchAny,
    SparseVectorParams, SparseIndexParams, SparseVector
)
from typing import List, Dict, Optional

client = QdrantClient(host="localhost", port=6333)

COLLECTION_NAME = "banking_policy"
DENSE_VECTOR_NAME = "dense"
SPARSE_VECTOR_NAME = "sparse"


def create_policy_collection(
    dimension: int = 1536,
    m: int = 16,
    ef_construction: int = 200
) -> None:
    """
    Create a Qdrant collection configured for both dense and sparse vectors.
    """
    client.recreate_collection(
        collection_name=COLLECTION_NAME,
        vectors_config={
            DENSE_VECTOR_NAME: VectorParams(
                size=dimension,
                distance=Distance.COSINE,
                hnsw_config=HnswConfigDiff(
                    m=m,
                    ef_construct=ef_construction,
                    full_scan_threshold=10000
                )
            )
        },
        sparse_vectors_config={
            SPARSE_VECTOR_NAME: SparseVectorParams(
                index=SparseIndexParams(on_disk=False)
            )
        }
    )


def upsert_policy_chunks(chunks: List[Dict]) -> None:
    """
    Index policy chunks with dense vectors, sparse vectors, and metadata payloads.

    Each chunk dict must contain:
        chunk_id, dense_vector, sparse_indices, sparse_values,
        chunk_text, document_id, document_version, policy_id,
        jurisdiction, effective_date, content_type, classification,
        permitted_roles
    """
    points = [
        PointStruct(
            id=chunk["chunk_id"],
            vector={
                DENSE_VECTOR_NAME: chunk["dense_vector"],
                SPARSE_VECTOR_NAME: SparseVector(
                    indices=chunk["sparse_indices"],
                    values=chunk["sparse_values"]
                )
            },
            payload={
                "chunk_text": chunk["chunk_text"],
                "document_id": chunk["document_id"],
                "document_version": chunk["document_version"],
                "policy_id": chunk.get("policy_id"),
                "jurisdiction": chunk.get("jurisdiction"),
                "effective_date": chunk.get("effective_date"),
                "content_type": chunk["content_type"],
                "classification": chunk["classification"],
                "permitted_roles": chunk.get("permitted_roles", []),
                "status": "active"
            }
        )
        for chunk in chunks
    ]
    client.upsert(collection_name=COLLECTION_NAME, points=points)


def search_dense_filtered(
    dense_query: List[float],
    jurisdiction: Optional[str] = None,
    permitted_classifications: List[str] = None,
    top_k: int = 10,
    score_threshold: float = 0.3
) -> List[Dict]:
    """
    Dense vector search with integrated payload filtering.
    Filtering is applied inside the HNSW graph traversal, not as a post-filter.
    """
    if permitted_classifications is None:
        permitted_classifications = ["PUBLIC", "INTERNAL"]

    must_conditions = [
        FieldCondition(
            key="classification",
            match=MatchAny(any=permitted_classifications)
        ),
        FieldCondition(key="status", match=MatchValue(value="active"))
    ]

    if jurisdiction:
        must_conditions.append(
            FieldCondition(key="jurisdiction", match=MatchValue(value=jurisdiction))
        )

    search_filter = Filter(must=must_conditions)

    results = client.search(
        collection_name=COLLECTION_NAME,
        query_vector=(DENSE_VECTOR_NAME, dense_query),
        query_filter=search_filter,
        limit=top_k,
        score_threshold=score_threshold,
        with_payload=True
    )

    return [
        {"chunk_id": hit.id, "score": hit.score, **hit.payload}
        for hit in results
    ]


def search_hybrid_qdrant(
    dense_query: List[float],
    sparse_query_indices: List[int],
    sparse_query_values: List[float],
    jurisdiction: Optional[str] = None,
    permitted_classifications: List[str] = None,
    top_k: int = 10
) -> List[Dict]:
    """
    Hybrid search using both dense and sparse vectors with access-control filtering.

    This uses Qdrant's native prefetch-and-fuse API. Both the dense and sparse
    signals contribute to retrieval. The Fusion.RRF strategy applies Reciprocal
    Rank Fusion internally.
    """
    from qdrant_client.models import Prefetch, FusionQuery, Fusion

    if permitted_classifications is None:
        permitted_classifications = ["PUBLIC", "INTERNAL"]

    must_conditions = [
        FieldCondition(
            key="classification",
            match=MatchAny(any=permitted_classifications)
        ),
        FieldCondition(key="status", match=MatchValue(value="active"))
    ]
    if jurisdiction:
        must_conditions.append(
            FieldCondition(key="jurisdiction", match=MatchValue(value=jurisdiction))
        )
    search_filter = Filter(must=must_conditions)

    results = client.query_points(
        collection_name=COLLECTION_NAME,
        prefetch=[
            Prefetch(
                query=dense_query,
                using=DENSE_VECTOR_NAME,
                limit=top_k * 5,
                filter=search_filter
            ),
            Prefetch(
                query=SparseVector(
                    indices=sparse_query_indices,
                    values=sparse_query_values
                ),
                using=SPARSE_VECTOR_NAME,
                limit=top_k * 5,
                filter=search_filter
            ),
        ],
        query=FusionQuery(fusion=Fusion.RRF),
        limit=top_k,
        with_payload=True
    )

    return [
        {"chunk_id": hit.id, "score": hit.score, **hit.payload}
        for hit in results.points
    ]

Weaviate

Dla etapu Weaviate należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić dany krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy udokumentować zarówno prawidłowy przebieg działania, jak i ścieżkę naprawczą. Próby ponownych działań, kontrola przez ludzi oraz obsługa wiadomości błędnych stanowią część produktu, a nie elementy dodawane później. Należy podać fragmenty tekstu, które faktycznie stanowią podstawę odpowiedzi. Bez tych odniesień operatorzy nie będą w stanie odróżnić halucynacji od luki w indeksowaniu.

Milvus

W fazie Milvus należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić dany krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy preferować małe, łatwe do przetestowania jednostki nad rozbudowanymi skryptami. Gdy dany krok zawiedzie, powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowany łańcuch operacji. Należy podawać konkretne fragmenty tekstu, które stanowią podstawę odpowiedzi. Bez tych odniesień operatorzy nie będą w stanie odróżnić halucynacji od braku danych w indeksie.

ChromaDB

Dla etapu ChromaDB należy zdefiniować dane wejściowe, osobę odpowiedzialną za ten krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nazwij pliki artefaktów, zdefiniuj sprawdzenia sukcesu i odrzuć ciche, częściowe ukończenie zadania. Podaj fragmenty tekstu, które faktycznie stanowią podstawę odpowiedzi. Bez tych odniesień operatorzy nie będą w stanie odróżnić halucynacji od luki w indeksowaniu. Dla etapu ChromaDB należy zdefiniować dane wejściowe, osobę odpowiedzialną za ten krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Przechowuj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury.

Pinecone

Gdy pracujesz nad etapem Pinecone, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Zdokumentuj zarówno prawidłowy przebieg, jak i ścieżkę naprawczą. Próby ponowne, kontrola przez ludzi oraz obsługa wiadomości błędowych są częścią produktu, a nie elementem dodatkowej optymalizacji. Zmierz stopień odzyskiwania informacji na ustalonej grupie pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko naprawiają słabe mechanizmy wyszukiwania.

Wyszukiwanie hybrydowe: łączenie gęstego i rzadkiego wyszukiwania

Gdy pracujesz nad etapem łączenia wyników z wyszukiwania hybrydowego typu Dense, najpierw zapisz specyfikację: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Wolę małe, testowalne jednostki niż rozbudowane skrypty. Gdy jakiś krok się nie powiedzie, błąd powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowany łańcuch operacji. Zmierz stopień odzyskiwania informacji na ustalonej grupie pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko naprawiają słabe wyniki wyszukiwania.

Fuzja rankingu wzajemnego z wagami

Gdy pracujesz nad etapem fuzji ważonego wzajemnego rankingu, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzuć ciche, częściowe ukończenie zadania. Zmierz stopień przywoływalności na ustalonej serii pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko poprawiają słabą efektywność wyszukiwania. Gdy pracujesz nad etapem fuzji ważonego wzajemnego rankingu, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Przechowuj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą audytować bez konieczności czytania całej struktury.

from typing import List, Dict, Tuple
from collections import defaultdict


def weighted_reciprocal_rank_fusion(
    dense_results: List[Tuple[str, float]],
    sparse_results: List[Tuple[str, float]],
    k: int = 60,
    dense_weight: float = 0.6,
    sparse_weight: float = 0.4
) -> List[Tuple[str, float]]:
    """
    Fuse dense vector search results with sparse BM25 results using weighted RRF.

    dense_results: List of (chunk_id, dense_score) sorted by dense score descending.
    sparse_results: List of (chunk_id, sparse_score) sorted by sparse score descending.
    k: RRF constant. Higher k reduces the influence of top-ranked documents.
       Conventional default: 60.
    dense_weight / sparse_weight: Relative weights. Tune against your evaluation set.
       For corpora with high-precision identifier queries, increase sparse_weight.

    Returns fused list of (chunk_id, rrf_score) sorted by rrf_score descending.
    """
    rrf_scores: Dict[str, float] = defaultdict(float)

    for rank, (chunk_id, _) in enumerate(dense_results, start=1):
        rrf_scores[chunk_id] += dense_weight * (1.0 / (k + rank))

    for rank, (chunk_id, _) in enumerate(sparse_results, start=1):
        rrf_scores[chunk_id] += sparse_weight * (1.0 / (k + rank))

    return sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True)

Dostrojenie masy gęstej-rzadkiej

Etap dostrojenia masy gęstej-rzadkiej działa najlepiej, gdy traktuje się go jako mierzalną powierzchnię. Zapisz jeden idealny przypadek, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań przed rozszerzeniem zakresu. Zdokumentuj razem ścieżkę pomyślnego przebiegu oraz ścieżkę przywracania stanu. Próby ponowne, kontrola przez ludzi oraz obsługa wiadomości nieodebranych stanowią część produktu, a nie elementy dodawane później. Oddziel zasadę dzielenia na fragmenty od zasady wyszukiwania. Zmiana jednej nie powinna zmuszać do przepisania drugiej w przypadku zmian wskaźników jakości.

Filtrowanie metadanych: zakres wyszukiwania i granice uprawnień

Etap filtrowania metadanych i określania zakresu pobierania działa najlepiej, gdy traktuje się go jako mierzalną powierzchnię. Zapisz jeden idealny przykład działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań przed rozszerzeniem zakresu. Wolno preferować małe, testowalne jednostki zamiast rozbudowanych skryptów. Gdy jakiś krok zawiedzie, awaria powinna wskazywać na konkretną odpowiedzialność, a nie na skomplikowany łańcuch operacji. Oddziel zasady dzielenia na fragmenty od zasad pobierania danych. Zmiana jednych nie powinna zmuszać do przepisywania drugich, gdy zmieniają się metryki jakości.

from typing import List, Dict, Optional
from enum import Enum


class ClassificationLevel(Enum):
    PUBLIC = 0
    INTERNAL = 1
    CONFIDENTIAL = 2


def build_access_filter(
    user_classification_ceiling: str,
    user_jurisdiction: Optional[str] = None,
    user_roles: Optional[List[str]] = None
) -> Dict:
    """
    Build a Qdrant-compatible filter dict enforcing access control rules.

    user_classification_ceiling: Highest classification the user can see.
    user_jurisdiction: If set, restrict to chunks applicable to that jurisdiction.
    user_roles: If set, restrict to chunks permitted for those roles.

    Integrate with your identity provider at request time, not at index time.
    This filter represents one layer of the authorisation model; it does not
    replace identity verification, audit logging, tenant isolation, or
    downstream response controls.
    """
    ceiling = ClassificationLevel[user_classification_ceiling].value
    permitted = [
        level.name
        for level in ClassificationLevel
        if level.value <= ceiling
    ]

    must_conditions = [
        {"key": "classification", "match": {"any": permitted}},
        {"key": "status", "match": {"value": "active"}}
    ]

    if user_jurisdiction:
        must_conditions.append(
            {"key": "jurisdiction", "match": {"value": user_jurisdiction}}
        )

    if user_roles:
        # Chunks with empty permitted_roles are accessible to all roles.
        must_conditions.append({
            "should": [
                {"key": "permitted_roles", "match": {"any": user_roles}},
                {"is_empty": {"key": "permitted_roles"}}
            ]
        })

    return {"must": must_conditions}

Indeksowanie stopniowe: dodawanie nowych dokumentów bez ponownego budowania

Etap dodawania nowych elementów w ramach stopniowego indeksowania funkcjonuje najlepiej, gdy traktowany jest jako mierzalna powierzchnia do analizy. Zapisz jeden idealny przykład działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań, zanim rozszerzysz zakres pracy. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzucaj ciche, częściowe ukończenie zadań. Rozdziel politykę dzielenia na fragmenty od polityki pobierania danych. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości. Etap dodawania nowych elementów w ramach stopniowego indeksowania funkcjonuje najlepiej, gdy traktowany jest jako mierzalna powierzchnia do analizy. Zapisz jeden idealny przykład działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań, zanim rozszerzysz zakres pracy. Przechowuj konfigurację poza kodem aplikacji. Pliki środowiskowe, składyce z danymi poufnymi oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą audytować bez konieczności czytania całej struktury.

import logging
from typing import List, Dict
from datetime import datetime

logger = logging.getLogger(__name__)


class IncrementalIndexManager:
    """
    Manages incremental updates to a Qdrant collection using soft deletion.

    Production pattern:
    1. New chunks are inserted immediately with status='active'.
    2. Superseded chunks are marked status='deleted' (soft delete).
    3. Retrieval filters exclude deleted chunks without graph rebuild.
    4. Full rebuild is triggered on schedule or when deleted fraction exceeds threshold.
    """

    def __init__(self, qdrant_client, collection_name: str):
        self.client = qdrant_client
        self.collection = collection_name
        self.deleted_threshold = 0.15  # Rebuild when 15% of index is soft-deleted

    def upsert_policy_version(
        self,
        new_chunks: List[Dict],
        superseded_chunk_ids: List[str],
        policy_id: str,
        new_version: str
    ) -> Dict:
        """
        Insert new policy version chunks and soft-delete superseded ones.
        """
        if superseded_chunk_ids:
            self.client.set_payload(
                collection_name=self.collection,
                payload={
                    "status": "deleted",
                    "deleted_at": datetime.now().isoformat(),
                    "superseded_by_version": new_version
                },
                points=superseded_chunk_ids
            )
            logger.info(
                f"Soft-deleted {len(superseded_chunk_ids)} chunks "
                f"from policy {policy_id}, superseded by version {new_version}"
            )

        from qdrant_client.models import PointStruct
        points = [
            PointStruct(
                id=chunk["chunk_id"],
                vector={"dense": chunk["dense_vector"]},
                payload={
                    **{k: v for k, v in chunk.items()
                       if k not in ("chunk_id", "dense_vector")},
                    "status": "active",
                    "indexed_at": datetime.now().isoformat()
                }
            )
            for chunk in new_chunks
        ]

        self.client.upsert(collection_name=self.collection, points=points)
        logger.info(
            f"Inserted {len(new_chunks)} chunks for policy {policy_id} version {new_version}"
        )

        return {
            "inserted": len(new_chunks),
            "soft_deleted": len(superseded_chunk_ids),
            "policy_id": policy_id,
            "new_version": new_version
        }

    def should_rebuild(self) -> bool:
        """Check whether the fraction of soft-deleted vectors justifies a full rebuild."""
        from qdrant_client.models import Filter, FieldCondition, MatchValue

        total = self.client.get_collection(self.collection).vectors_count
        deleted_filter = Filter(must=[
            FieldCondition(key="status", match=MatchValue(value="deleted"))
        ])
        deleted_count = self.client.count(
            collection_name=self.collection,
            count_filter=deleted_filter
        ).count

        fraction = deleted_count / total if total > 0 else 0
        logger.info(
            f"Index health: {deleted_count}/{total} soft-deleted ({fraction:.1%})"
        )
        return fraction >= self.deleted_threshold

Zawieranie zmian wersji modelu

W fazie zmian wersji modelu Embedding należy zdefiniować dane wejściowe, osobę odpowiedzialną za daną czynność oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić tę czynność na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy udokumentować zarówno prawidłowy przebieg procesu, jak i ścieżkę naprawczą. Próby ponownych działań, kontrola przez ludzi oraz obsługa wiadomości błędnych stanowią część produktu, a nie elementy dodawane później. W przypadku, gdy następnym krokiem jest kod lub wywołanie narzędzia, należy preferować ustrukturyzowane wyniki z walidacją schematu zamiast tekstów w formie swobodnej.

Sharding i replikacja: skalowanie poza pojedynczy węzeł

W fazie skalowania poprzez sharding i replikację należy zdefiniować dane wejściowe, osobę odpowiedzialną za daną czynność oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić tę czynność na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu systemu. Należy preferować małe, łatwe do przetestowania jednostki nad rozbudowanymi skryptami. Gdy jakaś czynność zawiedzie, powód awarii powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowaną strukturę procesów. Należy podawać konkretne fragmenty tekstu, które stanowią podstawę odpowiedzi. Bez tych odniesień operatorzy nie będą w stanie odróżnić fałszywych informacji od braków w indeksowaniu.

Strategie shardingu

W fazie Strategii Sharding należy zdefiniować dane wejściowe, osobę odpowiedzialną za daną czynność oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić tę czynność na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Traktuj tę fazę jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nazwij poszczególne elementy, zdefiniuj kryteria sukcesu i odrzuć ciche, częściowe ukończenie zadania. Podaj fragmenty tekstu, które faktycznie stanowią podstawę odpowiedzi. Bez tych odniesień operatorzy nie będą w stanie odróżnić halucynacji od luki w indeksowaniu. W fazie Strategii Sharding należy zdefiniować dane wejściowe, osobę odpowiedzialną za daną czynność oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić tę czynność na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Przechowuj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury.

Planowanie przepustowości

Podczas przechodzenia przez etap planowania przepustowości najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co się dzieje w przypadku częściowego awarii. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Zdokumentuj zarówno ścieżkę prawidłowego działania, jak i ścieżkę przywracania stanu. Próby ponowne, kontrolne punkty ludzkie oraz obsługa wiadomości błędnych stanowią część produktu, a nie elementy dodawane później w celu udoskonalenia. Zmierz skuteczność wyszukiwania na ustalonej serii pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko naprawiają słabe możliwości wyszukiwania.

from dataclasses import dataclass


@dataclass
class VectorIndexCapacityPlan:
    """
    Illustrative capacity model for HNSW vector indexes.
    All figures are approximations for planning purposes.
    Benchmark against your actual implementation and workload.
    """
    n_vectors: int
    dimension: int
    hnsw_m: int = 16
    replication_factor: int = 2
    avg_payload_bytes: int = 2048
    memory_headroom_factor: float = 1.5

    def vector_storage_gb(self) -> float:
        return (self.n_vectors * self.dimension * 4) / (1024 ** 3)

    def hnsw_graph_gb(self) -> float:
        # Approximate; actual graph overhead varies by implementation and configuration.
        return (self.n_vectors * self.hnsw_m * 2 * 8) / (1024 ** 3)

    def payload_storage_gb(self) -> float:
        return (self.n_vectors * self.avg_payload_bytes) / (1024 ** 3)

    def total_index_gb(self) -> float:
        return self.vector_storage_gb() + self.hnsw_graph_gb() + self.payload_storage_gb()

    def memory_per_replica_gb(self) -> float:
        return self.total_index_gb() * self.memory_headroom_factor

    def total_cluster_memory_gb(self) -> float:
        # Each replica holds a full copy of the index.
        return self.memory_per_replica_gb() * self.replication_factor

    def report(self) -> str:
        return (
            f"Illustrative capacity model — {self.n_vectors:,} vectors at {self.dimension}d:\n"
            f"  Vector storage (approx):         {self.vector_storage_gb():.2f} GB\n"
            f"  HNSW graph estimate:             {self.hnsw_graph_gb():.2f} GB\n"
            f"  Payload storage (approx):        {self.payload_storage_gb():.2f} GB\n"
            f"  Index footprint before overhead: {self.total_index_gb():.2f} GB\n"
            f"  Per-replica memory + headroom:   {self.memory_per_replica_gb():.2f} GB\n"
            f"  Total cluster memory (approx):   {self.total_cluster_memory_gb():.2f} GB\n"
            f"  ({self.replication_factor} replicas, each holding a full copy)\n"
            f"  Treat these as planning estimates, not deployment guarantees.\n"
            f"  Benchmark against your implementation before provisioning."
        )


# Illustrative example: banking policy corpus
plan = VectorIndexCapacityPlan(
    n_vectors=500_000,
    dimension=1536,
    hnsw_m=16,
    replication_factor=2,
    avg_payload_bytes=2048,
    memory_headroom_factor=1.5
)
print(plan.report())

Pełny pipeline wyszukiwania

Gdy przechodzisz przez etap The Complete Retrieval Pipeline, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Wolij małe, testowalne jednostki nad rozbudowane skrypty. Gdy jakiś krok się nie powiedzie, błąd powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowany proces. Zmierz stopień odzyskiwania informacji na ustalonej grupie pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko naprawiają słabe funkcje odzyskiwania danych.

User Query
    ↓
Query Embedding (dense + sparse)
    ↓
Metadata / Authorisation Constraints
    ↓
Dense Vector Retrieval (filtered HNSW)
        +
Sparse Retrieval (BM25)
    ↓
Weighted RRF Fusion
    ↓
Candidate Documents with Provenance Metadata
    ↓
[Part 7: Reranking and Context Assembly]
    ↓
LLM Generation
import logging
import re
from typing import List, Dict, Optional
from dataclasses import dataclass
from collections import defaultdict

logger = logging.getLogger(__name__)


@dataclass
class RetrievalConfig:
    dense_candidate_pool: int = 50
    sparse_candidate_pool: int = 50
    rrf_k: int = 60
    dense_weight: float = 0.6
    sparse_weight: float = 0.4
    final_top_k: int = 10
    score_threshold: float = 0.2


class BankingPolicyRetriever:
    """
    Production retrieval pipeline for a regulated banking policy corpus.
    Combines dense vector search, BM25 sparse retrieval, access filtering,
    and weighted RRF score fusion.

    Retrieval ends at the fused candidate list. Reranking and context assembly
    are handled in Part 7.
    """

    def __init__(
        self,
        qdrant_client,
        collection_name: str,
        embedding_pipeline,
        bm25_index,
        chunk_store: Dict[str, Dict],
        config: Optional[RetrievalConfig] = None
    ):
        self.client = qdrant_client
        self.collection = collection_name
        self.embedder = embedding_pipeline
        self.bm25 = bm25_index
        self.chunk_store = chunk_store
        self.config = config or RetrievalConfig()

    def retrieve(
        self,
        query: str,
        user_classification_ceiling: str = "INTERNAL",
        user_jurisdiction: Optional[str] = None,
        user_roles: Optional[List[str]] = None
    ) -> List[Dict]:
        """
        Full hybrid retrieval with access control.

        Returns top-k chunks with provenance metadata, access-filtered
        for the requesting user's classification ceiling and jurisdiction.
        """
        from qdrant_client.models import Filter, FieldCondition, MatchValue, MatchAny

        query_embedding = self.embedder.embed_query(query)

        classification_levels = {"PUBLIC": 0, "INTERNAL": 1, "CONFIDENTIAL": 2}
        ceiling = classification_levels.get(user_classification_ceiling, 1)
        permitted_classifications = [
            k for k, v in classification_levels.items() if v <= ceiling
        ]

        must_conditions = [
            FieldCondition(
                key="classification",
                match=MatchAny(any=permitted_classifications)
            ),
            FieldCondition(key="status", match=MatchValue(value="active"))
        ]
        if user_jurisdiction:
            must_conditions.append(
                FieldCondition(
                    key="jurisdiction",
                    match=MatchValue(value=user_jurisdiction)
                )
            )

        access_filter = Filter(must=must_conditions)

        # Dense vector search with integrated access filtering
        dense_hits = self.client.search(
            collection_name=self.collection,
            query_vector=("dense", query_embedding),
            query_filter=access_filter,
            limit=self.config.dense_candidate_pool,
            score_threshold=self.config.score_threshold,
            with_payload=True
        )
        dense_results = [(hit.id, hit.score) for hit in dense_hits]

        # Sparse BM25 retrieval with post-retrieval access filtering
        tokens = re.findall(r'\b\w+\b', query.lower())
        bm25_scores = self.bm25.get_scores(tokens)
        sparse_ranked = sorted(enumerate(bm25_scores), key=lambda x: x[1], reverse=True)

        chunk_ids = list(self.chunk_store.keys())
        sparse_results = []
        for corpus_idx, score in sparse_ranked:
            if score <= 0 or len(sparse_results) >= self.config.sparse_candidate_pool:
                break
            chunk_id = chunk_ids[corpus_idx]
            chunk_meta = self.chunk_store.get(chunk_id, {})
            if chunk_meta.get("classification") not in permitted_classifications:
                continue
            if user_jurisdiction and chunk_meta.get("jurisdiction") != user_jurisdiction:
                continue
            if chunk_meta.get("status") != "active":
                continue
            sparse_results.append((chunk_id, score))

        # Weighted RRF fusion
        rrf_scores: Dict[str, float] = defaultdict(float)
        k = self.config.rrf_k

        for rank, (chunk_id, _) in enumerate(dense_results, start=1):
            rrf_scores[chunk_id] += self.config.dense_weight * (1.0 / (k + rank))

        for rank, (chunk_id, _) in enumerate(sparse_results, start=1):
            rrf_scores[chunk_id] += self.config.sparse_weight * (1.0 / (k + rank))

        fused = sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True)
        top_chunk_ids = [cid for cid, _ in fused[:self.config.final_top_k]]

        # Assemble results with provenance metadata
        results = []
        for chunk_id in top_chunk_ids:
            chunk_data = self.chunk_store.get(chunk_id, {})
            results.append({
                "chunk_id": chunk_id,
                "rrf_score": rrf_scores[chunk_id],
                **chunk_data
            })

        logger.info(
            f"Retrieval complete: query={query[:60]!r}, "
            f"dense_candidates={len(dense_results)}, "
            f"sparse_candidates={len(sparse_results)}, "
            f"final_results={len(results)}"
        )
        return results

Obserwowalność: Co mierzyć w produkcji

Gdy przechodzisz przez etap Obserwowalności – co mierzyć, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Ta lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzuć przypadkowe, częściowe ukończenie zadań. Zmierz stopień przywoływalności na ustalonej serii pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko naprawiają słabe możliwości wyszukiwania informacji. Gdy przechodzisz przez etap Obserwowalności – co mierzyć, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Ta lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Przechowuj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury.

import time
import logging
from typing import Callable, TypeVar, Any
from functools import wraps

logger = logging.getLogger(__name__)
F = TypeVar("F", bound=Callable[..., Any])


def retrieval_instrumented(func: F) -> F:
    """
    Decorator that adds structured latency logging and empty-result alerting
    to retrieval functions. Wrap your primary retrieve() method in production.
    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = None
        error = None

        try:
            result = func(*args, **kwargs)
            return result
        except Exception as e:
            error = str(e)
            raise
        finally:
            elapsed_ms = (time.perf_counter() - start) * 1000
            n_results = len(result) if result is not None else 0

            log_payload = {
                "function": func.__name__,
                "latency_ms": round(elapsed_ms, 2),
                "n_results": n_results,
                "error": error
            }

            query = kwargs.get("query", args[1] if len(args) > 1 else None)
            if query:
                log_payload["query_prefix"] = str(query)[:80]

            if error:
                logger.error("retrieval_error", extra=log_payload)
            elif n_results == 0:
                logger.warning("retrieval_empty_result", extra=log_payload)
            elif elapsed_ms > 500:
                logger.warning("retrieval_high_latency", extra=log_payload)
            else:
                logger.info("retrieval_success", extra=log_payload)

    return wrapper  # type: ignore

Powrót do propozycji kredytu w wysokości 12 milionów euro

Faza powrotu do stanu EUR funkcjonuje najlepiej, gdy jest traktowana jako mierzalna powierzchnia do analizy. Zapisz jeden udany przypadek, jeden przypadek niepowodzenia oraz notatkę dotyczącą cofnięcia działań przed rozszerzeniem zakresu. Zdokumentuj zarówno ścieżkę pomyślnego działania, jak i ścieżkę naprawczą. Próby ponownych działań, kontrola przez ludzi oraz obsługa wiadomości nieodebranych stanowią część produktu, a nie elementy dodawane później. Oddziel zasady dzielenia na fragmenty od zasad wyszukiwania. Zmiana jednych nie powinna zmuszać do przepisywania drugich w przypadku zmian wskaźników jakości.

Zanim przejdziesz do ponownego rankingu: lista kontrolna dla produkcji

Faza „Before You Move to” funkcjonuje najlepiej, gdy traktowana jest jako mierzalna powierzchnia. Zapisz jeden przykład udanego przeprowadzenia, jeden przypadek niepowodzenia oraz notatkę dotyczącą cofnięcia działań, zanim rozszerzysz zakres pracy. Wolno preferować małe, testowalne jednostki zamiast rozbudowanych skryptów. Gdy jakiś krok się nie powiedzie, problem powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowany łańcuch operacji. Rozdziel politykę dzielenia na części od polityki pobierania danych. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości.

Co następuje dalej

Etap „What Comes Next” funkcjonuje najlepiej, gdy traktowany jest jako mierzalna powierzchnia do analizy. Zapisz jeden idealny przepis działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań, zanim rozszerzysz zakres pracy. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzucaj ciche, częściowe ukończenie zadań. Rozdziel politykę dzielenia na fragmenty od polityki pobierania danych. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości. Etap „What Comes Next” funkcjonuje najlepiej, gdy traktowany jest jako mierzalna powierzchnia do analizy. Zapisz jeden idealny przepis działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań, zanim rozszerzysz zakres pracy. Przechowuj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą audytować bez konieczności czytania całej struktury.

Lista kontrolna operacyjna

W fazie listy kontrolnej operacyjnej należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie wykonać ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu.

Należy odnotować czasy wykonywania oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega niespodziewanym rachunkom, gdy proces przechodzi z środowiska demonstracyjnego do współdzielonych środowisk.

Należy podać fragmenty tekstu, które faktycznie stanowią podstawę odpowiedzi. Bez tych odniesień operatorzy nie mogą odróżnić efektu halucynacji od luki w indeksowaniu.

Należy śledzić koszty i opóźnienia obok jakości. Odpowiedź nieco gorsza, która kosztuje 10 razy mniej, może być właściwym kompromisem w środowisku produkcyjnym.

Należy ustalić konkretne wersje zależności oraz zapisać informacje o obrazie użytym do uruchomienia demonstracji. Reprodukowalność jest ważniejsza od lokalnej wiedzy zespołu.

Należy preferować małe, testowalne jednostki zamiast rozbudowanych skryptów. Gdy jakiś krok zawiedzie, błąd powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowany łańcuch operacji.

Zanim wdrożymy nową architekturę, należy zamrozić istniejące wersje, utworzyć dokładny zapis działań dla kluczowych etapów oraz potwierdzić kroki odwracające zmiany. Środowiska współdzielone wymagają ograniczeń szybkości, weryfikacji uprawnień użytkowników oraz wyraźnego odpowiedzialnego za rotację haseł. Lepiej mieć nudną, niezawodną architekturę niż genialne, jednorazowe demonstracje.

Uwaga dotycząca fa68a70d815a: unikaj przechowywania kluczy dostawcy w repozytorium, ustaw ograniczenie liczby tokenów na sesję oraz przechowuj zapisy działań obok plików testowych, aby późniejsze zmiany modeli pozostały porównywalne.

Dla etapu 0 notatki dotyczącej wzmocnienia bezpieczeństwa należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie wykonać ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nazwij poszczególne elementy, zdefiniuj kryteria sukcesu i odrzuć ciche, częściowe ukończenie zadania.

Szczegóły wzmocnienia bezpieczeństwa 0/864: zmierz czas wykonywania, klasę błędów oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę, opierając się na ustalonej serii pytań, a nie na indywidualnych obserwacjach.

Gdy przechodzisz przez pierwszy etap notatki dotyczącej wzmocnienia bezpieczeństwa, najpierw zapisz warunki kontraktu: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego awarii. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Przechowuj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury.

Szczegół wzmocnienia bezpieczeństwa 1/864: zmierz czas wykonywania, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę na podstawie ustalonego zestawu pytań, a nie jedynie osobistych obserwacji.

Etap notatki dotyczącej wzmocnienia bezpieczeństwa 2 działa najlepiej, gdy traktuje się go jako mierzalną powierzchnię do analizy. Zapisz jeden idealny przepływ działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres prac. Wolij małe, testowalne jednostki nad rozbudowane skrypty. Gdy jakiś krok zawiedzie, awaria powinna wskazywać na konkretną odpowiedzialność, a nie na skomplikowany łańcuch operacji.

Szczegół wzmocnienia 2/864: zmierz czas wykonywania, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę na podstawie ustalonego zestawu pytań, a nie jedynie anegdoty.

W trzecim etapie notatki dotyczącej wzmocnienia określ dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie wykonać ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Zapisuj czas wykonywania oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega niespodziewanym rachunkom, gdy ścieżka przechodzi z środowiska demonstracyjnego do współdzielonych środowisk.

Szczegół wzmocnienia 3/864: zmierz czas wykonywania, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę na podstawie ustalonego zestawu pytań, a nie jedynie anegdoty.

Gdy przechodzisz przez etap nr 4 notatki dotyczącej wzmocnienia bezpieczeństwa, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego awarii. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Zdokumentuj zarówno prawidłowy przebieg działania, jak i ścieżkę odzyskiwania. Próby ponownych działań, kontrola przez ludzi oraz obsługa wiadomości błędowych stanowią część produktu, a nie elementy dodawane później.

Szczegół nr 4/864 dotyczący wzmocnienia bezpieczeństwa: zmierz czas wykonywania operacji, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę na podstawie ustalonego zestawu pytań, a nie jedynie informacji anegdotycznych.

Etap nr 5 notatki dotyczącej wzmocnienia bezpieczeństwa działa najlepiej, gdy traktuje się go jako mierzalną powierzchnię do analizy. Zapisz jeden idealny przepływ działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres prac. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzuć przypadki cichego, częściowego ukończenia zadania.

Szczegół wzmocnienia 5/864: zmierz czas wykonywania, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę na podstawie ustalonego zestawu pytań, a nie jedynie anegdoty.

W etapie 6 notatki dotyczącej wzmocnienia określ dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić ten krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Konfigurację należy przechowywać poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności przeglądania całej struktury.

Szczegół wzmocnienia 6/864: zmierz czas wykonywania, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę na podstawie ustalonego zestawu pytań, a nie jedynie anegdoty.

Gdy przechodzisz przez etap 7 notatki dotyczącej wzmocnienia bezpieczeństwa, najpierw zapisz warunki kontraktu: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego awarii. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Wolno preferować małe, testowalne jednostki zamiast rozbudowanych skryptów. Gdy jakiś krok zawiedzie, awaria powinna wskazywać na konkretną odpowiedzialność, a nie na skomplikowany łańcuch operacji.

Szczegół 7/864 dotyczący wzmocnienia bezpieczeństwa: zmierz czas wykonywania, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę na podstawie ustalonego zestawu pytań, a nie jedynie osobistych obserwacji.

Etap 8 notatki o wzmocnieniu bezpieczeństwa działa najlepiej, gdy traktuje się go jako mierzalną powierzchnię do analizy. Zapisz jeden idealny przykład działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres prac. Zapisuj czasy wykonywania oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega nieoczekiwanym rachunkom, gdy przechodzi się od środowiska demonstracyjnego do wspólnych środowisk.

Szczegół wzmocnienia 8/864: zmierz czas wykonywania, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę na podstawie ustalonego zestawu pytań, a nie jedynie anegdoty.

W fazie 9 notatki dotyczącej wzmocnienia określ dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie wykonać ten krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Zdokumentuj zarówno prawidłowy przebieg działania, jak i ścieżkę naprawczą. Próby ponowne, kontrola przez ludzi oraz obsługa wiadomości błędnych stanowią część produktu, a nie element późniejszej dopracowywania.

Szczegół wzmocnienia 9/864: zmierz czas wykonywania, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę na podstawie ustalonego zestawu pytań, a nie jedynie anegdoty.

Gdy przechodzisz przez etap 10 notatki dotyczącej wzmocnienia bezpieczeństwa, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzuć możliwość cichego, częściowego ukończenia zadania.

Szczegół 10/864 dotyczący wzmocnienia bezpieczeństwa: zmierz czas wykonywania, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę na podstawie ustalonego zestawu pytań, a nie jedynie informacji anegdotycznych.

Etap 11 notatki dotyczącej wzmocnienia bezpieczeństwa funkcjonuje najlepiej, gdy jest traktowany jako mierzalna powierzchnia do analizy. Zapisz jeden idealny przykład działania, jeden przypadek niepowodzenia oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres prac. Utrzymuj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności przeglądania całej struktury.

Szczegół wzmocnienia 11/864: zmierz czas wykonywania, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę na podstawie ustalonego zestawu pytań, a nie jedynie anegdoty.

W etapie 12 notatki dotyczącej wzmocnienia określ dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić ten krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Wolno preferować małe, testowalne jednostki zamiast rozbudowanych skryptów. Gdy dany krok zawiedzie, powód awarii powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowany proces.

Szczegół wzmocnienia 12/864: zmierz czas wykonywania, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę na podstawie ustalonego zestawu pytań, a nie jedynie anegdoty.

Gdy przechodzisz przez etap 13 notatki dotyczącej wzmocnienia bezpieczeństwa, najpierw zapisz warunki kontraktu: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego awarii. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Obok wyników funkcjonalnych zapisz czas wykonywania oraz koszt tokena lub zapytania. Wczesna widoczność kosztów zapobiega niespodziewanym rachunkom, gdy ścieżka przechodzi z środowiska demonstracyjnego do współdzielonych środowisk.

Szczegóły wzmocnienia bezpieczeństwa 13/864: zmierz czas wykonywania, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę na podstawie ustalonego zestawu pytań, a nie jedynie osobistych obserwacji.

Etap 14 notatki dotyczącej wzmocnienia bezpieczeństwa działa najlepiej, gdy traktuje się go jako mierzalną powierzchnię do analizy. Zanim rozszerzysz zakres, zapisz jeden idealny przypadek działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian. Zdokumentuj razem ścieżkę prawidłowego działania oraz ścieżkę naprawczą. Próby ponownych działań, kontrolne punkty ludzkie oraz obsługa wiadomości błędnych stanowią część produktu, a nie elementy dodawane później.

Szczegóły wzmocnienia bezpieczeństwa 14/864: zmierz czas przetwarzania ściany, klasę błędu oraz zużycie tokenów dla tej notatki, a następnie zdecyduj, czy zachować zmianę na podstawie ustalonego zestawu pytań, a nie jedynie anegdoty.

Literatura pokrewna

  • Notatki praktyczne: Czym jest Retrieval-Augmented Generation (RAG)? Praktyczny przewodnik — Krok po kroku opis Notatek praktycznych: Czym jest Retrieval-Augmented Generation (RAG)? Praktyczny przewodnik: kontrakty, sprawdzenia oraz gotowe elementy kodu dla zespołów wdrażających ten wzorzec.
  • Notatki praktyczne: Hybrydowe pozyskiwanie danych dla pamięci agenta: wektorowe, leksykalne i — Krok po kroku opis Notatek praktycznych: Hybrydowe pozyskiwanie danych dla pamięci agenta: wektorowe, leksykalne i: kontrakty, sprawdzenia oraz gotowe elementy kodu dla zespołów wdrażających ten wzorzec.
  • rebasis: Zmiana modeli embeddingowych RAG bez ponownego indeksowania — Szczegółowy przewodnik po rebasis: Zmiana modeli embeddingowych RAG bez ponownego indeksowania: kontrakty, sprawdzenia oraz gotowe fragmenty kodu dla zespołów wdrażających ten wzorzec.
  • Praktyczne wskazówki: Wdrażanie RAG do produkcji: problemy end-to-end z rzeczywistych przypadków — Szczegółowy przewodnik po Praktycznych wskazówkach: Wdrażanie RAG do produkcji: problemy end-to-end z rzeczywistych przypadków: kontrakty, sprawdzenia oraz gotowe fragmenty kodu dla zespołów wdrażających ten wzorzec.
  • Notatki praktyczne: Ograniczenia wyszukiwania wektorowego w RAG — podobieństwo kosinowe i inne aspekty — Szczegółowy przewodnik po Notatkach praktycznych: Ograniczenia wyszukiwania wektorowego w RAG — podobieństwo kosinowe i inne aspekty: szablony umów, sprawdzeń oraz gotowych fragmentów kodu dla zespołów wdrażających ten model.
  • Notatki praktyczne: Znaczenie jako współrzędne: wyszukiwanie semantyczne, bazy danych wektorowych i inne aspekty — Szczegółowy przewodnik po Notatkach praktycznych: Znaczenie jako współrzędne: wyszukiwanie semantyczne, bazy danych wektorowych i inne aspekty: szablony umów, sprawdzeń oraz gotowych fragmentów kodu dla zespołów wdrażających ten model.