Головна / Статті / Практичні нотатки: PostgreSQL + pgvector та SQL Server 2025 як сховища векторів

Практичні нотатки: PostgreSQL + pgvector та SQL Server 2025 як сховища векторів

Покрокове керівництво з практичних нотаток: PostgreSQL + pgvector та SQL Server 2025 як сховища векторів: контракти, перевірки та готові фрагменти коду для команд, які впроваджують цю модель.

4064 слів

Наступні примітки відтворюють практичний підхід до керування проектом „PostgreSQL + pgvector та SQL Server 2025 як сховища векторів для RAG — Посібник для практиків“. Основна увага приділяється контрактам, перевіркам та шаблонам коду замість мотиваційних формулювань.

Ландшафт векторних баз даних у 2025 році

Під час роботи над етапом аналізу ландшафту векторних баз даних спочатку запишіть контракт: необхідні вхідні дані, сигнал про успіх та наслідки часткової невдачі. Цей перелік допомагає зберігати чесність пізніших змін у коді. Розглядайте цей етап як контракт між вхідними даними та перевіреними результатами. Позначте всі елементи, визначте критерії успіху та не допускайте беззвучного часткового виконання завдань. Перед налаштуванням запитів вимірюйте рівень відтворення інформації на фіксованому наборі запитань. Часта зміна формулювань запитів рідко допомагає покращити ефективність пошуку.

-- Find the most relevant chunks, but only from documents
-- belonging to enterprise-tier customers — a single SQL query
SELECT c.content, 1 - (c.embedding <=> query_vec) AS score
FROM rag_chunks c
JOIN documents d ON d.filename = c.source
JOIN customers cu ON cu.id = d.customer_id
WHERE cu.tier = 'enterprise'
ORDER BY score DESC
LIMIT 5;

Повний стек

Під час роботи над етапом The Full Stack спочатку запишіть контракт: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допомагає зберігати чесність пізніших змін у коді. Запишіть час виконання та витрати на токени або запити поруч із функціональними результатами. Відображення витрат заздалегідь запобігає несподіваним рахункам, коли процес переходить від демо-середовища до спільних. Вимірюйте рівень відтворення даних на фіксованому наборі запитань перед налаштуванням підказок. Часта зміна підказок рідко виправляє проблеми з низькою ефективністю пошуку.

Частина 1 — PostgreSQL 18 + pgvector

Під час виконання етапу Part 1 PostgreSQL 18 спочатку запишіть умови використання: необхідні вхідні дані, сигнал про успішне виконання та те, що відбувається у разі часткової невдачі. Такий перелік допомагає зберігати чесність пізніших змін у коді. Тримайте конфігурацію окремо від коду додатку. Файли середовища, сховища конфіденційних даних та флаги функцій мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь код. Вимірюйте рівень відтворення інформації на фіксованому наборі запитань перед налаштуванням підказок. Зміна підказок рідко допомагає вирішити проблеми з низькою ефективністю пошуку.

Встановлення pgvector на Windows

Під час виконання етапу «Встановлення pgvector на Windows» спочатку запишіть контракт: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Такий перелік допомагає зберігати чесність пізніших змін у коді. Одночасно задокументуйте шлях успішного виконання та шлях відновлення. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Перед налаштуванням запитів вимірюйте рівень відтворення інформації на фіксованому наборі запитань. Зміна запитів рідко допомагає вирішити проблеми з низькою ефективністю пошуку.

set "PGROOT=C:\Program Files\PostgreSQL\18"
cd %TEMP%
git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git
cd pgvector
nmake /F Makefile.win
nmake /F Makefile.win install
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres --name pgvector pgvector/pgvector:pg18
conda install -c conda-forge pgvector

Створення таблиць у pgAdmin

Під час виконання етапу «Створення таблиць» у pgAdmin спочатку запишіть контракт: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допомагає зберігати чесність у подальших змінах коду. Віддавайте перевагу невеликим, тестованим одиницям коду перед об’ємними скриптами. Коли якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану послідовність дій. Перед налаштуванням запитів вимірюйте рівень відтворення інформації за фіксованим набором запитань. Часта зміна формулювань запитів рідко вирішує проблеми слабкого пошуку даних. Під час виконання етапу «Створення таблиць» у pgAdmin спочатку запишіть контракт: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допомагає зберігати чесність у подальших змінах коду. Поруч із функціональними результатами записуйте час виконання та витрати на обробку токенів чи запитів. Чітке бачення витрат заздалегідь запобігає несподіваним рахункам під час переходу від демо-середовищ до спільних.

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

-- Main chunks table with VECTOR(768) column
CREATE TABLE IF NOT EXISTS rag_chunks (
    id           UUID        PRIMARY KEY DEFAULT gen_random_uuid(),
    source       TEXT        NOT NULL,
    chunk_index  INTEGER     NOT NULL,
    content      TEXT        NOT NULL,
    file_hash    TEXT,
    ingested_at  TIMESTAMPTZ DEFAULT NOW(),
    embedding    vector(768)  -- pgvector native type
);

-- HNSW index for approximate cosine similarity search
CREATE INDEX IF NOT EXISTS idx_rag_chunks_embedding
    ON rag_chunks USING hnsw (embedding vector_cosine_ops);

-- Source filter index
CREATE INDEX IF NOT EXISTS idx_rag_chunks_source
    ON rag_chunks (source);

-- Staleness registry
CREATE TABLE IF NOT EXISTS rag_staleness (
    doc_name    TEXT        PRIMARY KEY,
    file_hash   TEXT        NOT NULL,
    chunk_count INTEGER,
    ingested_at TIMESTAMPTZ DEFAULT NOW(),
    version     INTEGER     DEFAULT 1
);

-- CDC chunk registry
CREATE TABLE IF NOT EXISTS rag_chunk_registry (
    doc_name    TEXT NOT NULL,
    chunk_hash  TEXT NOT NULL,
    chunk_id    TEXT NOT NULL,
    PRIMARY KEY (doc_name, chunk_hash)
);

-- Conversation sessions
CREATE TABLE IF NOT EXISTS rag_sessions (
    session_id  TEXT        PRIMARY KEY,
    created_at  TIMESTAMPTZ DEFAULT NOW(),
    updated_at  TIMESTAMPTZ DEFAULT NOW(),
    model       TEXT,
    embed_model TEXT,
    turn_count  INTEGER     DEFAULT 0
);

-- Conversation turns
CREATE TABLE IF NOT EXISTS rag_turns (
    id          SERIAL      PRIMARY KEY,
    session_id  TEXT        REFERENCES rag_sessions(session_id) ON DELETE CASCADE,
    role        TEXT        NOT NULL CHECK (role IN ('user','assistant')),
    content     TEXT        NOT NULL,
    sources     TEXT[],
    created_at  TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_rag_turns_session
    ON rag_turns (session_id, created_at);

Залежності Python

Етап залежностей Python працює найкраще, якщо його розглядати як вимірювану поверхню. Зафіксуйте один ідеальний варіант роботи, один випадок збою та примітку щодо скасування змін перед розширенням обсягу роботи. Тримайте конфігурацію окремо від коду додатку. Файли середовища, сховища конфіденційних даних та флаги функцій мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь код. Забезпечте фіксацію версії інтерпретатора та файлу блокування залежностей перед початком роботи з циклами. Розбіжності між ноутбуком та системою CI є найпоширенішою причиною прихованих збоїв у демонстраціях API.

uv add google-genai pypdf pgvector psycopg2-binary python-dotenv huggingface_hub

Налаштування підключення (Клітка 2)

Етап 2 «Налаштування з’єднання» працює найкраще, якщо його розглядати як вимірювану поверхню. Зафіксуйте один ідеальний варіант виконання, один випадок збою та примітку щодо скасування дій перед розширенням обсягу роботи. Документуйте як успішний, так і відновлювальний сценарії роботи. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Відокремте політику часткової обробки від політики отримання даних. Зміна однієї з них не повинна змушувати переписувати іншу при зміні показників якості.

import psycopg2
from pgvector.psycopg2 import register_vector

PG_HOST     = "localhost"
PG_PORT     = 5432
PG_DB       = "postgres"
PG_USER     = "postgres"
PG_PASSWORD = os.environ.get("PG_PASSWORD", "postgres")

def get_pg_conn():
    """Returns a fresh PostgreSQL connection with pgvector registered."""
    conn = psycopg2.connect(
        host=PG_HOST, port=PG_PORT,
        dbname=PG_DB, user=PG_USER, password=PG_PASSWORD
    )
    register_vector(conn)   # tells psycopg2 how to handle vector type
    return conn

Зберігання ембеддингів (Клітинка 6)

Етап 6 клітини зберігання ембеддингів працює найкраще, коли його розглядають як вимірювану поверхню. Зафіксуйте один ідеальний приклад роботи, один випадок збою та примітку щодо скасування змін перед розширенням обсягу завдань. Віддавайте перевагу невеликим, тестованим одиницям перед складними скриптами. Коли якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану послідовність дій. Розділяйте політику часткового оброблення даних та політику їх пошуку. Зміна однієї з них не повинна змушувати переписувати іншу при зміні показників якості.

import numpy as np

def store_in_postgres(chunks, embeddings, doc_name) -> int:
    conn = get_pg_conn()
    cur  = conn.cursor()
    for i, (chunk, emb) in enumerate(zip(chunks, embeddings)):
        cur.execute("""
            INSERT INTO rag_chunks (source, chunk_index, content, embedding)
            VALUES (%s, %s, %s, %s)
        """, (doc_name, i, chunk, np.array(emb)))  # np.array → pgvector handles serialization
    conn.commit()
    conn.close()
    return len(chunks)

Етап 6 клітини зберігання ембеддингів працює найкраще, коли його розглядають як вимірювану поверхню. Зафіксуйте один ідеальний приклад роботи, один випадок збою та примітку щодо скасування змін перед розширенням обсягу завдань. Записуйте час виконання та витрати на обробку токенів чи запитів разом із функціональними результатами. Відображення витрат на ранньому етапі запобігає несподіваним витратам під час переходу від демо-версії до спільних середовищ.

Пошук за допомогою косинусової схожості (Клітина 6, продовження)

Для етапу пошуку за допомогою косинусної схожості необхідно визначити вхідні дані, відповідального за цей крок та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити цей крок з відомої точки контролю, не намагаючись відгадати прихований стан. Конфігурацію слід зберігати окремо від коду додатку. Файли середовища, сховища конфіденційних даних та флаги функціоналу мають знаходитися в одному місці, яке оператори можуть перевірити, не читаючи весь код. Необхідно наводити конкретні уривки тексту, на яких ґрунтується відповідь. Без посилань оператори не зможуть відрізнити галюцинацію від проблем із індексуванням.

def retrieve_context(query: str) -> list[dict]:
    query_embedding = embed_query(query)
    conn = get_pg_conn()
    cur  = conn.cursor()

    cur.execute("""
        SELECT
            content,
            source,
            chunk_index,
            ingested_at,
            1 - (embedding <=> %s) AS cosine_score  -- <=> is cosine distance
        FROM rag_chunks
        ORDER BY embedding <=> %s                    -- sort ascending (smallest distance first)
        LIMIT %s
    """, (np.array(query_embedding), np.array(query_embedding), TOP_K))
    rows = cur.fetchall()
    conn.close()
    return [{"text": r[0], "source": r[1], "chunk_index": r[2],
             "ingested_at": str(r[3]) if r[3] else "",
             "score": round(float(r[4]), 4)} for r in rows]

CDC з pgvector — шаблон Upsert

Для CDC з етапом Upsert pgvector необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Необхідно документувати як шлях успішного виконання, так і шлях відновлення. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Наводьте уривки тексту, які фактично лежать в основі відповіді. Без посилань оператори не зможуть відрізнити галюцинації від проблем з індексуванням.

cur.execute("""
    INSERT INTO rag_staleness (doc_name, file_hash, chunk_count, version)
    VALUES (%s, %s, %s, 1)
    ON CONFLICT (doc_name) DO UPDATE SET
        file_hash   = EXCLUDED.file_hash,
        chunk_count = EXCLUDED.chunk_count,
        ingested_at = NOW(),
        version     = rag_staleness.version + 1;
""", (doc_name, file_hash, chunk_count))

Частина 2 — SQL Server 2025 (Native Vector)

Для другого етапу з SQL Server необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Краще використовувати невеликі, перевірювані одиниці коду замість об’ємних скриптів. Коли крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану структуру процесу. Наводьте ті уривки, які фактично лежать в основі відповіді. Без посилань оператори не зможуть відрізнити галюцинацію від проблем із індексуванням.

Чому SQL Server 2025 не потребує розширень

На етапі «Чому SQL Server 2025» необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Розглядайте цей етап як угоду між вхідними даними та перевіреними результатами. Призначте назви елементів, визначте критерії успіху та не допускайте беззвучного часткового завершення. Наводьте уривки тексту, які фактично лежать в основі відповіді. Без цитат оператори не зможуть відрізнити галюцинацію від проблем із індексуванням.

Налаштування в SSMS

На етапі налаштування в SSMS необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість знову виконати крок з відомої точки контролю, не намагаючись визначити прихований стан. Записуйте час виконання та витрати на обробку токенів чи запитів поруч із функціональними результатами. Чітке відображення витрат заздалегідь запобігає несподіваним рахункам під час переходу з демо-середовища у спільні. Наводьте конкретні уривки тексту, які лягли в основу відповіді. Без посилань оператори не зможуть відрізнити галюцинації від проблем з індексуванням.

-- Batch 1: Run this first — must commit before vector objects are recognized
ALTER DATABASE SCOPED CONFIGURATION SET PREVIEW_FEATURES = ON;
-- Batch 2: Create all tables
CREATE TABLE rag_chunks (
    id           INT              IDENTITY(1,1) PRIMARY KEY CLUSTERED,
    source       NVARCHAR(500)    NOT NULL,
    chunk_index  INT              NOT NULL,
    content      NVARCHAR(MAX)    NOT NULL,
    file_hash    NVARCHAR(64),
    ingested_at  DATETIME2        DEFAULT GETUTCDATE(),
    embedding    VECTOR(768)      -- native SQL Server 2025 type
);

CREATE INDEX idx_rag_source ON rag_chunks(source);

CREATE TABLE rag_staleness (
    doc_name    NVARCHAR(500)   PRIMARY KEY,
    file_hash   NVARCHAR(64)    NOT NULL,
    chunk_count INT,
    ingested_at DATETIME2       DEFAULT GETUTCDATE(),
    version     INT             DEFAULT 1
);

CREATE TABLE rag_chunk_registry (
    doc_name    NVARCHAR(500)   NOT NULL,
    chunk_hash  NVARCHAR(64)    NOT NULL,
    chunk_id    NVARCHAR(64)    NOT NULL,
    PRIMARY KEY (doc_name, chunk_hash)
);

CREATE TABLE rag_sessions (
    session_id  NVARCHAR(100)   PRIMARY KEY,
    created_at  DATETIME2       DEFAULT GETUTCDATE(),
    updated_at  DATETIME2       DEFAULT GETUTCDATE(),
    model       NVARCHAR(200),
    embed_model NVARCHAR(200),
    turn_count  INT             DEFAULT 0
);

CREATE TABLE rag_turns (
    id          INT             IDENTITY(1,1) PRIMARY KEY,
    session_id  NVARCHAR(100)   NOT NULL REFERENCES rag_sessions(session_id),
    role        NVARCHAR(20)    NOT NULL CHECK (role IN ('user','assistant')),
    content     NVARCHAR(MAX)   NOT NULL,
    sources     NVARCHAR(MAX),
    created_at  DATETIME2       DEFAULT GETUTCDATE()
);

CREATE INDEX idx_rag_turns_session ON rag_turns(session_id, created_at);
-- Batch 3: Must run AFTER Batch 2 commits
-- Cannot run inside a transaction — this is a known SQL Server 2025 preview constraint
CREATE VECTOR INDEX idx_rag_embedding
    ON rag_chunks(embedding)
    WITH (METRIC = 'COSINE');

Залежності Python

На етапі залежностей Python необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись визначити прихований стан. Конфігурацію слід тримати окремо від коду додатку. Файли середовища, сховища конфіденційних даних та флаги функціоналу мають знаходитися в одному місці, яке оператори можуть перевірити, не читаючи весь граф. Необхідно розділити процес створення клієнта від циклу обробки повідомлень, щоб можна було замінювати постачальників без переписування машини станів розмови.

uv add google-genai pypdf pyodbc python-dotenv huggingface_hub fpdf2

Налаштування з’єднання (Клітка 2)

На етапі налаштування з’єднання 2 визначте вхідні дані, відповідальну особу за крок та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись визначити прихований стан. Документуйте як успішний, так і відновлювальний сценарії роботи. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Наводьте уривки тексту, які фактично лежать в основі відповіді. Без посилань оператори не зможуть відрізнити галюцинації від проблем з індексуванням.

import pyodbc

SQL_SERVER   = "YOUR_SERVER_NAME"   # from SSMS title bar
SQL_DATABASE = "local_rag"
SQL_CONN_STR = (
    f"DRIVER={{ODBC Driver 17 for SQL Server}};"
    f"SERVER={SQL_SERVER};"
    f"DATABASE={SQL_DATABASE};"
    f"Trusted_Connection=yes;"    # Windows Authentication - no password needed
)

def get_conn():
    return pyodbc.connect(SQL_CONN_STR)

def get_conn_autocommit():
    """Required for CREATE/DROP VECTOR INDEX - cannot run inside a transaction."""
    return pyodbc.connect(SQL_CONN_STR, autocommit=True)

Зберігання ембеддингів (Клітина 6)

Для етапу 6 «Зберігання ембеддингів» необхідно визначити вхідні дані, відповідальну особу за крок та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Краще використовувати невеликі, перевірювані одиниці коду замість об’ємних скриптів. Коли крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану структуру обробки даних. Наводьте уривки тексту, які фактично лежать в основі відповіді. Без посилань оператори не зможуть відрізнити галюцинації від проблем з індексуванням. Для етапу 6 «Зберігання ембеддингів» необхідно визначити вхідні дані, відповідальну особу за крок та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Записуйте час виконання та витрати на токени або запити поруч із функціональними результатами. Чітке відображення витрат заздалегідь запобігає несподіваним рахункам під час переходу від демо-версії до спільного середовища.

Коментарі.

import json

def vec_to_json(embedding: list[float]) -> str:
    return json.dumps(embedding)   # '[0.12, 0.34, ...]'

def store_in_sqlserver(chunks, embeddings, doc_name) -> int:
    drop_vector_index()    # must drop before any INSERT

    conn = get_conn()
    cur  = conn.cursor()

    sql = """
        INSERT INTO rag_chunks (source, chunk_index, content, embedding)
        VALUES (?, ?, ?, CAST(? AS VECTOR(768)))
    """
    for i, (chunk, emb) in enumerate(zip(chunks, embeddings)):
        cur.setinputsizes([
            (_pyodbc.SQL_WVARCHAR, 500, 0),
            _pyodbc.SQL_INTEGER,
            (_pyodbc.SQL_WVARCHAR, 0,   0),
            (_pyodbc.SQL_VARCHAR,  0,   0),   # ← must be VARCHAR, not NTEXT
        ])
        cur.execute(sql, (doc_name, i, chunk, vec_to_json(emb)))

    conn.commit()
    conn.close()
    create_vector_index()   # recreate after all inserts
    return len(chunks)

Отримання даних за допомогою VECTOR_DISTANCE (Клітинка 6, продовження)

Під час роботи над етапом отримання даних за допомогою VECTORDISTANCE спочатку запишіть умови використання: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Такий перелік допомагає уникнути неочікуваних змін у коді. Зберігайте конфігурацію окремо від коду додатку. Файли середовища, сховища конфіденційних даних та флаги функцій мають знаходитися в одному місці, яке оператори можуть перевірити, не читаючи весь код. Вимірюйте рівень відтворення даних на фіксованому наборі запитань перед налаштуванням підказок. Зміна підказок рідко допомагає покращити якість отримання даних.

def retrieve_context(query: str) -> list[dict]:
    query_embedding = embed_query(query)
    query_vec_json  = vec_to_json(query_embedding)

    conn = get_conn()
    cur  = conn.cursor()

    cur.setinputsizes([(_pyodbc.SQL_VARCHAR, 0, 0)])   # force VARCHAR for vector param

    cur.execute(f"""
        SELECT TOP ({TOP_K})
            content, source, chunk_index, ingested_at,
            VECTOR_DISTANCE('cosine', embedding, CAST(? AS VECTOR(768))) AS distance
        FROM rag_chunks
        ORDER BY distance ASC;
    """, (query_vec_json,))

    rows = cur.fetchall()
    conn.close()
    return [{"text": r[0], "source": r[1], "chunk_index": r[2],
             "ingested_at": str(r[3]) if r[3] else "",
             "score": round(1 - float(r[4]), 4)} for r in rows]

Підводні камені векторних функцій SQL Server 2025 — усі, з якими ми стикалися

Під час роботи з етапом Vector у SQL Server 2025 спочатку запишіть контракт: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Такий перелік допомагає зберігати чесність пізніших змін у коді. Документуйте як шлях успішної роботи, так і шлях відновлення одночасно. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Вимірюйте рівень відтворення даних на фіксованому наборі запитань перед налаштуванням підказок. Зміна підказок рідко вирішує проблеми слабкої системи пошуку.

Проблема 1 — Невідомий тип об’єкта ‘VECTOR’ у операторі CREATE

Під час роботи над етапом „Gotcha 1 Unknown object“ спочатку запишіть умови взаємодії: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допомагає зберігати чесність пізніших змін у коді. Віддавайте перевагу невеликим, тестованим одиницям коду перед об’ємними скриптами. Коли якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на складну ієрархію операцій. Перед налаштуванням запитів вимірюйте рівень точності відповідей на фіксованому наборі запитань. Часта зміна формулювань запитів рідко допомагає покращити якість пошуку. Під час роботи над етапом „Gotcha 1 Unknown object“ спочатку запишіть умови взаємодії: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допомагає зберігати чесність пізніших змін у коді. Поруч із функціональними результатами записуйте час виконання та витрати на обробку токенів чи запитів. Чітке бачення витрат заздалегідь запобігає несподіваним витратам під час переходу з демо-середовища до спільних середовищ.

Msg 343, Level 15: Unknown object type 'VECTOR' used in CREATE, DROP, or ALTER statement.

Підступ 2 — Primарний ключ має бути одним стовпцем типу INT довжиною 4 байти

Етап формування primарного ключа у рамках «Підступу 2» працює найкраще, якщо його розглядати як вимірювану характеристику. Збережіть один ідеальний зразок даних, один випадок невдачі та примітки щодо скасування змін перед розширенням обсягу роботи. Тримайте конфігурацію окремо від коду додатку. Файли середовища, сховища конфіденційних даних та флаги функцій мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь код. Розділяйте політику часткового оброблення даних та політику їх отримання. Зміна однієї з них не повинна змушувати переписувати іншу при зміні показників якості.

Msg 42217: Table must have a clustered primary key on a single 4 byte INT column to create a vector index.
-- ❌ Does not work with vector index
id  UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID()

-- ✅ Required
id  INT IDENTITY(1,1) PRIMARY KEY CLUSTERED

Підступ 3 — Неможливо виконувати операції INSERT/DELETE/UPDATE, поки існує векторний індекс

The Gotcha 3 „Cannot INSERT stage“ працює найкраще, коли його розглядають як вимірювану поверхню. Запишіть один ідеальний приклад виконання, один випадок збою та примітки щодо скасування змін перед розширенням обсягу роботи. Документуйте як успішний, так і відновлювальний сценарії роботи. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Розділіть політику часткової обробки даних від політики їх отримання; зміна однієї не повинна змушувати до переписування іншої при зміні показників якості.

Msg 42231: Data modification statement failed because table 'rag_chunks' has a vector index on it.
def drop_vector_index():
    conn = get_conn_autocommit()   # autocommit required
    conn.cursor().execute("""
        IF EXISTS (
            SELECT 1 FROM sys.indexes
            WHERE name = 'idx_rag_embedding'
            AND object_id = OBJECT_ID('rag_chunks')
        )
        DROP INDEX idx_rag_embedding ON rag_chunks;
    """)
    conn.close()

def create_vector_index():
    conn = get_conn_autocommit()   # autocommit required
    conn.cursor().execute("""
        CREATE VECTOR INDEX idx_rag_embedding
            ON rag_chunks(embedding)
            WITH (METRIC = 'COSINE');
    """)
    conn.close()

Gotcha 4 — CREATE VECTOR INDEX не може виконуватися всередині транзакції

Етап Gotcha 4 CREATE VECTOR працює найкраще, якщо його розглядати як вимірювану поверхню. Збережіть один ідеальний зразок виконання, один випадок збою та примітку щодо скасування змін перед розширенням обсягу роботи. Віддавайте перевагу невеликим, тестованим одиницям перед величезними скриптами. Коли якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану послідовність дій. Розділяйте політику часткового оброблення даних та політику їх отримання. Зміна однієї з них не повинна змушувати переписувати іншу при зміні показників якості. Етап Gotcha 4 CREATE VECTOR працює найкраще, якщо його розглядати як вимірювану поверхню. Збережіть один ідеальний зразок виконання, один випадок збою та примітку щодо скасування змін перед розширенням обсягу роботи. Записуйте час виконання та витрати на токени чи запити поруч із функціональними результатами. Візуалізація витрат заздалегідь запобігає несподіваним рахункам під час переходу від демо-середовищ до спільних середовищ.

Msg 574: CREATE VECTOR INDEX statement cannot be used inside a user transaction.
# ❌ Fails — implicit transaction
conn = pyodbc.connect(SQL_CONN_STR)
conn.cursor().execute("CREATE VECTOR INDEX ...")

# ✅ Works - no transaction wrapper
conn = pyodbc.connect(SQL_CONN_STR, autocommit=True)
conn.cursor().execute("CREATE VECTOR INDEX ...")

Підступ 5 — пряма конвертація з ntext у вектор заборонена

На етапі прямої конвертації з Підступу 5 необхідно спочатку визначити вхідні дані, виконавця цього кроку та критерії завершення, перш ніж змінювати код. Оператори повинні мати можливість перезапустити цей крок з відомої точки контролю, не намагаючись визначити прихований стан. Конфігурацію слід тримати окремо від коду додатку. Файли середовища, сховища конфіденційних даних та флаги функціоналу мають знаходитися в одному місці, яке оператори можуть перевірити, не читаючи весь алгоритм. Необхідно наводити конкретні уривки тексту, на яких ґрунтується відповідь. Без посилань оператори не зможуть відрізнити галюцинацію від проблем з індексуванням.

Msg 529: Explicit conversion from data type ntext to vector is not allowed.
cur.setinputsizes([
    (_pyodbc.SQL_WVARCHAR, 500, 0),  # source — Unicode fine
    _pyodbc.SQL_INTEGER,              # chunk_index
    (_pyodbc.SQL_WVARCHAR, 0,   0),  # content — Unicode fine
    (_pyodbc.SQL_VARCHAR,  0,   0),  # embedding ← must be ASCII VARCHAR
])
cur.execute(sql, (doc_name, i, chunk, vec_to_json(emb)))

Підступ 6 — VECTOR_SEARCH не приймає замінники параметра ?

Для процесу Gotcha 6 VECTORSEARCH необхідно заздалегідь визначити вхідні дані, виконавця кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Необхідно документувати як шлях успішного виконання, так і шлях відновлення. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Указуйте конкретні уривки тексту, які лягли в основу відповіді. Без посилань оператори не зможуть відрізнити галюцинації від проблем з індексуванням.

Msg 102: Incorrect syntax near '('.
-- ❌ VECTOR_SEARCH with parameter placeholder — fails
FROM VECTOR_SEARCH(
    TABLE = rag_chunks USING VECTOR INDEX idx_rag_embedding,
    SIMILAR_TO = CAST(? AS VECTOR(768)),  -- pyodbc cannot pass ? here
    ...
)

-- ✅ VECTOR_DISTANCE - fully parameterized, GA, works perfectly
SELECT TOP (5)
    content,
    VECTOR_DISTANCE('cosine', embedding, CAST(? AS VECTOR(768))) AS distance
FROM rag_chunks
ORDER BY distance ASC;

PostgreSQL проти SQL Server — порівняння

На етапі порівняння PostgreSQL та SQL Server необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Краще використовувати невеликі, перевірювані одиниці коду замість об’ємних скриптів. Коли крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану структуру обробки даних. Наводьте ті уривки, які фактично лежать в основі відповіді. Без посилань оператори не зможуть відрізнити хибну інформацію від проблем з індексуванням.

Що додають обидві бази даних, чого немає у ChromaDB

На етапі «Що додають обидві бази даних» необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Розглядайте цей етап як контракт між вхідними даними та перевіреними результатами. Призначте назви елементів, визначте критерії успіху та не допускайте беззвучного часткового завершення. Наводьте уривки тексту, які фактично лежать в основі відповіді. Без цитат оператори не зможуть відрізнити галюцинацію від проблем із індексуванням.

-- PostgreSQL: Find chunks from documents belonging to a specific customer
SELECT c.content, 1 - (c.embedding <=> query_vec) AS score
FROM rag_chunks c
JOIN documents d ON d.filename = c.source
JOIN customers cu ON cu.id = d.customer_id
WHERE cu.tier = 'enterprise'
ORDER BY score DESC
LIMIT 5;
-- SQL Server: Same query, T-SQL syntax
SELECT TOP 5
    c.content,
    1 - VECTOR_DISTANCE('cosine', c.embedding, CAST(? AS VECTOR(768))) AS score
FROM rag_chunks c
JOIN documents d ON d.filename = c.source
JOIN customers cu ON cu.id = d.customer_id
WHERE cu.tier = 'enterprise'
ORDER BY score DESC;

Висновок

На етапі завершення необхідно визначити вхідні дані, відповідальну особу за крок та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись визначити прихований стан. Записуйте час виконання та витрати на токени або запити поруч із функціональними результатами. Чітке відображення витрат заздалегідь запобігає несподіваним рахункам під час переходу з демо-середовища у спільні середовища. Наводьте уривки тексту, які фактично лягли в основу відповіді. Без посилань оператори не зможуть відрізнити галюцинації від проблем з індексуванням.

Чек-лист для роботи