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

Вектарны пошук у Azure AI Search: HNSW, профілі і RAG-рэтрывер на Python

Будуем вектарны індэкс у Azure AI Search на Python: HNSW супраць поўнага KNN, профілі, эмбедынгі, вектарныя запыты і RAG-промпт, а таксама парады па гібрыдным пошуку.

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

Крыніцы: Building a Vector Index in Azure AI Search: HNSW, Profiles, and RAG Retrieval

Адміністрацыя даных — гэта частка сістэмы RAG, якая вяршыць пра тое, чы рэшэння ў канцы ёсць конкрэтным чы нечыясным. У цім кароткам адгуку паспелюем, як працюе векторны пошук у Azure AI Search, а пасля стварым маленькій сервіс на Python, які прымае запіт, знаходзіць найбліжэйшы тэкст па значэнню і складае запрошэнне для мовной мадэлі. Адзіна прыметка па назве: раней шэрыс называлася Azure Cognitive Search, і ў багато старэйшых пастоў і прыкладаў досягналі ўжо тая назва. Канцэпцыі застаюцца ідэнтычнымі.

Две часткі RAG

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

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

Чаму падбіранне ключоўых слоў не ўсё

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

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

Сервіс прыменяе два алгорытмы пошуку для вектароў:

  • HNSW (Hierarchical Navigable Small World) — гэта апрыксіматыўны метод пошуку найбліжэйшых суседаў. Ён ўзяты за звычай для практычнага вікорыстання, такім як у прыладзях працы.
  • Эксклюзыўны KNN парабягае запіт з кожным схованым вектарам. Ён ўсё практычна, але медленейшы, таму падходзіць для маленькіх набораў даных або для перакананняся ў тым, насколькі точны метод апрыксівання.
  • Алгорытмы не прыўязаны безпасэродзе да поля. Індэкс мае настройкі пошуку вектароў, якія залучаюць спіс алгорытмоў і спіс профіляў, пры чым кожны профіль пазначае назву аднаго з варыянтав алгорытма. Поле вектароў пасля чаго апісваецца па назве профіля, тады ідэнтычныя настройкі алгорытма можна выкарыстоўваць у калькі лягкоў.

    Тып поля — Collection(Edm.Single), тое ўзаемнае збора значэнняў з адной точнасцю. Які ўжо трэба задаць колькасць вымераў, і гэта чысло павінна быць роўная выхідным размерам модэля інкапсулявання, які вы вжываеце.

    Форма сістэмы

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

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

    Практычная рэалізацыя

    Вам патрэбна прыўязка Azure, Python 3.9 або новейшая версія, а таксама базовыя навыкі запуску скрыптав. Эмбеддынгі ствараюцца локальна за дапамою невялікага адкрытага модэлю, таму для ўжыцця не трэба наладжваць Azure OpenAI. Перэхад на Azure OpenAI практыкуецца ў канцы.

    Шаг 1: Створэнне службы пошуку

    У портале Azure створыце рэсурс Azure AI Search. Для гэтага падойдзе безплатны рывень. Калі ён будзе створаны, скопіюйце два значэння: канец пункту службы у фармаце https://<your-service>.search.windows.net і ключ адміністратара з раздэлу Keys. Спрыяйце гэтаму ключу як секрэту. У рэальным праекце ён павінен знаходзіцца ў змэнный аб’ектае сяродовышча або у Azure Key Vault, а не ў кодзе вучоркі.

    Шаг 2: Інсталяцыя бібліятэкаў

    Патрэбны два пакеты: Azure Search SDK і sentence-transformers.

    pip install azure-search-documents sentence-transformers
    

    Шаг 3: Адактувація індэксу вектара

    Следзячы скрыпт стварае індэкс з вектарным полем, пасля чыгунача адактувае настройкі пошуку вектараў за дапамою алгорытму і профілю пры стварэнні індэкса.

    from azure.core.credentials import AzureKeyCredential
    from azure.search.documents.indexes import SearchIndexClient
    from azure.search.documents.indexes.models import (
        SearchIndex,
        SimpleField,
        SearchableField,
        SearchField,
        SearchFieldDataType,
        VectorSearch,
        HnswAlgorithmConfiguration,
        HnswParameters,
        VectorSearchProfile,
    )
    
    endpoint = "https://<your-service>.search.windows.net"
    admin_key = "<your-admin-key>"
    index_name = "rag-docs"
    index_client = SearchIndexClient(
        endpoint=endpoint,
        credential=AzureKeyCredential(admin_key),
    )
    # 1. Define the fields. The embedding field is the vector field.
    fields = [
        SimpleField(name="doc_id", type=SearchFieldDataType.String, key=True, filterable=True),
        SearchableField(name="text", type=SearchFieldDataType.String),
        SimpleField(name="source", type=SearchFieldDataType.String, filterable=True),
        SearchField(
            name="embedding",
            type=SearchFieldDataType.Collection(SearchFieldDataType.Single),
            searchable=True,
            vector_search_dimensions=384,             # must match the embedding model
            vector_search_profile_name="my-vector-profile",
        ),
    ]
    # 2. Define the vector search configuration: one algorithm and one profile.
    vector_search = VectorSearch(
        algorithms=[
            HnswAlgorithmConfiguration(
                name="my-hnsw",
                parameters=HnswParameters(
                    m=4,
                    ef_construction=400,
                    ef_search=500,
                    metric="cosine",
                ),
            )
        ],
        profiles=[
            VectorSearchProfile(
                name="my-vector-profile",
                algorithm_configuration_name="my-hnsw",
            )
        ],
    )
    # 3. Create (or update) the index.
    index = SearchIndex(name=index_name, fields=fields, vector_search=vector_search)
    index_client.create_or_update_index(index)
    print("Index created:", index_name)
    

    Калькі ласць у гэтым скрыпце трэба падробна адказаць. Колькасць вымяраў становіць 384, таму што самэ гэта значэнне выдае выбраныя мадэль эмбедынгу. Поле вектара паказвае на прафіль пад назвай my-vector-profile, які у свою чаргу паказвае на алгорытм HNSW пад назвай my-hnsw. Метрыка адстані — косынус, якая ўзніклае як стандартна для тэкстовых эмбедынгу.

    HNSW апускае параметры налаштавання, а Azure прыносіць разумныя стандартныя значэння. Дакументаваныя значэння: m = 4 для колькасці двунаправленых звязаў на кожны вузел, efConstruction = 400 для спіса кандыдатаў пад час стварэння графа, і efSearch = 500 для спіса кандыдатаў пад час запітаў. Падвышэнне ўсіх гэтых значэнняў павышае рывень вярнага аднаходжэння даных, але за рахунак затрымкі і часу стварэння, таму спачатку вжывайце стандартныя значэння і налаштавайце ўсё па своіх цялях.

    Шаг 4: Эмбедынг і аплоджэнне

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

    from azure.search.documents import SearchClient
    from sentence_transformers import SentenceTransformer
    
    search_client = SearchClient(
        endpoint=endpoint,
        index_name=index_name,
        credential=AzureKeyCredential(admin_key),
    )
    # Load the embedding model (produces 384-dimensional vectors)
    model = SentenceTransformer("all-MiniLM-L6-v2")
    # Our small knowledge base
    documents = [
        {"doc_id": "1", "source": "billing-faq",
         "text": "You can update your payment method from the account settings page under Billing."},
        {"doc_id": "2", "source": "billing-faq",
         "text": "Refunds are processed within five to seven business days to the original payment method."},
        {"doc_id": "3", "source": "shipping-faq",
         "text": "Standard delivery takes three to five working days within the country."},
        {"doc_id": "4", "source": "account-faq",
         "text": "To reset your password, click the forgot password link on the login screen."},
        {"doc_id": "5", "source": "shipping-faq",
         "text": "International orders may take up to fourteen working days depending on customs."},
    ]
    # Create embeddings for the text of each document
    texts = [d["text"] for d in documents]
    vectors = model.encode(texts)
    # Attach the embedding to each document and upload
    to_upload = []
    for doc, vector in zip(documents, vectors):
        to_upload.append({
            "doc_id": doc["doc_id"],
            "source": doc["source"],
            "text": doc["text"],
            "embedding": vector.tolist(),
        })
    result = search_client.upload_documents(documents=to_upload)
    print("Uploaded", len(result), "documents.")
    

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

    Шаг 5: Запыт на адналічэнне па вектарах

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

    from azure.search.documents.models import VectorizedQuery
    
    def search(question, k=3):
        # Embed the question with the same model
        query_vector = model.encode([question])[0]
        vector_query = VectorizedQuery(
            vector=query_vector.tolist(),
            k_nearest_neighbors=k,
            fields="embedding",
        )
        results = search_client.search(
            search_text=None,                 # pure vector search
            vector_queries=[vector_query],
            select=["doc_id", "source", "text"],
        )
        output = []
        for r in results:
            output.append({
                "score": r["@search.score"],
                "source": r["source"],
                "text": r["text"],
            })
        return output
    
    for item in search("how do I get my money back", k=3):
        print(round(item["score"], 4), "|", item["source"], "|", item["text"])
    

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

    Шаг 6: Складанне запытання

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

    def build_prompt(question, k=3):
        hits = search(question, k=k)
        context = "\n\n".join(f"- {hit['text']}" for hit in hits)
    
    prompt = (
            "You are a support assistant. Use only the context below to answer "
            "the question. If the answer is not in the context, say that you do "
            "not have enough information.\n\n"
            f"Context:\n{context}\n\n"
            f"Question: {question}\n"
            "Answer:"
        )
        return prompt
    
    print(build_prompt("how do I get my money back"))
    

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

    Іспытанне вкладкі Azure OpenAI

    Багато команд у Azure выкарыстоўваюць хоставаныя модэлі вкладкі, такія як text-embedding-3-small, заместо локальных. Змяны незначныя: трэба вызваць кліент Azure OpenAI для стварэння кожнай вкладкі і задаць колькасць вымераў індэкса так, каб яна падходзіла да модэля, напрыклад 1536 для text-embedding-ada-002. Усе іншае ў кодзе індэкса і запыту застаецца без змян.

    from openai import AzureOpenAI
    
    client = AzureOpenAI(
        api_key="<your-azure-openai-key>",
        api_version="2024-10-21",
        azure_endpoint="https://<your-resource>.openai.azure.com/",
    )
    def get_embedding(text):
        response = client.embeddings.create(
            input=text,
            model="text-embedding-3-small",   # your deployment name
        )
        return response.data[0].embedding
    

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

    Што можа пайсці не так

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

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

    Перакананне якосці адзысквання

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

    Прыметкі для пераходу за межы дэманстрацыі

    • Модэль укладання. Яна вялікая меры вплывае на размернасць вектараў і якасць пошуку, таму выбірайце ёю адказна.
    • Разбіўка на фрагменты. Раздзеліце дугія дакументы на менейшыя фрагменты пры стварэнні індэкса.
  • Гібрыдныя пошукавыя функцыі. Чырвоны ўжыванню ключавых слоў і сэмантычнага падабрання, неабходна прыслаць значэнне search_text разам з векторным запытам. Сэрвіс спаўнае рэзультаты, атрыбутаваныя ключавымі словамі за прынцыпам BM25, з векторнымі рэзультатамі.
  • Сэмантычныя рангаванні. Апцыйнае сэмантычнае рангавальнік пераранжуе найлепшыя рэзультаты за дапамогою мовнага мадэлі, каб павысіць якасць аранжавання.
  • Размер індэкса. Векторныя індэксы спрацоўваюць з велікай колькасцю памяці, таму трэба старанна сочыць за ўжоўваемай памяцюю і, як толькі з’яўляюцца проблемы з ахованнем, рассмотрзець можлівасць стыснення вектораў або іх пераклад у бінарны фармат.
  • Звярнуўшыся сюды, вы можэце дадаць своія дакументы, адсортаваць іх, выкарыстоўваць гібрыдныя пошукавыя методы і выбраць мадэль па сваму выбору, каб ператварыць дэманстрацыйны прыклад у полноценную RAG-аплікацыю.