Галоўная / Артыкулы / Практычныя прытамакі: Сэмантычны пошук з PostgreSQL: Прагматызм перамага галас — большая частка

Практычныя прытамакі: Сэмантычны пошук з PostgreSQL: Прагматызм перамага галас — большая частка

Практычныя вядомасці пра Semantic Search з PostgreSQL: прагматызм перамагае ажыянне — у большасці случаяў: контракты, перакананні і месцы для коду для команд, якія выкарыстоўваюць гэты патэрн.

2581 слоў

Існавайце гэта як перапрацоўаны варыянт ідэй з артыкула «Semantic Search with PostgreSQL: Pragmatism Beats Hype – Most of the Time», адпрацаваны для аператараў: чыстыя этапы, арранжаваныя блакі коду і прыметкі з восстанавлення, якія застаюцца пасля перадачы заданняў. Этап Аналізу работае наякша, калі яго розглядаць як мерыябельную паверхню. Запісаце адна ідеальная транскрыпцыя, адзін прыклад неудачы і прыметкі з вярнення да пачатковага стану прычаму, перш чым расширваць масштаб задання. Валіце маленькія, тэставальныя елементы замест большых скрыптов. Калі якісьць крок не выйшае, прычына неудачы павінна вказываць на адную адпаведальнасць, а не на заплутаны ланцюг заданняў.

Семантычны пошук за адну фразу

Для семантычнага пошуку ў адной стадзіі неабходна пазначыць вхідныя даны, адпаведальнага за выкананне крока і критэрыя завершэння прычыні змены коду. Аперацыйныя працавнікі должны магчыма ўвайсці крок з вядомага пункта контролю, не спрабоўваючы здагадвацца пра схованы стан. Спрыяйце цій стадзіі як даговору межаў вхідных даных і перакананых выходных рэзультатаў. Дайце назвы артыфактам, пазначыце критэрыя успеху і не прымайце часткова завершаныя рэзультаты без паведамлення. Указуйце тыя часткі тексту, якія фактычна ляглі в основу адпаведнай адказы. Без цых цітатаў аперацыйныя працавнікі не зможуць адразніць галюцинацыю ад працягу індексавання.

Найважлівейшая рашэнне па дызайну: Модель эмбеддінга

Для стадіі «Найважлівэйшы дзейн» неабяжна ўскладніць вхідныя даны, адпаведальнага за крок і крэтырыя завершэння пры зміне коду. Аперацыйныя працавнікі должны магчымае перадзеяць крок з вядомай точкі контролю, не падозрываючы схованы стан. Запісваць час выконання і кост токена або запыту праза функцыйнальныя рэзултаты. Відразувая видаванне костаў запобегае неспакойным рахункам, калі траекторыя пераходзіць з дэмаверсіі ў спакульнаныя среды. Калі наступны крок — гэта код або вызов інструмента, лепш выкарыстоўваць структураваныя выходныя даны з паўнай пераверкай схемы, чым вольныя тэкстовыя форматы.

Інсталяванне pgvector

Для этапа установкі пакета pgvector неабяжна ўзначыць вхідныя даны, адпаведальнага за выкананне кроку і крэтыяры завершэння пры перадзеі коду. Аператары должны магчымаць перывыканне кроку з вядомай точкі контролю без неабяжнага вычыслення захаваных станоў. Канфігурацыю трэба знаходзіць за межамі коду прыкладнення. Файлы сераўнавання, хранільнікі секрэтных дадзеных і флагі функцый належыць у аднам месца, якое аператары можаць пераглядаць без неабяжнага чытання всіх элементаў системы. Паказваць неабяжна тые часткі тексту, якія фактычна лежаць у падставе адпаведнай адказы. Без ціх цитатаў аператары не можаць разлічыць галюцинацію ад працяглага прытуплення ў процесе індексавання. Для этапа установкі пакета pgvector неабяжна ўзначыць вхідныя даны, адпаведальнага за выкананне кроку і крэтыяры завершэння пры перадзеі коду. Аператары должны магчымаць перывыканне кроку з вядомай точкі контролю без неабяжнага вычыслення захаваных станоў. Лепш выбіраць маленькія, тэставаныя елементы працэсу замест большых скрыптав. Калі крок не выканаецца, прычына неудачы должна вказываць на адну конкрэтную адпаведальнасць, а не на заплутаны цэлы процес.

CREATE EXTENSION IF NOT EXISTS vector;

Схема: Зберагачыць фрагменты, а не толькі дакументы

Калі працуеце на стадыі «Зберагачыць фрагменты, а не толькі дакументы» схемы, спачатку запісайце умовы викорыстання: неабяжлівыя данні, сигнал успеху і тое, што выканаецца у разе частковага невыпання. Такі список пераконвае ў тым, што пазнейшыя змены коду будуць чыстымі. Спрыятлівае ставленне да гэтай стадыі як да угоды межаў між вхіднымі даннымі і перакананымі выходнымі рэзультатамі. Дайце назвы артыфактам, задаце критэрыя успеху і не падпішывайцеся на тыхое часткова завершэння задання. Перад налаштаваннем запитоў пераканайцеся ў рэверсным запам’ятаванні на фіксованым наборе запитанняў. Рэдкаколі змены запитоў можа выправіць слабую эфектывнасць адзыскання інформаціі.

CREATE TABLE documents (
    id          BIGSERIAL PRIMARY KEY,
    title       TEXT NOT NULL,
    source      TEXT,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE document_chunks (
    id           BIGSERIAL PRIMARY KEY,
    document_id  BIGINT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
    chunk_index  INTEGER NOT NULL,
    content      TEXT NOT NULL,
    embedding    vector(1536) NOT NULL,
    created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (document_id, chunk_index)
);

Інтэграцыя ў .NET з Npgsql

Калі працуеце над інтэграцыяй у NET з адміністрацыйным режымам, спачатку запісайце умовы взаўневыконання: неабходныя даны, сігнал пра успех і тое, што выходзіць на частым невялікім браке. Такі список дапамагае заліцварыць будучыя змены ў кодзе. Запісвайце час выконання і кост токена або запыту праза функцыйнальныя рэзултаты. Відразувая ведамасць пра косты, можна ухіліцца ад неспакою, калі працэс пераходзіць з дамоўнага режыму ў спакульнае сераўерное сэрвіса. Перад налаштаваннем запытаў пераканайцеся ў якосці адзысквання інформацыі на фіксованай сэтке запытаў. Частае зміненне запытаў рэдка калі-небудзь вылечвае слабую систему адзысквання дакументаў.

dotnet add package Npgsql
dotnet add package Pgvector
dotnet add package Pgvector.Dapper
using Npgsql;
using Pgvector;

var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
dataSourceBuilder.UseVector();

await using var dataSource = dataSourceBuilder.Build();

Стварэнне эмбеддынгаў з OpenAI

Калі працуеце над этапам «Стварэнне імбеддзінга з OpenAI», спачатку запісайце умовы: неабяжлівыя данні, сигнал успеху і тое, што выходзіць па частый неякосці. Такі список дапамагае залічыць пазнейшыя змены ў кодзе чыстымі. Зберагаюце настройкі праза код аплікацыі. Файлы сераўіса, хранільнікі секрэтных дадзенняў і флагі функцыйяй должны знаходзіцца ў аднам месцы, куды аператары можаюць адбавіць аудыт без неабяжлівага чытання всей структуры. Перад налаштаваннем запрошэнняў пераканайцеся ў рівеньцы вярнага адналёгчэння на фіксаваны набор запитаў. Частыя змены запрошэнняў рэдка калі выправляюць слабкую систему адналёгчэння. Калі працуеце над этапам «Стварэнне імбеддзінга з OpenAI», спачатку запісайце умовы: неабяжлівыя данні, сигнал успеху і тое, што выходзіць па частый неякосці. Такі список дапамагае залічыць пазнейшыя змены ў кодзе чыстымі. Валіце маленькія, тэставаныя елементы замест большых скрыптов. Калі якісь крок не выйшае, неякосць должна вказываць на адну адпаведальнасць, а не на заплутаны процес.

using OpenAI.Embeddings;

var embeddingClient = new EmbeddingClient("text-embedding-3-small", apiKey);

async Task<float[]> GetEmbeddingAsync(string text)
{
    var result = await embeddingClient.GenerateEmbeddingAsync(text);
    return result.Value.ToFloats().ToArray();
}

Зберагчыце частку дакумента

Этап зберагчыця частку дакумента працюе наўсёрэдзе, калі яго рассматраць як параметр, які можна вымерыць. Зафіксавайте адну ідеальную версію транскрыпцыі, адзін прыклад неудачы і запіс пра вярнэнне да пачатковага стану перш чым расширваць масштабы. Рассматрайце гэты этап як кантракт межа вхіднымі даннымі і перакананымі выходнымі рэзультатамі. Дайце назвы артыфактам, задаць критэрыя успеху і не падзеўляйцеся частым, непূরным выкананнем задачы. Раздзеліце правілы формавання частак і правілы ўзяць іх зборам. Змена адных не павінна прымусваць перапісванне іншых, калі змянююцца показнікі якосці.

async Task InsertChunkAsync(
    long documentId,
    int chunkIndex,
    string content,
    CancellationToken cancellationToken = default)
{
    var embedding = await GetEmbeddingAsync(content);
    var vector = new Vector(embedding);

    await using var conn = await dataSource.OpenConnectionAsync(cancellationToken);
    await using var cmd = new NpgsqlCommand("""
        INSERT INTO document_chunks (document_id, chunk_index, content, embedding)
        VALUES (@documentId, @chunkIndex, @content, @embedding)
        ON CONFLICT (document_id, chunk_index)
        DO UPDATE SET
            content = EXCLUDED.content,
            embedding = EXCLUDED.embedding
        """, conn);

    cmd.Parameters.AddWithValue("documentId", documentId);
    cmd.Parameters.AddWithValue("chunkIndex", chunkIndex);
    cmd.Parameters.AddWithValue("content", content);
    cmd.Parameters.AddWithValue("embedding", vector);

    await cmd.ExecuteNonQueryAsync(cancellationToken);
}

Семантычны пошук

Этап семантычнага пошуку працюе наяўней, калі яго розглядаць як мерыябельную плошчу. Зафіксавайце адны ідеальны прыклад, адзін прыклад неудачы і запіс пра вярнэнне да пачатковага стану перш чым расширваць сферу дзеяння. Запісвайце часы выканання і кост токеноў або запита праза функцыональныя рэзултаты. Відразлівасць костаў з самага пачатку запобегае неспакойным рахункам, калі процес пераходзіць з дэмаверсіі ў спяльныя сераўы. Раздзеліце правілы часткавання дадзеных ад правілаў ўзяць іх. Змена аднаго з іх не должна вымагаць перапісвы другога, калі зменяюцца паказнікі якосці.

public sealed record SearchResult(
    long DocumentId,
    long ChunkId,
    string Title,
    string Content,
    double Distance);

async Task<List<SearchResult>> SearchAsync(
    string query,
    int limit = 5,
    CancellationToken cancellationToken = default)
{
    var queryEmbedding = await GetEmbeddingAsync(query);
    var queryVector = new Vector(queryEmbedding);

    await using var conn = await dataSource.OpenConnectionAsync(cancellationToken);
    await using var cmd = new NpgsqlCommand("""
        SELECT d.id,
               c.id,
               d.title,
               c.content,
               c.embedding <=> @queryVector AS distance
        FROM document_chunks c
        JOIN documents d ON d.id = c.document_id
        ORDER BY c.embedding <=> @queryVector
        LIMIT @limit
        """, conn);

    cmd.Parameters.AddWithValue("queryVector", queryVector);
    cmd.Parameters.AddWithValue("limit", limit);

    var results = new List<SearchResult>();

    await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
    while (await reader.ReadAsync(cancellationToken))
    {
        results.Add(new SearchResult(
            DocumentId: reader.GetInt64(0),
            ChunkId: reader.GetInt64(1),
            Title: reader.GetString(2),
            Content: reader.GetString(3),
            Distance: reader.GetDouble(4)));
    }

    return results;
}

Операторы адстані

Этап Distance Operators працюе найкраща, калі яго розглядаць як вимерную паверхню. Зберагчыце адны ідеальны прыклад роботы, адзін кейс неудачы і прыметку па вярнэнні да пачатковага стану, перш чым расширваць сферу дзеяння. Храніце настройкі парадульна коду прыемлена. Файлы серавэра, базы секретных даных і флагі функцыйяў должны знаходзіцца ў аднам месцы, куда оператары можуць аудытаваць іх без неабяжнага чытання всіх данных. Раздзеляйце політыку часткавання дадзеных ад політыки ўзяць іх. Змена адной з яных не должна вымагаць перапісвання другой, калі зменяюцца паказнікі якасці. Этап Distance Operators працюе найкраща, калі яго розглядаць як вимерную паверхню. Зберагчыце адны ідеальны прыклад роботы, адзін кейс неудачы і прыметку па вярнэнню да пачатковага стану, перш чым расширваць сферу дзеяння. Вядомей выбірайце маленькія, тэставаныя елементы замест большых скрыптов. Калі якісь крок не выйшае, прычына неудачы должна вказываць на адну конкрэтную абавязку, а не на заплутаны ланцужок задач.

Стварэнне індекса з HNSW

Для стварэння індекса з павялом неабяжна ўзначыць вхідныя даны, адпаведальнага за крок і критэрыя завершэння пры перадзмене коду. Аперацыйныя працавнікі должны магчымае перайсці на гэты крок з вядомага пункта контролю, не спрабоўваючы здогадвацца пра схованы стан. Спрыяйце цэму павялу як кантракту межа вхіднымі данымі і перакананымі выходнымі рэзультатамі. Дайце назвы артыфактам, узначыце перакананні пра успех і не падтрымайце тыхя частковых завершэнняў. Цітуйце тыя часткі, якія фактычна лежалі в основе адпаведнай адказы. Без цітатаў аперацыйныя працавнікі не зможуць адразніць галюцинацыю ад прасоў у індексаванні.

CREATE INDEX document_chunks_embedding_hnsw_idx
ON document_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
SET hnsw.ef_search = 100;
BEGIN;
SET LOCAL hnsw.ef_search = 100;
SELECT ...
COMMIT;

IVFFlat як альтэрнатыва

Для IVFFlat як альтэрнатыўнага етапу, пры змяне коду, неабходна пазначыць вхідныя даны, адпаведальнага за этап і крэтыяры завершэння. Аператары должны магчымае перзапускать этап з вядомай точкі контролю, не падозрываючы прыхованы стан. Запісваць час выконання і вартасць токенаў або запытак праза функцыйнае рэзультат. Відкрытая візуабілізацыя вартасцей запобегае неспакоўным рахункам, калі парадокс пераходзіць з дэмавай версіі ў спяльныя сераўры. Указваць тые часткі тексту, якія фактычна лежалі в основе адпаведнай адказу. Без цых цітатаў аператары не можаюць разлічыць галюцинацію ад прасоўкі ў індэксаванні.

CREATE INDEX document_chunks_embedding_ivfflat_idx
ON document_chunks
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
SET ivfflat.probes = 10;

Фільтрацыя і гібрыдны пошук

Для стадіі фільтрацыі і гібрыднага пошуку неабяжна ўзначыць вхідныя даны, адпаведальнага за этап і крэтырыя завершэння пры перадзеіснавленні коду. Аператары должны магчымаць перзапуск этапа з вядомай точкі контролю, не спрабоўваючы здагадвацца пра схованы стан. Конфігурацыю трэба захаваць паза кодам прыкладнення. Файлы сераўіса, хранільнікі секрэтных дадзеных і флагі функцыйяў должны знаходзіцца ў аднам месцы, якое аператары можаць пераглядаць, не чытаючы весь код. Прыкладзіце аддзелы тексту, якія фактычна ляглі в основу адпаведнай адказы. Без цых адналежнасцей аператары не можаць розразліці галюцинацію ад прычыны, зв’язанай з індэксаванням. Для стадіі фільтрацыі і гібрыднага пошуку неабяжна ўзначыць вхідныя даны, адпаведальнага за этап і крэтырыя завершэння пры перадзеіснавленні коду. Аператары должны магчымаць перзапуск этапа з вядомай точкі контролю, не спрабоўваючы здагадвацца пра схованы стан. Валідзіце маленькія, тэставаныя елементы замест большых скрыптов. Калі этап не выканаецца, прычына нехтарактару должна вказываць на адну конкрэтную адпаведальнасць, а не на шмат прычын.

Паглядзіце на працэс адрабаткі.

SELECT d.id, d.title, c.content, c.embedding <=> @queryVector AS distance
FROM document_chunks c
JOIN documents d ON d.id = c.document_id
WHERE d.created_at > NOW() - INTERVAL '30 days'
  AND d.source = 'documentation'
ORDER BY c.embedding <=> @queryVector
LIMIT 10;
WITH semantic AS (
    SELECT c.id,
           row_number() OVER (ORDER BY c.embedding <=> @queryVector) AS semantic_rank
    FROM document_chunks c
    LIMIT 100
),
keyword AS (
    SELECT c.id,
           row_number() OVER (
               ORDER BY ts_rank_cd(
                   to_tsvector('english', c.content),
                   plainto_tsquery('english', @query)
               ) DESC
           ) AS keyword_rank
    FROM document_chunks c
    WHERE to_tsvector('english', c.content) @@ plainto_tsquery('english', @query)
    LIMIT 100
)
SELECT d.id AS document_id,
       c.id AS chunk_id,
       d.title,
       c.content,
       COALESCE(1.0 / (60 + semantic.semantic_rank), 0) +
       COALESCE(1.0 / (60 + keyword.keyword_rank), 0) AS score
FROM semantic
FULL OUTER JOIN keyword ON keyword.id = semantic.id
JOIN document_chunks c ON c.id = COALESCE(semantic.id, keyword.id)
JOIN documents d ON d.id = c.document_id
ORDER BY score DESC
LIMIT 10;

Калі pgvector ёсць правы выбар

Калі працуеце на стадыі «Калі pgvector», спачатку запісайце умовы: неабяжлівыя вхідныя даны, сигнал успеху і тое, што выканаецца у разы ўзельнага нявыполнення. Такі чарткі спамагаець залічыць пазнейшыя змены ў кодзе. Спрэцьвачайце гэтую стадію як угоду межаў вхідных даных і перакананых выходных рэзультатаў. Дайце назвы артыфактам, задаць тэсты на успех і не падпішывайцеся на мовчаны частковы выкананне. Замерьце рэгрэсію на фіксаванай сэтке запытаў прычым падбір прамптав. Частая змена прамптав рэдка калі вярнайце слабую структуру адшукаў.

Калі спецыяльны сховішча вектароў можа быць кращым

Калі працюеце над этапам «Калі ёсць спецыяльны вектар», спачатку запішыце умовы кантракту: неабяжлівыя даннэ, сигнал успеху і тое, што выканаецца у разы ў частковай нявыполненасці. Такі список контроля дапамагае залічыць зямнымі пазнейшыя змены коду. Запісвайце час выканання і кост токена або запытку празаўсёды разам з функцыйнальнымі рэзултатамі. Відразы коста з самага пачатку запобегае неспакоўным рахункам, калі працэс пераходзіць з дэмаверсіі ў спяльныя среды. Замерьце рэткість аднаходжання адпаведных дадзеных на фіксованым наборе запытак прычыну налаштавання прамптаў. Частае змена прамптаў рэдка калі-небудзь выправляе слабую эфектыўнасць аднаходжання інформацыі.

Полны прыклад ASP.NET Core

Калі працюеце над стадзіяй «Complete ASP NET Core», спачатку запісайце контракт: неабяжлівыя даны, сігнал успеху і тое, што выходзіць у разе частковага нявыпання. Такі список контроля дапамагае заліцьварыць пазнейшыя змены ў кодзе. Храніце настройкі параду ўнутры коду прыемленае. Файлы сераўіснага сэрвісу, хранальнікі секретных данных і флагі функцый належаць у аднам месца, куды аператары можаць адбавляць контроль без неабяжлівага чытання всей структуры. Перад налаштаваннем запытаў пераканайцеся ў рэкале на фіксаваным наборы запытаў. Частае змена запытаў рэдка калі вярнуе слабую эфектыўнасць адзысквання дакументаў.

app.MapGet("/search", async (
    string q,
    NpgsqlDataSource db,
    CancellationToken cancellationToken) =>
{
    var queryEmbedding = await GetEmbeddingAsync(q);
    var queryVector = new Vector(queryEmbedding);

    await using var conn = await db.OpenConnectionAsync(cancellationToken);
    await using var cmd = new NpgsqlCommand("""
        SELECT d.id,
               c.id,
               d.title,
               c.content,
               c.embedding <=> @v AS distance
        FROM document_chunks c
        JOIN documents d ON d.id = c.document_id
        ORDER BY c.embedding <=> @v
        LIMIT 5
        """, conn);

    cmd.Parameters.AddWithValue("v", queryVector);

    var results = new List<object>();

    await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
    while (await reader.ReadAsync(cancellationToken))
    {
        results.Add(new
        {
            documentId = reader.GetInt64(0),
            chunkId = reader.GetInt64(1),
            title = reader.GetString(2),
            content = reader.GetString(3),
            distance = reader.GetDouble(4)
        });
    }

    return Results.Ok(results);
});

Заключэнне

Калі працюеце над стадзіяй Заключэння, спачатку запісайце угоду: неабяжлівыя даны, сігнал успеху і тое, што выходзіць у разе частковага нявыпання. Такі список пераконвае ў тым, што пазнейшыя змены коду будуць адкрытымі. Дакументавайце як шлях успеху, так і шлях вярнення. Перапрыбуткі, людзкіе контралі і обработка некоректных паведамленняў ёсць часткай продукту, а не пазнейшым дапрацоўкам. Змяроўвайце ступень вярнення на фіксаваным наборе запитаў прычым падбір запросоў. Частае змена запросоў рэдка калі вярнее слабую систему пошуку.

Справакі

Калі працуеце над стадзіяй «Справакі», спачатку запісайце умовы працы: неабяжлівыя даны, сигнал успеху і тое, што выканаецца у разе частковага нявыполнення. Такі справак дапамагае заліцвачыць пазнейшыя змены коду.

Чек-ліст для эксплуатацыі

Калі працуеце над стадзіяй «Чек-ліст для эксплуатацыі», спачатку запісайце умовы працы: неабяжлівыя даны, сигнал успеху і тое, што выканаецца у разе частковага нявыполнення. Такі справак дапамагае заліцвачыць пазнейшыя змены коду.

Дакументавайце як правільны, так і альтернатыўны парадкі працы. Павторныя спробы, людзкі контроль і обработка некоректных дадзеных ёсць часткай продукту, а не пазнейшым дапрацоўкам.

Зьвярзіце рэкалі на фіксаванай сэтке запытаў прычымо да налагоджэння прамптав. Часта змена прамптав рэдка калі-небудзь выправляе слабую систему аднаходжэння інфармацыі.

Зафіксавайце версіі залежнасцей і запісаўце сумарны атрыбут зображэння, якое было выкаранае пад час дамоў. Возможнасць перапрыявлення рэзультатаў важлівей за традыцыйныя знання.

Валічыце маленькія, тэставаныя елементы замест большых скрыптав. Калі якісь крок не выйшае, адказ за гэтая неудача павінен быць прысвечаны адзіной адпаведальнасці, а не заплутанай лініи обробкі.

Зьвярзіце рэкалі на фіксаванай сэтке запытаў прычымо да налагоджэння прамптав. Часта змена прамптав рэдка калі-небудзь выправляе слабую систему аднаходжэння інфармацыі.

Перш чым пераводзіце стак, зафіксавайце версіі, запісаўце „золаты“ транскрыпт для критычнай лініі і паказваце крокі для адворачэння. У спакульнаваных средах патрэбны ліміты частоты, перакананні ў прыналежнасці і чысткі власнік для ротацыі секрэтных дадзеных. Валічыце простую надзяйнасць замест крэатыўных, адзінразовых дамоў.

Запіскі для пакета 5e57ac6d3d33: не класты ключі прадаўцоў у репазітарыю, задаць максымальны ліміт токена на сесію, а таксама зберагчы транскрыпціі праза фіксатуры адлічэння, каб пазнейшыя замены моделяў заставаліся пораўнанымі.