Implementa una semplice ricerca ibrida per il recupero della generazione aumentata utilizzando Oracle Database 23ai

Introduzione

Questa esercitazione descrive come implementare una ricerca ibrida nell'ambito di un processo Retrieval Augmented Generation (RAG) utilizzando sia la ricerca vettoriale che quella per parola chiave in Oracle 23ai.

RAG è emersa come una capacità chiave per le aziende che utilizzano modelli LLM (Large Language Models) per aumentare le loro risposte con informazioni aziendali o di dominio specifiche. La ricerca con una knowledge base aziendale di informazioni rilevanti per la query di un utente, quindi l'associazione delle informazioni recuperate alla richiesta all'LLM consente risposte che si basano su dati interni, criteri, e informazioni specifiche sullo scenario, con informazioni specifiche che riducono la probabilità di allucinazione, consentendo una risposta in linguaggio naturale che includa citazioni appropriate e riferimenti ai documenti nella knowledge base.

Oracle Database include diverse potenti funzionalità per l'esecuzione di task RAG. La release Oracle Database 23ai ha introdotto una funzionalità AI Vector Search, che consente di eseguire ricerche semantiche rapide su dati non strutturati. L'utilizzo della ricerca semantica con embeddings vettoriali di alta qualità può sembrare quasi magico, con quasi tutte le query che evocano documenti altamente rilevanti da un'enorme base di conoscenze. Tuttavia, solo perché la ricerca vettoriale è disponibile e fornisce risultati di alta qualità nella maggior parte degli scenari non significa che la ricerca tradizionale basata su parole chiave debba essere abbandonata. Qualsiasi sviluppatore che abbia trascorso molto tempo a testare il recupero ha certamente scoperto alcune stranezze, in cui i documenti che coprono l'argomento specifico richiesto e che sarebbero intuitivamente inclusi nella risposta non sono, anche se sarebbero banalmente trovati da una ricerca di parole chiave.

Perché non usare entrambi?

Oracle Database 23ai si basa su tutte le potenti funzionalità aggiunte al database nel tempo, incluso Oracle Text, che offre funzionalità di query RTF e questa esercitazione è progettata per dimostrare in che modo l'esistenza di entrambe queste funzionalità nel database rende incredibilmente semplice l'implementazione di una ricerca ibrida solida, offrendo il meglio di entrambi i mondi senza alcuna duplicazione dei dati.

Nota: in questa esercitazione viene illustrata l'implementazione della logica per la ricerca ibrida in Python utilizzando un modello di incorporamento locale. Oracle Database 23ai supporta il calcolo delle incorporazioni di testo nel database tramite l'uso di modelli ONNX ed è disponibile il supporto nativo per la ricerca ibrida utilizzando un modello ONNX nel database tramite un package DBMS (Database Management System). Implementare la logica direttamente in Python fornisce un controllo molto maggiore sul comportamento della ricerca, tuttavia il pacchetto DBMS offre un set di funzionalità semplice ma potente per alcuni casi d'uso. Per ulteriori informazioni, vedere Importa modelli ONNX in Oracle Database - Esempio end-to-end e Comprendere la ricerca ibrida.

Obiettivi

Prerequisiti

Task 1: Impostazione della tabella del database

La tabella utilizzata per la ricerca ibrida può essere identica a una tabella utilizzata per la ricerca vettoriale, in quanto Oracle Text può indicizzare i campi CLOB (Character Large Object), in genere utilizzati per memorizzare il contenuto del documento.

Nota: l'istruzione SQL per l'impostazione iniziale della tabella viene visualizzata qui direttamente, anziché essere richiamata da Python. L'account utente del database utilizzato dal processo RAG deve disporre solo delle autorizzazioni per eseguire query sulla tabella e non creare tabelle e indici, in quanto tali task verranno eseguiti da un amministratore del database utilizzando gli strumenti preferiti.

  1. Dopo aver eseguito la connessione al database, creare una tabella da utilizzare per memorizzare i documenti utilizzati dal processo RAG utilizzando l'istruzione SQL seguente.

    CREATE TABLE hybridsearch 
    (id RAW(16) DEFAULT SYS_GUID() PRIMARY KEY,
    text CLOB, 
    embeddings VECTOR(768, FLOAT32),
    metadata JSON);
    

    La dimensione della colonna vettoriale dipende dal modello di incorporamento che verrà utilizzato per generare i vettori per la ricerca semantica. Qui stiamo usando 768, che corrisponde al modello vettoriale utilizzato più avanti in questo esempio, anche se se se si utilizza un modello alternativo, potrebbe essere necessario aggiornare questo valore per riflettere tale modifica. Viene specificata una colonna JSON per la memorizzazione dei metadati dei documenti, in quanto può fornire strutture flessibili pur consentendo di filtrare gli attributi del documento e, sebbene non venga utilizzata in questa esercitazione, viene inclusa in quanto qualsiasi scenario reale richiederà i metadati dei documenti.

  2. Per abilitare la ricerca per parola chiave del testo, è necessario creare un indice di testo nella colonna di testo.

    CREATE SEARCH INDEX rag_text_index ON hybridsearch (text);
    

Task 2: Installa librerie

Questa esercitazione descrive l'utilizzo di un runtime Python per implementare l'inclusione dei documenti e la ricerca ibrida. Si consiglia di configurare il runtime Python utilizzando un ambiente venv o conda.

Nota: questa esercitazione tenta di introdurre sezioni di codice necessarie per dimostrare ogni concetto e richiederà la rifattorizzazione se deve essere incorporato in una soluzione più ampia.

  1. Installare le dipendenze richieste per questa esercitazione utilizzando pip.

    $ pip install -U oracledb sentence-transformers git+https://github.com/LIAAD/yake
    

Task 3: inclusione di documenti nel database

Una volta creata la tabella, i documenti possono essere inseriti come righe nella tabella. In genere, il processo di ingestione deve essere separato dal processo di query e utilizzare account di database diversi con autorizzazioni diverse (poiché il processo di query non dovrebbe essere in grado di modificare la tabella), tuttavia, ai fini di questa esercitazione, non vengono differenziati qui.

  1. Nell'ambiente Python stabilire la connessione al database. Ad esempio, per Autonomous Transaction Processing.

    import os
    import oracledb
    import traceback
    import json 
    import re
       
    try:
        print(f'Attempting to connect to the database with user: [{os.environ["DB_USER"]}] and dsn: [{os.environ["DB_DSN"]}]')
        connection = oracledb.connect(user=os.environ["DB_USER"], password=os.environ["DB_PASSWORD"], dsn=os.environ["DB_DSN"],
                                    config_dir="/path/to/dbwallet",
                                    wallet_location="/path/to/dbwallet",
                                    wallet_password=os.environ["DB_WALLET_PASSWORD"])
        print("Connection successful!")
    except Exception as e:
        print(traceback.format_exc())
        print("Connection failed!")
    

    La documentazione python-oracledb fornisce dettagli sulla connessione a istanze non ADB che potrebbero non utilizzare i wallet di connessione.

  2. Inizializza il modello di incorporamento che verrà utilizzato per calcolare i vettori di incorporamento. Qui viene utilizzato il modello all-mpnet-base-v2, disponibile sotto la licenza Apache. Anche se questo modello di incorporamento specifico viene utilizzato solo per l'illustrazione, altri modelli possono funzionare meglio o peggio a seconda dei dati. In questo esempio viene utilizzata l'interfaccia SentenceTransformers per semplicità. Per ulteriori informazioni, vedere SentenceTransformers Documentazione.

    from sentence_transformers import SentenceTransformer
       
    model = SentenceTransformer('sentence-transformers/all-mpnet-base-v2')
    
  3. Implementa una semplice funzione di inclusione dei documenti. Il processo di ottenere, analizzare e chunking dei documenti è fuori portata per questo tutorial, e per lo scopo di questo tutorial si presume che saranno solo forniti come stringhe. Questa funzione calcola le incorporazioni utilizzando il modello fornito, quindi inserisce il documento e le incorporazioni nella tabella creata.

    def add_document_to_table(connection, table_name, model, document, **kwargs):
        """
        Adds a document to the database for use in RAG.
        @param connection An established database connection.
        @param table_name The name of the table to add the document to
        @param model A sentence transformers model, with an 'encode' function that returns embeddings
        @param document The document to add, as a string
        Keyword Arguments:
        metadata: A dict with metadata about the document which is stored as a JSON object
        """
        #Calculate the embeddings for the document
        embeddings = model.encode(document)
        insert_sql = f"""INSERT INTO {table_name} (text, embeddings, metadata) VALUES (:text, :embeddings, :metadata)"""
        metadata = kwargs.get('metadata', {})
        cursor = connection.cursor()
        try:
            cursor.execute(insert_sql, text=document, embeddings=json.dumps(embeddings.tolist()), metadata=json.dumps(metadata))
        except Exception as e:
            print(traceback.format_exc())
            print("Insert failed!")
    
  4. Aggiungere alcuni documenti di esempio al database per il test.

    Nota: viene richiamato un commit() esplicito che attiva l'aggiornamento dell'indice di testo.

    table_name = "testhybrid"
       
    # These samples are just included to provide some data to search, not to 
    # demonstrate the efficacy of key phrase versus semantic search. The benefits
    # of hybrid search typically start to emerge when using a much larger volume
    # of content.
    document_samples = [
        "Oracle Database 23ai is the next long-term support release of Oracle Database. It includes over 300 new features with a focus on artificial intelligence (AI) and developer productivity.",
        "Features such as AI Vector Search enable you to leverage a new generation of AI models to generate and store vectors of documents, images, sound, and so on;  index them and quickly look for similarity while leveraging the existing analytical capabilities of Oracle Database.",
        "New developer-focused features now make it simpler to build next-generation applications that use JSON or relational development approaches or both interchangeably.",
        "With a built-in VECTOR data type, you can run AI-powered vector similarity searches within the database instead of having to move business data to a separate vector database.",
        "Property graphs provide an intuitive way to find direct or indirect dependencies in data elements and extract insights from these relationships. The enterprise-grade manageability, security features, and performance features of Oracle Database are extended to property graphs.",
        "The ISO SQL standard has been extended to include comprehensive support for property graph queries and creating property graphs in SQL.",
        "Transactional Event Queues (TxEventQ) are queues built into the Oracle Database. TxEventQ are a high performance partitioned implementation with multiple event streams per queue.",
        "Transactional Event Queues (TxEventQ) now support the KafkaProducer and KafkaConsumer classes from Apache Kafka. Oracle Database can now be used as a source or target for applications using the Kafka APIs.",
        "Database metrics are stored in Prometheus, a time-series database and metrics tailored for developers are displayed using Grafana dashboards. A database metrics exporter aids the metrics exports from database views into Prometheus time series database."
        "The Java Database Connectivity (JDBC) API is the industry standard for database-independent connectivity between the Java programming language and a wide range of databases—SQL databases and other tabular data sources, such as spreadsheets or flat files.",
        "Java Database Connectivity (JDBC) is a Java standard that provides the interface for connecting from Java to relational databases. The JDBC standard is defined and implemented through the standard java.sql interfaces. This enables individual providers to implement and extend the standard with their own JDBC drivers.",
        "The JDBC Thin driver enables a direct connection to the database by providing an implementation of Oracle Net Services on top of Java sockets. The driver supports the TCP/IP protocol and requires a TNS listener on the TCP/IP sockets on the database server.",
        "The JDBC Thin driver is a pure Java, Type IV driver that can be used in applications. It is platform-independent and does not require any additional Oracle software on the client-side. The JDBC Thin driver communicates with the server using Oracle Net Services to access Oracle Database.",
        "The JDBC OCI driver, written in a combination of Java and C, converts JDBC invocations to calls to OCI, using native methods to call C-entry points. These calls communicate with the database using Oracle Net Services.",
        "The python-oracledb driver is a Python extension module that enables access to Oracle Database. By default, python-oracledb allows connecting directly to Oracle Database 12.1 or later. This Thin mode does not need Oracle Client libraries.",
        "Users interact with a Python application, for example by making web requests. The application program makes calls to python-oracledb functions. The connection from python-oracledb Thin mode to the Oracle Database is established directly.",
        "Python-oracledb is said to be in ‘Thick’ mode when it links with Oracle Client libraries. Depending on the version of the Oracle Client libraries, this mode of python-oracledb can connect to Oracle Database 9.2 or later.",
        "To use python-oracledb Thick mode, the Oracle Client libraries must be installed separately. The libraries can be from an installation of Oracle Instant Client, from a full Oracle Client installation (such as installed by Oracle’s GUI installer), or even from an Oracle Database installation (if Python is running on the same machine as the database).",
        "Oracle’s standard client-server version interoperability allows connection to both older and newer databases from different Oracle Client library versions."
    ]
       
    for document in document_samples:
        add_document_to_table(connection, table_name, model, document)
       
    #Call an explicit commit after adding the documents, which will trigger an async update of the text index
    connection.commit()
    

Una volta caricati i documenti, le funzionalità di ricerca vettoriale AI di Oracle Database 23ai possono essere utilizzate per eseguire una ricerca semantica basata su un vettore derivato da una query.

  1. Implementare una funzione di supporto per lavorare con gli oggetti CLOB restituiti dal database.

    def get_clob(result):
        """
        Utility function for getting the value of a LOB result from the DB.
        @param result Raw value from the database
        @returns string
        """
        clob_value = ""
        if result:
            if isinstance(result, oracledb.LOB):
                raw_data = result.read()
                if isinstance(raw_data, bytes):
                    clob_value = raw_data.decode("utf-8")
                else:
                    clob_value = raw_data
            elif isinstance(result, str):
                clob_value = result
            else:
                raise Exception("Unexpected type:", type(result))
        return clob_value
    
  2. Implementare una funzione per eseguire la ricerca semantica utilizzando la funzione SQL vector_distance(). Il modello all-mpnet-base-v2 utilizzato in questa esercitazione utilizza la somiglianza COSINE, che è stata utilizzata per impostazione predefinita qui. Se si utilizza un modello diverso, potrebbe essere necessario specificare una strategia di distanza alternativa.

    def retrieve_documents_by_vector_similarity(connection, table_name, model, query, num_results, **kwargs):
        """
        Retrieves the most similar documents from the database based upon semantic similarity.
        @param connection An established database connection.
        @param table_name The name of the table to query
        @param model A sentence transformers model, with an 'encode' function that returns embeddings
        @param query The string to search for semantic similarity with
        @param num_results The number of results to return
        Keyword Arguments:
        distance_strategy: The distance strategy to use for comparison One of: 'EUCLIDEAN', 'DOT', 'COSINE' - Default: COSINE
        @returns: Array<(string, string, dict)> Array of documents as a tuple of 'id', 'text', 'metadata'
        """
        # In many cases, building up the search SQL may involve adding a WHERE 
        # clause in order to search only a subset of documents, though this is
        # omitted for this simple example.
        search_sql = f"""SELECT id, text, metadata,
                        vector_distance(embeddings, :embedding, {kwargs.get('distance_strategy', 'COSINE')}) as distance
                        FROM {table_name}
                        ORDER BY distance
                        FETCH APPROX FIRST {num_results} ROWS ONLY
                        """
        query_embedding = model.encode(query)
        cursor = connection.cursor()
        try:
            cursor.execute(search_sql, embedding=json.dumps(query_embedding.tolist()))
        except Exception as e:
            print(traceback.format_exc())
            print("Retrieval failed!")
        rows = cursor.fetchall()
        documents = []
        for row in rows:
            documents.append((row[0].hex(), get_clob(row[1]), row[2]))
       
        return documents
    
  3. Convalidare la funzionalità di ricerca semantica utilizzando il seguente esempio.

    query = "I am writing a python application and want to use Apache Kafka for interacting with queues, is this supported by the Oracle database?"
       
    documents_from_vector_search = retrieve_documents_by_vector_similarity(connection, table_name, model, query, 4)
    print(documents_from_vector_search)
    

Questa esercitazione utilizza Oracle Text, che fornisce potenti strumenti di query di testo all'interno di Oracle Database. Sebbene Oracle Text offra una vasta gamma di funzionalità ai fini della ricerca ibrida, tutto ciò che è necessario è una semplice ricerca per parole chiave o frasi chiave. Esistono diverse tecniche per l'estrazione e la ricerca di parole chiave, ma questa implementazione è pensata per essere il più semplice possibile, utilizzando Yet Another Keyword Extractor (YAKE), che si basa sulle funzioni del linguaggio per eseguire l'estrazione non supervisionata di parole chiave e frasi chiave.

Esistono una vasta gamma di altri approcci alla ricerca per parola chiave, con l'algoritmo Okapi BM25 popolare. Tuttavia, l'utilizzo dell'estrazione di parole chiave senza supervisione con un potente indice di ricerca di testo, come quello fornito da Oracle Text, ha il vantaggio di essere particolarmente semplice e la robustezza viene fornita tramite la combinazione con la ricerca semantica.

  1. Implementa una funzione per l'estrazione della frase chiave.

    import yake
       
    def extract_keywords(query, num_results):
        """
        Utility function for extracting keywords from a string.
        @param query The string from which keywords should be extracted
        @param num_results The number of keywords/phrases to return
        @returns Array<(string, number)> Array of keywords/phrases as a tuple of 'keyword', 'score' (lower scores are more significant)
        """
        language = "en"
        #Max number of words to include in a key phrase
        max_ngram_size = 2
        windowSize = 1
       
        kw_extractor = yake.KeywordExtractor(lan=language, n=max_ngram_size, windowsSize=windowSize, top=num_results, features=None)
        keywords = kw_extractor.extract_keywords(query.strip().lower())
        return sorted(keywords, key=lambda kw: kw[1])
    

    Poiché questo metodo di estrazione delle parole chiave si basa sulle caratteristiche della lingua, è importante impostare la lingua appropriata e le prestazioni possono variare a seconda della lingua stessa.

  2. Convalidare l'estrazione della parola chiave utilizzando il seguente esempio.

    query = "I am writing a python application and want to use Apache Kafka for interacting with queues, is this supported by the Oracle database?"
       
    keywords = extract_keywords(query, 4)
    print(keywords)
    
  3. Implementa una funzione per eseguire una ricerca basata su parole chiave.

    def retrieve_documents_by_keywords(connection, table_name, query, num_results):
        """
        Retrieves the documents from the database which have the highest density of matching keywords as the query
        @param connection An established database connection.
        @param table_name The name of the table to query
        @param query The string from which to extract keywords/phrases for searching
        @param num_results The number of results to return
        @returns: Array<(string, string, dict)> Array of documents as a tuple of 'id', 'text', 'metadata'
        """
        num_keywords = 4
        keywords = extract_keywords(query, num_keywords)
        search_sql = f"""SELECT id, text, metadata, SCORE(1)
                        FROM {table_name}
                        WHERE CONTAINS (text, :query_keywords, 1) > 0
                        ORDER BY SCORE(1) DESC
                        FETCH APPROX FIRST {num_results} ROWS ONLY
                        """
        #Assemble the keyword search query, adding the stemming operator to each word
        stemmed_keywords = []
        splitter = re.compile('[^a-zA-Z0-9_\\+\\-/]')
        for keyword in keywords:
            stemmed_keyword = ""
            for single_word in splitter.split(keyword[0]):
                stemmed_keyword += "$" + single_word +" "
            stemmed_keywords.append(stemmed_keyword.strip())
        cursor = connection.cursor()
        try:
            cursor.execute(search_sql, query_keywords=",".join(stemmed_keywords))
        except Exception as e:
            print(traceback.format_exc())
            print("Retrieval failed!")
        rows = cursor.fetchall()
        documents = []
        for row in rows:
            documents.append((row[0].hex(), get_clob(row[1]), row[2]))
        return documents
    

    Uno dei comportamenti più semplici di Oracle Text è l'esecuzione di ricerche per parola chiave tramite la funzione CONTAINS, che supporta una vasta gamma di operatori aggiuntivi per perfezionare o ampliare la ricerca. In questa esercitazione viene utilizzato l'operatore stemming. Espande una query per includere tutti i termini con la stessa radice o radice del termine specificato. Viene utilizzato per normalizzare le parole indipendentemente dalla pluralità e dal tempo, ad esempio per consentire a cat di corrispondere a cats. Per ulteriori informazioni, vedere Operatori di query CONTAINS Oracle Text.

    Nota: se questa applicazione viene applicata a un ampio corpus di documenti, si consiglia di configurare l'indice di testo in modo che includa steli di parole per migliorare le prestazioni. Per ulteriori informazioni su Lexer di base, vedere BASIC_LEXER.

  4. Convalidare la ricerca basata su parole chiave utilizzando il seguente esempio.

    query = "I am writing a python application and want to use Apache Kafka for interacting with queues, is this supported by the Oracle database?"
       
    documents_from_keyphrase_search = retrieve_documents_by_keywords(connection, table_name, query, 4)
    print(documents_from_keyphrase_search)
    

Combinare e utilizzare i risultati

Una volta che i documenti sono stati ottenuti, possono essere forniti a un LLM come contesto aggiuntivo che può utilizzare per rispondere a domande o istruzioni. In alcuni scenari può essere opportuno includere semplicemente tutti i documenti recuperati nel prompt all'LLM. In altri casi, la mancanza di documenti pertinenti è un importante elemento di contesto in sé e per sé, e quindi può essere importante determinarne la rilevanza. Questo può essere basato su una particolare ponderazione che viene posta su ogni tipo di ricerca, o la rilevanza di ogni documento può essere valutata indipendentemente da come è stato restituito utilizzando un modello di ri-classificazione.

In ciascuno di questi casi d'uso, la deduplicazione dei risultati sarà un passo importante. Ogni funzione ha conservato l'ID documento che fornisce un identificativo univoco che può essere utilizzato a questo scopo. Ad esempio:

def deduplicate_documents(*args):
    """
    Combines lists of documents returning a union of the lists, with duplicates removed (based upon an 'id' match)
    Arguments:
        Any number of arrays of documents as a tuple of 'id', 'text', 'metadata'
    @returns: Array<(string, string, dict)> Single array of documents containing no duplicates
    """
    #Definitely not the most efficient de-duplication, but in this case, lists are typically <10 items
    documents = []
    for document_list in args:
        for document in document_list:
            if document[0] not in map(lambda doc: doc[0], documents):
                documents.append(document)
    return documents

Questi due metodi consentono l'estrazione di documenti pertinenti dalla stessa tabella di database, utilizzando due meccanismi diversi per determinare la somiglianza. Entrambi i metodi sono molto veloci da eseguire, quindi c'è un sovraccarico minimo oltre l'indice di testo per applicare entrambe le tecniche per recuperare i documenti pertinenti. La ricerca semantica consente il recupero di documenti indipendentemente dall'uso di sinonimi o errori di battitura occasionali, mentre la ricerca di frasi chiave può acquisire scenari in cui l'utente chiede molto specificamente un particolare argomento, come un nome di prodotto o funzione. I risultati combinati possono completarsi a vicenda per aggiungere robustezza al processo RAG complessivo.

Conferme

Altre risorse di apprendimento

Esplora altri laboratori su docs.oracle.com/learn o accedi a più contenuti gratuiti sulla formazione su Oracle Learning YouTube channel. Inoltre, visita education.oracle.com/learning-explorer per diventare un Oracle Learning Explorer.

Per la documentazione del prodotto, visita l'Oracle Help Center.