Галоўная / Артыкулы

Гібрыдны пошук у Postgres для медыцынскага асістэнта: схема, разбіўка, RRF і ацэнкі

Гібрыдны пошук у Postgres для медыцынскага асістэнта: разбіўка з улікам структуры, pgvector з RRF, бінарная квантызацыя, парогі ацэнак і належная ацэнка якасці.

Выдавец: REACTAPP.TOP19 хв чытання4283 слоў

Крыніцы: RAG in Postgres: What I Learned Building a Health Assistant Where Being Wrong Is Expensive

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

У ям ёсць запароджэнне. Або правільная адпаведзь на яго залежыць ад дакумента, перэрабленага падзеўнацца месяцэў тыму, які заменіў дакумент, напісаны ў 2011 годзе. Або два аўтарытэтныя публічныя істочнікі не пагадваюцца, і правільнае падходжэнне — сказаць пра гэта і поясніць, чаму гэта важна, а не выбраць „пераможця“. Або чыстая адпаведзь — „Я не ведаю, запытайце клінічнага спецыяліста“, але мадэль, навучаная быць корыстной, такім чынам пішае абавесцовы параграф.

Самэйшчынны вопыт — гэта тое месца, дзе фактычна знаходзіцца інфармацыя, і практычна жадна з работ, неабяжлівых для яго адпаведзення, не выканана ў модэлі мовы. Яна выканана ў слою адзысквання інфармацыі, які ў гэтым случае — гэта Postgres. Далей опісваецца гэты слой: схема, разбіўка на часткі, гібрыдны запіт на пошук, квантызацыя і прыемы ацэнкі, якія запобегаюць появе адхылэнняў у рэзультатах. Ён незалежны ад будзь-яга джэрага дадзэнняў, і гэтыя ідэі можна застосавіць у будзь-ям корпусе, дзе некарэктны адпаведзення мае вялікія наследкі.

Чатыры спосабы, якімі медычныі тэксты перамагаюць простыя методы RAG

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

  1. Значэнне не мае формы абзаца. У монаграфіі пра лекарство ёсць окольныя раздзелы для протыпаказванняў, адночаснага застаўлення і дозавання. Калі разбіць яе на вікна з 512 токанамі, частка з іх будзе мячыць палову інформацыю пра протыпаказвання і палову — пра табліцу дозавання. Такая частка вельмі добра знаходзіцца, па косай схожасці, але з нізкім значэннем правды. Гэта найбольша небяспечная працэздатная прычына, таму што паказнікі пошуку все равно выглядаюць нормальна.
  • Словнаўкі ўжоў двухвідовы. Карыстувальнікі опісваюць бол у жоўчніку пасля еўхаліі, тады як у корпусе тэкста пішацца „боль у эпігастрыі, пасляўжоўчніковы“. Семантычны пошук добра заполняе гэты прыемак. Але карыстувальнікі таксама вводзяць точныя ідэнтыфікаторы, такія як назва прыемлівага засобу, код чыя-небудзь сульфату, і ў такім случае ўжоўкі працуюць гэрш за індэкс ключоўых слоў. Мадэль, якая ставіць метопролол і метопролол сукцінат у суседзкія пункты, спакойна дапамагае людзям робіць аблуды. Патрэбны якія-то з гэтых спосабаў адналёгчэння пошуку, іх трэба з’едыніць, а не выбіраць аднаго з іх.
  • Няўзабавнасць — гэта семантыка. У іншых сферах стары дакумент ёсць дакументам з нейкімі недагодамі. У сферы здароў’я гэта можа быць актываю шкодай, адколі рэкамендаціі пераглядаюцца самэлькі таму, што паканальная версія была некорэктная. Няўзабавнасць не можа быць фактарам, які прыменяецца толькі ў канцы; яна павінна быць праднае полем, пра якое разам размышляюць як слой адналёгчэння пошуку, так і слой стварэння адпаведзі.
  • Адказы маюць правовы фармат. Дзеяныя запытанні трэба направляць, а не адпаведна адпавядаць: сімптамы надзвычайных сітуацыяў, рашэнні па дозаванні для конкрэтнага чалавека і ўсё, што можа служыць дыягназам. Для іх патрэбны фіксаваныя, адправаныя правіламі шляхі, якія не можуць змяніцца залежна ад таго, як саме адчувае ся мадэль.
  • Кожны з гэтых прыкладаў ўскладнюецца проблемай шара выкарыстоўвання даных, што прыводзіць да нудных, але найболей значных рашэнняў.

    Чаму Postgres, а не вектарная база дадзеных

    Гэты выбор трэба чытра адзначыць, адтолькі ў падходзе тыпу „вам не патрэбна вектарная база дадзеных“ з’явілася недбаласць, і існуюць рэальныя сцэны, калі яна дакладна неабходна.

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

    Postgres аддае адну межу транзакцыі. Гэта здаецца дробным моментам, але так не ёсць. У корпусе дакументаў, які зміняецца, архітэктура з двума сістэмамі караеся значнымі наследкамі:

    • Калі дагэнны дакумент перасматрываецца, яго старыя часткі павінны быць выдалены, новыя — даданы, эмбедынгі — перыякшаны, а метаданы — апошнія, і ўсё гэта трэба здзейсніць атамарна. У разе наявнасці окрэмай базы дадзеных вектараў атамарнасць не існуе. У некалькіх случаях індэкс ключоў і індэкс вектараў не пагоджуюцца ў тым, што існуе, і асістэнт наводзіць дакумент, які больш не адпавядае таму, для чаго ён быў наведаны.
    • Фільтрацыя па ліцэнцыі, ўповажненых органах, адпаведным аўтарытэту, даце перазробкі і мове, а пасля класыфікацыя па вектарнай аддаленасці — гэта клаза WHERE у Postgres. У двух сістэмах гэта можа значыць або рэпліку метаданых, якая падтрымлівае сінхронізацыю, або процедуру падчас якой спачатку запрашваецца занадто многа дадзеных, а пасля фільтрацыі тых, што патрабаваліся, вяртаецца менша колькасць рэзультатаў.
    • З’ясаванне, чаму адпаведны адказ быў запрашаны шэсць тыдзён назад, — гэта операцыя з’еднання.

    Практычны ліміт масштабавання існуе, але ён вышы, чым думаюць людзі: з дапамою належна настроеныя інстанцыі з сучасным pgvector можна легка обрабоцваць дзесяткі мільйонаў частак. Розумны прынцып — пачынаць з Postgres і спрыяць пазнейшай міграцыі як досягненню, а не як цэлі праектавання. Праект, які ніколі не досягае гэтага ліміту, ухіляецца ад усіх проблем, зв’язаных з распадзеленымі сістэмамі. Праект, які яго досягае, мае готовы продукт, рэальныя логі запытоў і чыстае уявленне пра сваія патрэбы.

    Версіі маюць значэнне. У версіі pgvector 0.7.0 былі адгукнутыя halfvec, sparsevec, індэксаванне на аснове бітов і функцыя binary_quantize. У версіі 0.8.0 былі дадзены можлівасці ітэратыўнага сканавання індэксаў, якія спрыяюць рашэнню проблемы фільтрацыі, опісанай ніжэй, а версіі 0.8.x дадалі падтрымку Postgres 18. У версіі 0.6.x большая частка таго, што будзе напісана далей, не будзе працаваць так, як задумана.

    Схема

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

    CREATE EXTENSION IF NOT EXISTS vector;
    CREATE EXTENSION IF NOT EXISTS pg_trgm;
    -- Where knowledge comes from, and how much we trust it.
    CREATE TABLE source (
      id bigserial PRIMARY KEY,
      slug text UNIQUE NOT NULL,
      license text NOT NULL, -- redistribution rules are a query concern
      authority smallint NOT NULL CHECK (authority BETWEEN 1 AND 5),
      base_url text
    );
    -- One row per upstream document, at whatever granularity upstream uses.
    CREATE TABLE document (
      id bigserial PRIMARY KEY,
      source_id bigint NOT NULL REFERENCES source(id),
      external_id text NOT NULL, -- the stable ID assigned upstream
      title text NOT NULL,
      url text,
      published_at date,
      revised_at date, -- recency is a column, not a vibe
      superseded_by bigint REFERENCES document(id),
      lang text NOT NULL DEFAULT 'en',
      raw_hash bytea NOT NULL, -- skip work on unchanged re-ingests
      meta jsonb NOT NULL DEFAULT '{}',
      UNIQUE (source_id, external_id)
    );
    -- Retrieval unit. Note heading_path: structure survives chunking.
    CREATE TABLE chunk (
      id bigserial PRIMARY KEY,
      document_id bigint NOT NULL REFERENCES document(id) ON DELETE CASCADE,
      ord int NOT NULL,
      heading_path text[], -- {Contraindications,"Renal impairment"}
      body text NOT NULL,
      body_tsv tsvector GENERATED ALWAYS AS
        (to_tsvector('english', body)) STORED,
      token_count int NOT NULL,
      content_hash bytea NOT NULL,
      lang text NOT NULL, -- denormalized from document
      is_current boolean NOT NULL, -- denormalized: superseded_by IS NULL
      UNIQUE (document_id, ord)
    );
    CREATE INDEX chunk_tsv_idx ON chunk USING gin (body_tsv);
    CREATE INDEX chunk_trgm_idx ON chunk USING gin (body gin_trgm_ops);
    -- Embeddings are versioned by model. This is not optional.
    -- lang and is_current are DENORMALIZED from document -- see below.
    CREATE TABLE embedding (
      chunk_id bigint NOT NULL REFERENCES chunk(id) ON DELETE CASCADE,
      model text NOT NULL,
      vec halfvec(1536) NOT NULL,
      lang text NOT NULL,
      is_current boolean NOT NULL,
      PRIMARY KEY (chunk_id, model)
    );
    

    Чатыры элементы являюцца несучымі.

    Эмбеддынгі прыязначаюцца за часткамі і мадэлямі. Мадэлі эмбеддынга могу змяніцца. Калі такое адбуваецца, обе версіі павінны працаваць адразу, ўблізу, каб іх можна было тэставаць у рэжыме A/B на рэальным трафіце і заменіць без перыоду тыпавання. З самага пачатку размясціце мадэль у галоўным ключы і створыце частковы індэкс для кожной мадэлі, каб кожная застаўалася компактной:

    CREATE INDEX embedding_hnsw_v3_en
      ON embedding USING hnsw (vec halfvec_cosine_ops)
      WITH (m = 16, ef_construction = 128)
      WHERE model = 'emb-v3' AND lang = 'en' AND is_current;
    

    Язык і чырвоны паўерак спецыяльна копіююцца як у чак, так і ў embedding. Гэта ўзначальна тае месца, дзе нормалізацыя была нарочна нарушана, і прычыну гэтага знадобілася няякша дліжынная час, каб яе зрозумець. У Postgres прэдыкат частковага індэкса можа згадваць толькі столбцы табліцы, якая індэксуецца. Індэкс на embedding не можа выкарыстоўваць WHERE document.is_current; Postgres абоўсюды адхоўляе такі запіт, включаючы падзапыты. Ёнколі трэба ўбудаваць актуальныя фільтры ў вектарны індэкс, што неабходна з прычын, пасвячаных ніжэй у раздзеле пра пастку фільтраў, тыя столбцы павінны фізычна знаходзіцца ў рэядку embedding.

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

    Гэта таксама не ёсць безкоштовным. Змена флага выкарыстоўваецца для выдалення гэтых рэядкаў з обох частковых індэксаў HNSW, а індэксы HNSW звяртаюць свободны прастор толькі пад час чысткі, таму частыя змены значыць, што трэба сочыць за збільшэнням розміру індэксаў.

    Колонка вектара — гэта halfvec, а не вектар. Зберагачча 16-бітных чысел з плаваючай крапкою зменшуе первачную велічыну дадзеных з 6,152 байтаў да 3,076 байтаў пры 1,536 вымерах, а індэкс HNSW скарочваецца ўсё бол, таму што маленькія туплы збіраюцца ўжо ўжо ў стораніцы. Для выкарыстоўвання дадзеных тут патрэбна толькі апошніяя 50 кандыдатаў для пераранжавання, і пад час тэставання розніца ў рэкале ўсталявалася ў межах шуму. Да таго, halfvec можа быць індэксаваны пры 4,000 вымерах, тады як звычны вектар — толькі пры 2,000, што дапамагае з большымі мадэлямі эмбедынгаў.

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

    Чанкаванне — гэта там, дзе знаходзіцца найбольшая цэннасць

    Калі існуе якісь урокі: функцыя нарэзкі тэксту мае большое значэнне, чым мадэль адкуплівання. Крашчая мадэль дае незначныя прыбавкі ў рэзультатах. Функцыя нарэзкі, якая адзначае структуру, дапамагае розлічыць правільнага асістэнта ад таго, які помылкава, але з упэўненасцю. Даплываюць тры правілы.

    Спачатку структура, потым размер. Разбіваеце кожны документ на дрэво загалоў і трэба, каб кожны фрагмент належаў да аднаго верхньага раздзела. Калі раздзел меньшы за паказаны размер, залішыце яго такім, а не спаіваеце з суседнім: абмежэнне ў 60 токэнаў значыць толькі, што гэта корыстней, чым фрагмент з 500 токэнаў, дзе ён тоне. Абмежэнні па токэнах — гэта запасны варыянт, калі структура не падходзіць, а не главнае правіле.

    Ніколі не дзеркаліюйце табліцу. Табліцы з медычнымі данымі зазвычай являюцься сеткамі доз, матрыцамі взаімадзеяў чыста крытэрамі адпаведнасці, тэкстам, у яком палова рэядоў мае меньшую цэннасць, чым ўсё жаданае. Калі якаясь частка перакрое бюджэт, раздзеліце яе на групы рэядоў, павтарыце заголовак у кожной групе і прыбліжна перакладзіце рэзультат у рэчаныя выразы пры ўбудовванні. Рэчаны выраз на кшталт «Возраст 6–11 гадоў: 5 мг адназначна ў дзень» убудовваецца набліжна лепш, чым сетка, раздзеланая сімволамі «|».

    Перад умяшчэннем паказвайце кантэкст. Умяшчаны тэкст не павінен быць суровым фрагментам, а фрагментам, у якіх восстанавленае ўсе іхнё паходжэнне: радок заголовка, у яком называецца крыніцы, назва дакумента і дата перазробкі, пасля чаго — шлях да роздзела, напрыклад «Протыпаказанні», потым «Парадкі функцій нэры», а пасля — сам тэкст. Умяшчайце гэта, а суровы тэкст залішайце для адразы і цітатаў. Фрагмент, які чытаецца як «Не выкорыстоўваць у паціентах з зніжаным клірэнсам», сам по сабе майжэ нічога не значыць, адколі ён не называе ні леку, ні стану, ні кантэксту. Ён стае доступным толькі тады, калі заголовак паведаміць модэлю, пра шта йдзе мова. Гэяе адна змяна павысіла рывень запам’ятовання больш, чым будзь-якія іншыя спробы замены модэля, і ўсё, што ёй касаецца, — гэта толькі кантэйнеры за час індэксавання.

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

    BEGIN;
      DELETE FROM chunk WHERE document_id = $1; -- cascades to embedding
      -- insert new chunks + embeddings
    COMMIT;
    

    Гэтыя аператыўнія ёсць абоўсюдным адказам на тое, чаму гэта знаходзіцца ў Postgres.

    Гібрыдны спосаб адналёгчэння з аднаёднаннем взаімных рангаў

    Кожны запіт адбывае семантычны пошук і пошук за ключовымі словамі па-свойму, а пасля з’едынае іх па рангу, а не па чыстай оцэнцы. Аднаёднанне оцэнак не работае: косінусава адстань і ts_rank_cd знаходзяцца на незв’язаных шкалах, не ўзаемна пораўнаныя между запытамі, а будзь-якія вагі, прызначаныя для аднаго набора запытов, будуць занадта адпаведныя для яго. Аднаёднанне взаімных рангаў ужо цэлкам абходзіце гэта, прыглядаючыся толькі да пазіцый.

    WITH params AS (
      SELECT $1::halfvec(1536) AS qvec,
             $2::text AS qtext,
             60::int AS rrf_k, -- damping constant
             50::int AS pool
    ),
    -- Two rules in this CTE, both non-obvious, both explained below:
    -- 1. qvec comes from a SCALAR subquery, never a joined relation.
    -- 2. Filters live INSIDE each arm, not in the outer query.
    semantic AS (
      SELECT chunk_id, row_number() OVER (ORDER BY dist) AS rank
      FROM (
        SELECT e.chunk_id,
               e.vec <=> (SELECT qvec FROM params) AS dist
        FROM embedding e
        WHERE e.model = 'emb-v3'
          AND e.lang = 'en'
          AND e.is_current
        ORDER BY e.vec <=> (SELECT qvec FROM params)
        LIMIT (SELECT pool FROM params)
      ) s
    ),
    lexical AS (
      SELECT chunk_id, row_number() OVER (ORDER BY score DESC) AS rank
      FROM (
        SELECT c.id AS chunk_id,
               ts_rank_cd(c.body_tsv,
                          websearch_to_tsquery('english',
                            (SELECT qtext FROM params))) AS score
        FROM chunk c
        WHERE c.body_tsv @@ websearch_to_tsquery('english',
                              (SELECT qtext FROM params))
          AND c.lang = 'en' -- same predicate as the semantic arm,
          AND c.is_current -- from the same denormalized source
        ORDER BY score DESC
        LIMIT (SELECT pool FROM params)
      ) l
    ),
    fused AS (
      SELECT chunk_id,
             SUM(1.0 / ((SELECT rrf_k FROM params) + rank)) AS score
      FROM (
        SELECT chunk_id, rank FROM semantic
        UNION ALL
        SELECT chunk_id, rank FROM lexical
      ) u
      GROUP BY chunk_id
    )
    SELECT c.id, d.title, d.url, d.revised_at, s.slug, s.authority,
           c.heading_path, c.body, f.score
    FROM fused f
    JOIN chunk c ON c.id = f.chunk_id
    JOIN document d ON d.id = c.document_id
    JOIN source s ON s.id = d.source_id
    ORDER BY f.score DESC, s.authority DESC, d.revised_at DESC NULLS LAST
    LIMIT 20;
    

    Фрагмент, які выступаець трэцім па значэнні і сьпявама па ключавым словам, парадуе таму, дзе іншы выступае толькі першым па аднам показніку. Так і задумана, каліж два незалежныя сігналы, якія падтверджваюць адно, маюць большую цэнна, чым адзін сігнал, які крычыць. websearch_to_tsquery ёсць розумны парсер, каліж ён можа працаваць з рэальным вводам корыстніка, фразамі ў лапках і выключэнням за дапамою знака мінус, не выклікаючы проблем з некорэктным тэкстам. У гэтым случае ts_rank_cd парадуе над звычным ts_rank, каліж ўжытак ўважаецца ўражынка паверхносты покрыцця, якая спрыяе тэрмінам, якія з’являюцца падалей адзін ад другога, што падходзіць для запытанняў, якія аб’еднваюць калькія панявы.

    Две характэрыстыкі гэтага запытання выглядаюць як выборы стылю, але такімі не ўсё.

    Першая пастка: вектар запытання павінен быць скалярным падзапытаннем

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

    Інтуўітывная версія выбірае элементы з таблы вбудоввання і зв’язку параметраў разам, пасля чаго сортавае іх за адстанню да столбца параметраў. Яна працюе добра, ёсць правільной і вяртае правыя радкі, самэ гэта і ўзрабатвае ёй апрацоўку коду. Але проблема тая, што планавальнік можа выкарыстоўваць індэкс найбліжэйшага суседа толькі тады, калі оператар ORDER BY порывнюе індэксаваны столбец з чымось фіксаваным для всьго сканування. Значэння, якія прыносіцца праз з’еднання, не падпадаюць пад ціе правіла, таму не існуе ніякога сортаванага шляху: Postgres чытае кожны радок і сортавае іх.

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

    Пераканаўцеся, а не прымніце. Абясніце запіт на выкарыстоўванне дадзеных у CI і паставіце тэчка, што план выконвае скан індэкса HNSW з рэгламентам Order By. Секвэнцыйны скан, за якім следуе сортаванне, означае, што вектарны індэкс ёсць толькі дэкоратыўным. Тэст, які не праходзіць пад час стварэння, мае значэнне, адколькі добра намераваная перэработка можа знову стварыць проблэму, не даючы жадных некорэктных адпаведзей, толькі медленных у рэальных умовах працы.

    Вторая пастка: фільтрацыя пасля LIMIT

    Фільтры – а саме тэкушчы стан, мова і адсутнасць замены – павінны знаходзіцца ў падзапытэ кожнай часткі, прычым раней за параметр LIMIT. У першай версіі іх паставілі ў зовнішні запыт, у якім яны знаходзяцца на явным, але некорэктным месцы.

    HNSW вяртае толькі апрыксіматычныя найбліжэйшыя суседы. Выбіраюць 50 элементаў па расстою, пасля чаго яны фільтруюцца, і, можа, застаецца толькі 12. Колькасць 50 элементаў таямніча зменшылася да 12, а метод злучэння рангаў цяпер працуе з наборам, занятым элементамі, якія апэндаваныя да катэгорыі адхілення, і ў падсумку запэчатлеванне LIMIT 20 таямніча вяртае 9 рэзультатаў. Адхіленні не фіксуюцца, і нічога пра гэта няма ў логах. Кожны аддзінны рэзультат застаецца правдападобным, таму ніхто не падае звестку пра баг. Асістэнт проста стае адносна гіршым у запытаннях, дзе корпус фільтруецца неравнамерна, а ў корпусе з медычным тэкстам гэта значыць запытанні пра няякша перасмотрэны матэрыял.

    Тры способы выправлення, па спадарожнасці:

    1. Створыце фільтр у частковы індэкс. Гэта прычына, чаму мова і тэкушчы флаг былі копіюваны ў радок укладкі. Калі прэдыкат індэкса становіцца роўным клазу WHERE запытку, індэкс ужо містыць толькі фільтруваныя радкі, і проблема знікае. Гэты падход падходзіць для вімяроў з нізкай кардынальнасцю і медленнымі змянамі, такіх як мова, валюта або ранг крыніцы, але не падходзіць для таго, што мае багато значэнняў, адтуды ўсё будзе значыць падтрымкі ста індэксаў.
    2. Ітэратыўныя сканаванні індэксаў (pgvector 0.8.0 і новейшыя версіі) таксама падтрымляюць фільтры, якія занадта дынамічныя, ўжо не можна ўключыць у сам індэкс:
    SET hnsw.iterative_scan = relaxed_order;
    SET hnsw.max_scan_tuples = 100000; -- default is 20000
    SET hnsw.scan_mem_multiplier = 4; -- raise if the above is not enough
    

    Калі гэта ўвёшана, індэкс продовжае шукаць новыя кандыдаты, пакуль не будзе достатнько тых, якія працяюць па фільтру. Існуюць два ліміты: фільтр павінен знаходзіцца на там самым рэвэлі-рэвэлі, чым скануванне індэкса, інакш нічога не змяніцца, а скануванне таксама мае максімальную колькасць рэзультатаў, таму ён можа вернуць менш рэкордаў, чым было запрасана. Гэта адчынена проблему, але не усунула яе. Выбіранне параметра relaxed_order замест strict_order павышае канцэнтрацыю рэзультатаў за рахунак таго, што рэзультаты не будуць абоўсюды сортаваныя па адстані, што ёсць дапустым, калі пасля гэтага выкорыстоўваецца пракцэс перэсортавання.

    Швайнае адчынэнне за дапамой бінарнай квантызаціі

    У масштабных сістэмах найэфектывнейшы спосаб — дваэтапнае адчынэнне: спачатку выбіраюцься кандыдаты з бінарнага індэкса па показніку адстані Хаммінга, а пасля — адбываецца точнае перэсортавання гэтага скорачанага спіску.

    CREATE INDEX embedding_bit_idx ON embedding
      USING hnsw ((binary_quantize(vec)::bit(1536)) bit_hamming_ops)
      WHERE model = 'emb-v3' AND lang = 'en' AND is_current;
    SELECT chunk_id
    FROM (
      SELECT chunk_id, vec
      FROM embedding
      WHERE model = 'emb-v3' AND lang = 'en' AND is_current
      ORDER BY binary_quantize(vec)::bit(1536)
               <~> binary_quantize($1::halfvec(1536))
      LIMIT 400
    ) candidates
    ORDER BY vec <=> $1::halfvec(1536)
    LIMIT 50;
    

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

    Важна тачна вялічына заўтрошчэння, каліж тым у Інтэрнэце ёсць множыцтва варыянтав. У 1 536 вымерах вектар з бітамі займае апошнія 200 байт, тады як для halfvec — прыблізна 3 КБ, тое ў 16 разоў менш за сырыя даны. Цыфра 32 разы, якая цытуецца паўсюды, паводзіцца пра вектары з паўной точнасцю, якія не вікорыстоўваюцца ў гэтым схэме.

    Індэкс не скорачваецца у 16 разоў, і прычына гэтага ёсць інструктыўная. Кожны тупл HNSW несе фіксаваны надтэкст у размерэ каля кількох сот калікоў, лінкі графа і заголовак тупла, незалежна ад пэйлоаду. Працуючы з 3 КБ дадзенняў у фармаце halfvec, гэты надтэкст ёст кантрольны элемент; працуючы з 200-байтовым векторам бітоў, гэты надтэкст перакрывае сам пэйлоад. З урахоўваннем вітараў, якія выступаюць у рэзультаце пакавання дадзенняў на сторанцы, рэальны ступень стыснення застаецца значна ніжэй за первісны парадакт. На гэтых дадзеннях бінарны індэкс выйшаў прыблізна у 4–5 разоў меньшы за індэкс у фармаце halfvec. Гэта яшчэ і ўстанавлівае, чы індэкс паспеліў бы ў RAM, што і ёсць галоўной метай, але тыя, хто плануе 32-кратнае стыснення, будуць непрыятна здзівлены. Перад планаваннем магчымасцей трэба пераканацца за дапамою pg_relation_size, а таксама вжываць рэалістычныя векторы: адна з перапытак забрала паў дня, таму што сінтэтычныя векторы былі ўсе паазначаныя як паазітыўныя, функцыя binary_quantize стварыла векторы, усе элементы якога былі паазітыўныя, а Postgres стысніў усе дадзеныя да нуля.

    Усе іншае — гэта стандартныя процедуры, якія добра задокументаваны. Перад ствароўкай індэксаў HNSW даўце больш прыемлівую вялічыну для параметра maintenance_work_mem, інакша працэс запісу на дыск займе гадзіны, а не хвіліны. Выберыце значэнне hnsw.ef_search па кожнам запіту: ад 100 да 200, калі помылкі маюць вялікія наследкі, адносна 40 для функціі автодаполнення. Ствараюце індэксы для працы ў працэсе одночасна. Тэставаць трэба на савойных даных, адтолькі ў публікуемых показніках рэкаліі рэдка калі есць правдзіва інформацыя.

    Пераранжаванне і рашыненне не вяртаць нічога

    Алгорытм Fusion вяртае прыемныя 20 найлепшых рэзультатаў, а не толькі 5, а запіт можа падтрымаць толькі 5 кандыдатаў. Алгорытм пераранжавання на адной базе, які чытае запіт і кандыдатаў як пару заместа порэванняя збераглых вектараў, даўае найбольшы падых у якосці пасля гібрыднага пошуку. Ён медленны, таму можа рассмотрэць толькі 20 кандыдатаў, а не мільйон.

    Што частаюць працяваць: трэба выкорыстоўваць рэйтынгі сістэмы пераранжавання як абсалютны порог, а не проста для сортавання. Калі найкрашчы кандыдат падпадае ніж пад гэты порог, правы выхад не будзе найкрашчым з усіх можлівых варыянтав. Гэта означае, што у корпусе дакументаў няма ніч практычнага на даную тэму.

    Самэль тут большасць дапаможнікаў у сферы здароў’я паспяшаюцца, і рашэннем яе ёсць выбор продукту, які ўжо рэалізаваны ў кодзе. Сістэма, створаная для адпаведзення на усё, будзе выдумваць адпаведзенні на рэдкія запытанні. Калі пераранжаванне служыць толькі для сортавання, мадэль завжды будзе отрымляць якісь даныя, а прыемная мадэль завжды будзе ўтвараць адпаведзенні. Порг рэйтынгу ператварае немагчымасць знайсці інфармацыю на простае адмова, а не на вялікі выдумак. Належыць задаць гэты порг на адпаведныя запытанні, адпаведзенні на якія вядома, што ў корпусе ўжо няма, і застаўляць яго, калі корыстувачы скаржацца на занадто многія адмовы.

    Спачатку — разумеў запытанне

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

    • Расшырыць і нормалізаваць. Пераклаць простую мову на слоўнік корпуса, не адмовляючыся ад аднаго ўсходнага варыянту. Оба варыянты прайходзяць у гібрыдны запіт: аднаго ў лексычную частку, а другога — у сэмантычную.
    • Раздзеліць складныя запіты. Запіт пра тое, чы можна сумешчаць два прыемнікі пад час вагітнасці, на самай працэ па правэлі трох пашукоў: па кожны прыемнік окрэма, а таксама з урахоўваннем аспекта вагітнасці. Адбыць іх окрэма, з’яеднаць рангі і параднаць занова аб’еднаны набор.
    • Распрацаваць кантэкст. Даўеджэнні на кшталт запіта пра вышэю дозу сама по сабе не маюць сэнсу. Урахавіць паказваны ранейшы запіт і перазмовіць яго як самастоятэльны запіт прытым, як толькі будзе адбывацца эмбедынг, адколькі эмбедынгі не зберагаюць нічога у памяці.
  • Класыфікацыя і направленне. Знакі надзеі, запыткі пра асобовую дозаваначу і ўсё, што нагадвае дыягназу, не патрапляюць у працэс аналізу і прымаюць прагматычны шлях адпаведнай рэакцыі. Класыфікатор павінен быць чутлівым да сігналаў. Хыбна пазітыўная рэакцыя — гэта лёгкая няяквасць у відзеце „Будзь ласка, праконтактавацеся з клінікам“, тады як хыбна негатыўная рэакцыя — гэта неудача, якая завершае работу продукту.
  • Фіксацыя на адпаведных структурах

    Этап стварэння тэксту ёсць короткі. Большая частка зусіль направлена на фармат выходных даных, а не на формулюванне запытку. Кожны абдзінак запытку, які быў аналізаваны, павінен быць пазначаны стабільным тэгам, а выходны тэкст — структураваны так, каб кожна твэрджэння паводзілася перыядком тэгаў, на якія яна спынаецца:

    {
      "claims": [
        { "text": "…", "sources": [2, 5] },
        { "text": "…", "sources": [1] }
      ],
      "uncertain": true,
      "route_to_clinician": false
    }
    

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

    Значэнне мелі два меншыя выборы. Паказваць дату перасмотру дакумента пад кожным цітатаваннем, каб корыстнікі самі моглі ацэніць яго свежасць. А калі знайдзеныя істочнікі суперсечуцца, паказваць гэтыя суперсечанні, а не прыменяць якісь компроміс. Фраза «Кераванне разнае: істочнік А кажа X, істочнік B кажа Y» ёсць болей корыстной і чыстасловнай, чым сінтэзаваны сярэдній значэнні.

    Ацэнка, чы розгадка

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

    Толькі адзыскванне інфармацыі, без LLM. Каля каляга запытанняў з ручна пазначанымі ідэнтыфікаторамі актуальных частак, што дазволяе ацэніць рэкалі@20 і nDCG@10. Гэта выконваецца за секунды, выяўляе большасць рэгрэсій і, што галоўнае, аддзеляе якосьць адзысквання інфармацыі ад якосці генеравання. Калі адпаведзі пагіршуюцца, адразу стае зрозумела, чыі ў цьым вінаваць — корпус дадзеных чы запытанне.

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

    Точна адаптацыя адказу. Адзінаковы набор складаецца з запытанняў, для якіх правільным спосабам адпаведзення ёсць адказ пра адмову чыста перадача на іншую службу, тады кантрольныя прагі не можу таямна парадузіць такую поведэнку.

    Супаўзвязаная літэратура

  • Практычныя прытамулы: Безпека базы дадзейнаў вектараў: Новая паверхня атак у сістэмах RAG — Паследовы апіс практычных прытамулаў: Безпека базы дадзейнаў вектараў: Новая паверхня атак у сістэмах RAG: контракты, перакананні і шаблоны коду для команд, якія вікорыстоўваюць гэты патэрн.
  • Docling Pipelines: Стварэнне пайплайнаў дадзейнаў RAG высокага класу — Як docling-pipelines заменяе нестабільныя скрыпты на складальныя аператары і потокі JSON: запоўненне дадзейнаў, выкарыстоўванне, перакананні ў якасці, стварэнне вектараў і ўтрыманне іх у базе дадзейнаў.
  • Стварэнне асістэнта Slack RAG без адзінольнай базы дадзэння вектараў — Бот Slack адпавядае на запыткі спяльнай групы на аснове дакументаў, якія зберагліся ў адной таблеце StarRocks. Адзысканне інфармацыі адбываецца за дапамой звычайнага SQL, а шасць урокаў з практычнага апыту раскрываюць тэмы запыткаў, разбівкі дакументаў на часткі і коставаць.
  • Паўны план развіцця RAG: ад фундаментальных прынцыпаў да проблем у працэсе выкарыстоўвання — Две паўтары, тры крокі і спіс названых проблем: як працюе генераванне з адзысканнем інфармацыі, дзе ён выклікае проблемы і якія способы ўсунуць кожную з іх.
  • Пераказ RAG з Pinecone у Postgres pgvector: шасць месца пазней — Канкрэтныя паказнікі працы пасля пераказу 9 мільйона вектараў з Pinecone: вартасць, затрымкі, ступень адзыскання, надзеянае працаванне, тое, што пагіршылася, і калі pgvector ўзьме на сябе правылы рашэнні. — Канкрэтныя паказнікі працы пасля пераказу 9 мільйона вектараў з Pinecone: вартасць, затрымкі, ступень адзыскання, надзеянае працаванне, тое, што пагіршылася, і калі pgvector ўзьме на сябе правылы рашэнні.