Implemente a Pesquisa Híbrida Simples para Geração Aumentada de Recuperação usando o Oracle Database 23ai
Introdução
Este tutorial demonstra como implementar uma pesquisa híbrida como parte de um processo de Geração Aumentada de Recuperação (RAG) usando pesquisa de vetor e palavra-chave no Oracle 23ai.
A RAG surgiu como um recurso-chave para empresas que usam Modelos de Linguagem Grande (LLMs) para aumentar suas respostas com informações específicas de negócios ou domínio. Pesquisar com uma base de conhecimento empresarial informações relevantes para a consulta de um usuário e anexar as informações recuperadas à solicitação ao LLM permite respostas que se baseiam em dados internos, políticas, e informações específicas de cenários, com as informações específicas reduzindo a probabilidade de alucinação, permitindo uma resposta de linguagem natural que inclua citações e referências apropriadas a documentos na base de conhecimento.
O Oracle Database inclui vários recursos avançados para executar tarefas de RAG. A versão do Oracle Database 23ai introduziu um recurso AI Vector Search, que fornece a capacidade de executar pesquisas semânticas rápidas em dados não estruturados. Usar a pesquisa semântica com incorporações de vetores de alta qualidade pode parecer quase mágico, com quase qualquer consulta convocando documentos altamente relevantes de uma enorme base de conhecimento. No entanto, só porque a pesquisa vetorial está disponível e fornece resultados de alta qualidade na maioria dos cenários não significa que a pesquisa tradicional baseada em palavras-chave deve ser abandonada. Qualquer desenvolvedor que passou muito tempo testando a recuperação certamente descobriu algumas esquisitices, em que os documentos que cobrem o assunto específico perguntado sobre e que seriam intuitivamente incluídos na resposta não são, mesmo que eles seriam trivialmente encontrados por uma pesquisa de palavra-chave.
Então, por que não usar os dois?
O Oracle Database 23ai baseia-se em todos os recursos avançados que foram adicionados ao banco de dados ao longo do tempo, incluindo o Oracle Text, que fornece recursos de consulta de rich text, e este tutorial foi projetado para demonstrar como a existência desses dois recursos no banco de dados torna incrivelmente simples implementar uma pesquisa híbrida robusta, fornecendo o melhor dos dois mundos sem duplicação de dados.
Observação: Este tutorial demonstra a implementação da lógica para pesquisa híbrida no Python, usando um modelo de incorporação local. O Oracle Database 23ai suporta o cálculo de incorporações de texto no banco de dados por meio do uso de modelos ONNX, e há suporte nativo para pesquisa híbrida usando um modelo ONNX no banco de dados por meio de um pacote Database Management System (DBMS). A implementação da lógica diretamente no Python fornece um controle muito maior sobre o comportamento da pesquisa, no entanto, o pacote DBMS oferece um conjunto simples, mas poderoso, de recursos para alguns casos de uso. Para obter mais informações, consulte Importar Modelos ONNX no Exemplo de Ponta a Ponta do Oracle Database e Entender a Pesquisa Híbrida.
Objetivos
-
Definir uma tabela de banco de dados para pesquisa híbrida.
-
Implemente um processo simples de ingestão de documentos no Python.
-
Implementar pesquisa de vetor e pesquisa de palavra-chave dos documentos.
Pré-requisitos
-
Acesso a uma instância do banco de dados Oracle Database 23ai, com um ou mais usuários que podem criar tabelas e índices.
-
Um runtime Python com a capacidade de estabelecer conexão com o banco de dados.
Observação: Este tutorial usa Python para interagir com o Oracle Database, pois supõe-se que a pesquisa híbrida de documentos será implementada como parte de um processo RAG mais amplo, no entanto, os principais recursos são demonstrados usando apenas SQL, o que deve permitir que a abordagem seja aplicada em outras linguagens de desenvolvimento.
Tarefa 1: Configurar a Tabela de Banco de Dados
A tabela usada para pesquisa híbrida pode ser idêntica a uma tabela usada para pesquisa vetorial, pois o Oracle Text pode indexar campos de Objeto de Caractere Grande (CLOB), que geralmente são usados para armazenar o conteúdo do documento.
Observação: O SQL da configuração da tabela inicial é mostrado aqui diretamente, em vez de ser chamado do Python. A conta de usuário do banco de dados empregada pelo processo RAG só deve ter permissões para consultar a tabela e não criar tabelas e índices, como tal, essas tarefas serão executadas por um administrador de banco de dados, usando suas ferramentas preferenciais.
-
Depois de estabelecer conexão com o banco de dados, crie uma tabela a ser usada para armazenar os documentos usados pelo processo RAG usando o SQL a seguir.
CREATE TABLE hybridsearch (id RAW(16) DEFAULT SYS_GUID() PRIMARY KEY, text CLOB, embeddings VECTOR(768, FLOAT32), metadata JSON);
O tamanho da coluna do vetor depende do modelo de incorporação que será usado para gerar os vetores para pesquisa semântica. Aqui estamos usando o 768, que corresponde ao modelo vetorial usado posteriormente neste exemplo, embora se um modelo alternativo for usado, esse valor talvez precise ser atualizado para refletir essa alteração. Uma coluna JSON é especificada para armazenar metadados de documentos, pois isso pode fornecer estruturas flexíveis, permitindo ainda a filtragem dos atributos do documento e, embora não seja usada neste tutorial, ela é incluída, pois qualquer cenário do mundo real exigirá metadados de documentos.
-
Para ativar a pesquisa de palavra-chave do texto, um índice de texto precisa ser criado na coluna de texto.
CREATE SEARCH INDEX rag_text_index ON hybridsearch (text);
Tarefa 2: Instalar Bibliotecas
Este tutorial ilustra como usar um runtime do Python para implementar ingestão de documentos e pesquisa híbrida. É recomendável configurar o runtime do Python usando um ambiente venv
ou conda
.
Observação: Este tutorial tenta introduzir seções de código conforme necessário para demonstrar cada conceito e exigirá refatoração se for incorporado a uma solução mais ampla.
-
Instale as dependências necessárias para este tutorial usando
pip
.$ pip install -U oracledb sentence-transformers git+https://github.com/LIAAD/yake
Tarefa 3: Ingerir Documentos no Banco de Dados
Uma vez criada a tabela, os documentos poderão ser inseridos como linhas na tabela. Normalmente, o processo de ingestão deve ser separado do processo de consulta e usar diferentes contas de banco de dados com permissões diferentes (como o processo de consulta não deve ser capaz de modificar a tabela), no entanto, para os fins deste tutorial, elas não são diferenciadas aqui.
-
Em seu ambiente Python, estabeleça sua conexão com o banco de dados. Por exemplo, para o 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!")
A documentação
python-oracledb
fornece detalhes sobre a conexão com instâncias não ADB que podem não usar wallets de conexão. -
Inicialize o modelo de incorporação que será usado para calcular vetores de incorporação. Aqui o modelo
all-mpnet-base-v2
está sendo usado, que está disponível sob a licença do Apache. Embora esse modelo de incorporação específico seja usado apenas para ilustração, outros modelos podem ter um desempenho melhor ou pior, dependendo dos seus dados. Este exemplo está usando a interface SentenceTransformers para simplificar. Para obter mais informações, consulte a Documentação de SentenceTransformers.from sentence_transformers import SentenceTransformer model = SentenceTransformer('sentence-transformers/all-mpnet-base-v2')
-
Implemente uma função de ingestão de documento simples. O processo de obtenção, análise e fragmentação de documentos está fora do escopo deste tutorial, e para a finalidade deste tutorial presume-se que eles só serão fornecidos como strings. Essa função calcula as incorporações usando o modelo fornecido e, em seguida, insere o documento e as incorporações na tabela criada.
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!")
-
Adicione alguns documentos de amostra ao banco de dados para teste.
Observação: Um
commit()
explícito é chamado, o que aciona a atualização do índice de texto.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()
Tarefa 4: Implementar o Oracle Database 23ai AI Vector Search
Depois que os documentos forem carregados, os recursos de Pesquisa de Vetor de IA do Oracle Database 23ai poderão ser usados para executar uma pesquisa semântica com base em um vetor que derivamos de uma consulta.
-
Implemente uma função auxiliar para trabalhar com os objetos CLOB retornados pelo banco de dados.
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
-
Implemente uma função para executar a pesquisa semântica usando a função SQL
vector_distance()
. O modeloall-mpnet-base-v2
usado neste tutorial usa a similaridadeCOSINE
, que foi padronizada aqui. Se você usar um modelo diferente, talvez seja necessário especificar uma estratégia de distância 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
-
Valide o recurso de pesquisa semântica usando o exemplo a seguir.
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)
Tarefa 5: Implementar Pesquisa de Palavra-chave
Este tutorial usa o Oracle Text, que fornece ferramentas avançadas de consulta de texto no Oracle Database. Embora o Oracle Text forneça uma ampla gama de recursos para fins de pesquisa híbrida, tudo o que é necessário é uma pesquisa simples por palavras-chave ou frases-chave. Existem várias técnicas para extração e pesquisa de palavras-chave, no entanto, essa implementação deve ser a mais simples possível, usando YET Another Keyword Extractor (YAKE), que depende de recursos de idioma para executar extração não supervisionada de palavras-chave e frases-chave.
Há uma ampla gama de outras abordagens para pesquisa de palavra-chave, com o algoritmo Okapi BM25 sendo popular. No entanto, o uso de extração de palavra-chave não supervisionada com um poderoso índice de pesquisa de texto, como o fornecido pelo Oracle Text, tem a vantagem de ser particularmente simples, e a robustez é fornecida por meio da combinação com pesquisa semântica.
-
Implemente uma função para extração de frase-chave.
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])
Como esse método de extração de palavras-chave depende de recursos de idioma, definir o idioma apropriado é importante, e o desempenho pode variar dependendo do idioma em si.
-
Valide a extração de palavra-chave usando a amostra a seguir.
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)
-
Implementar uma função para executar pesquisa baseada em palavra-chave.
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
Um dos comportamentos mais simples no Oracle Text é realizar pesquisas de palavras-chave por meio da função
CONTAINS
, que suporta uma ampla gama de operadores adicionais para refinar ou ampliar a pesquisa. Neste tutorial, o operador stemming é usado. Expande uma consulta para incluir todos os termos com a mesma palavra-tronco ou raiz que o termo especificado. Isso é usado para normalizar palavras independentemente de pluralidade e tempo, para permitir que o cat corresponda a gatos, por exemplo. Para obter mais informações, consulte Oracle Text CONTÉM Operadores de Consulta.Observação: se aplicar isso a um grande corpus de documentos, é aconselhável configurar o índice de texto para incluir hastes de palavra para melhorar o desempenho. Para obter mais informações sobre o Basic Lexer, consulte BASIC_LEXER.
-
Valide a pesquisa baseada em palavra-chave usando o exemplo a seguir.
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)
Combinar e Usar os Resultados
Depois que os documentos forem obtidos, eles poderão ser fornecidos a um LLM como contexto adicional que ele pode usar para responder a consultas ou instruções. Em alguns cenários, pode ser apropriado simplesmente incluir todos os documentos recuperados no prompt para o LLM. Em outros casos, a falta de documentos relevantes é uma parte importante do contexto em si e, portanto, pode ser importante determinar sua relevância. Isso pode ser baseado em uma ponderação específica que é colocada em cada tipo de pesquisa, ou a relevância de cada documento pode ser avaliada independentemente de como ele foi retornado usando um modelo de reclassificação.
Em cada um desses casos de uso, a desduplicação dos resultados será uma etapa importante. Cada função tem preservado o ID do documento que fornece um identificador exclusivo que pode ser usado para essa finalidade. Por exemplo:
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
Esses dois métodos permitem a extração de documentos relevantes da mesma tabela de banco de dados, usando dois mecanismos diferentes para determinar a similaridade. Ambos os métodos são muito rápidos de executar, portanto, há uma sobrecarga mínima além do índice de texto para aplicar ambas as técnicas para recuperar documentos relevantes. A pesquisa semântica permite a recuperação de documentos, independentemente do uso de sinônimos ou erro de digitação ocasional, enquanto a pesquisa de frase-chave pode capturar cenários em que o usuário está perguntando muito especificamente sobre um assunto específico, como um nome de produto ou função. Os resultados combinados podem se complementar para adicionar robustez ao processo geral de RAG.
Links Relacionados
Confirmações
- Autor - Callan Howell-Pavia (Especialista em Soluções APAC)
Mais Recursos de Aprendizagem
Explore outros laboratórios em docs.oracle.com/learn ou acesse mais conteúdo de aprendizado gratuito no canal Oracle Learning YouTube. Além disso, visite education.oracle.com/learning-explorer para se tornar um Oracle Learning Explorer.
Para obter a documentação do produto, visite o Oracle Help Center.
Implement Simple Hybrid Search for Retrieval Augmented Generation using Oracle Database 23ai
G19806-01
November 2024