Porque quem tem tempo? (também parte 1 porque me levou mais longe do que eu esperava 😬)
O Couchbase apresentou recentemente suporte para pesquisa vetorial. E eu estava procurando uma desculpa para brincar com ele. Acontece que recentemente houve um ótimo tópico no Twitter sobre marketing para desenvolvedores. Eu me identifico com a maior parte do que está lá. É um tópico fantástico. Eu poderia resumi-lo para garantir que meus colegas de equipe possam tirar o melhor proveito dele em pouco tempo. Por exemplo, eu poderia escrever esse resumo manualmente. Ou essa pode ser a desculpa que eu estava procurando.
Vamos pedir a um LLM (Large Language Model) que resuma essa brilhante discussão para mim e para o benefício de outras pessoas. Em teoria, as coisas devem acontecer da seguinte forma:
-
- Obtendo os tweets
- Transformando-os em vetores graças a um LLM
- Armazenamento do tweet e dos vetores no Couchbase
- Criação de um índice para consultá-los
- Pergunte algo ao LLM
- Transforme isso em um vetor
- Execute uma pesquisa de vetores para obter algum contexto para o LLM
- Criar o prompt do LLM a partir da pergunta e do contexto
- Obtenha uma resposta fantástica
Esse é basicamente um fluxo de trabalho RAG. RAG significa Retrieval Augmented Generation (Geração Aumentada de Recuperação). Ele permite que os desenvolvedores criem aplicativos baseados em LLM mais precisos e robustos, fornecendo contexto.
Extração de dados do Twitter
A primeira coisa a fazer é obter dados do Twitter. Na verdade, essa é a parte mais difícil se você não assinar a API deles. Mas com um bom e velho trabalho de sucata, você ainda pode fazer algo decente. Provavelmente não é 100% preciso, mas é decente. Então, vamos a isso.
Obtendo meu IDE favorito, com o Plug-in do Couchbase instalado, crio um novo script Python e começo a brincar com twikituma biblioteca de coleta de dados do Twitter. Tudo funciona bem até que recebo rapidamente um erro HTTP 429. Muitas solicitações. Tenho me esforçado demais. Fui pego. Algumas coisas para atenuar isso.
-
- Primeiro, certifique-se de armazenar o cookie de autenticação em um arquivo e reutilizá-lo, em vez de refazer o login freneticamente, como eu fiz.
- Em segundo lugar, mude para um IDE on-line, pois você poderá alterar o IP com mais facilidade.
- Terceiro, introduza o tempo de espera e torne-o aleatório. Não tenho certeza se a parte aleatória ajuda, mas por que não, é fácil.
O script final tem a seguinte aparência:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
de twikit importação Cliente de aleatório importação randint importação json importação tempo def get_json_tweet(t, parentid): retorno { 'created_at': t.created_at, 'id': t.id, 'pai' : parentid, 'full_text': t.full_text, 'created_at': t.created_at, 'texto': t.texto, 'lang': t.lang, 'in_reply_to': t.em_resposta_ao, 'quote_count': t.quote_count, 'reply_count': t.reply_count, 'favorite_count': t.favorite_count, 'view_count': t.view_count, 'hashtags': t.hashtags, 'usuário' : { 'id' : t.usuário.id, "nome : t.usuário.nome, 'nome_da_tela ' : t.usuário.tela_nome , 'url ' : t.usuário.url , }, } def get_replies(id, total_replies, recordTweetid): tuíte = cliente.get_tweet_by_id(id) se( tuíte.reply_count == 0): retorno # Obter todas as respostas todas_respostas = [] tweets = tuíte.respostas todas_respostas += tweets enquanto len(tweets) != 0: tentar: tempo.dormir(randint(10,20)) tweets = tweets.próxima() todas_respostas += tweets exceto IndexError: impressão("Erro de índice de matriz") quebra impressão(len(todas_respostas)) impressão(todas_respostas) para t em todas_respostas: jsonTweet = get_json_tweet(t, id) se (não t.id em recordTweetid) e ( t.em_resposta_ao == id): tempo.dormir(randint(10,20)) get_replies(t.id, total_replies, recordTweetid) f.escrever(',\n') json.lixão(jsonTweet, f, ensure_ascii=Falso, recuo=4) cliente = Cliente('en-US') ## Você pode comentar essa parte do `login` após a primeira vez que executar o script (e você tiver o arquivo `cookies.json`) cliente.login( auth_info_1='nome de usuário', senha="secreto, ) cliente.salvar_cookies('cookies.json'); # client.load_cookies(path='cookies.json'); respostas = [] recordTweetid = [] com aberto('data2.json', 'a', codificação='utf-8') como f: get_replies('1775913633064894669', respostas, recordTweetid) |
Foi um pouco doloroso evitar o 429, passei por várias iterações, mas no final consegui algo que funciona em sua maior parte. Só precisei adicionar o colchete inicial e final para transformá-lo em uma matriz JSON válida:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
[ { "created_at": "Thu Apr 04 16:15:02 +0000 2024", "id": "1775920020377502191", "full_text": nulo, "texto": "@kelseyhightower SOCKS! Jogarei milhões de dólares na primeira empresa que me oferecer meias!\n\nImportante observar aqui: Eu não tenho milhões de dólares! \n\nAcho que posso ter um problema.", "lang": "en", "in_reply_to": "1775913633064894669", "quote_count": 1, "reply_count": 3, "favorite_count": 23, "view_count": "4658", "hashtags": [], "usuário": { "id": "4324751", "name" (nome): "Josh Long", "nome_da_tela": "starbuxman", "url ": "https://t.co/PrSomoWx53" } }, ... ] |
Josh está obviamente certo, as meias estão no centro do que fazemos no marketing para desenvolvedores, juntamente com a ironia.
Agora, tenho um arquivo que contém uma matriz de documentos JSON, todos com dicas de marketing para desenvolvedores. O que vem a seguir?
Transformando tweets em vetores
Para garantir que possa ser usado por um LLM como contexto adicional, ele precisa ser transformado em um vetor, ou incorporação. Basicamente, é uma matriz de valores decimais entre 0 e 1. Tudo isso permitirá o RAG, Retrieval Augmented Generation. Isso não é universal, cada LLM tem sua própria representação de um objeto (como dados de texto, áudio ou vídeo). Por ser extremamente preguiçoso e não saber o que está acontecendo nesse espaço, escolhi OpenAI/ChatGPT. É como se houvesse mais modelos surgindo a cada semana do que tínhamos estruturas JavaScript em 2017.
De qualquer forma, criei minha conta na OpenAI, criei uma chave de API, adicionei alguns dólares porque, aparentemente, você não pode usar a API deles se não o fizer, mesmo as coisas gratuitas. Então, eu estava pronto para transformar tweets em vetores. O caminho mais curto para obter a incorporação por meio da API é usar o curl. Ele terá a seguinte aparência:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
enrolar https://API.openai.com/v1/incorporações -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Content-Type: application/json" \ -d '{"input": " SOCKS! Atirarei milhões de dólares para a primeira empresa que me oferecer meias! Eu não tenho milhões de dólares! \n\nAcho que posso ter um problema.", "model": "text-embedding-ada-002"}' { "objeto": "lista", "dados": [ { "objeto": "embedding" (incorporação), "índice": 0, "embedding" (incorporação): [ -0.008340064, -0.03142008, 0.01558878, ... 0.0007338819, -0.01672055 ] } ], "model" (modelo): "text-embedding-ada-002", "uso": { "prompt_tokens": 40, "total_tokens": 40 } } |
Aqui você pode ver que a entrada JSON tem um campo de entrada que será transformado em um vetor e o campo de modelo que faz referência ao modelo a ser usado para transformar o texto em um vetor. A saída fornece o vetor, o modelo usado e as estatísticas de uso da API.
Fantástico, e agora? Transformar esses dados em vetores não é barato. É melhor que sejam armazenados em um banco de dados para serem reutilizados posteriormente. Além disso, você pode obter facilmente alguns recursos adicionais interessantes, como a pesquisa híbrida.
Há algumas maneiras de ver isso. Há uma maneira manual tediosa que é ótima para aprender coisas. E há o uso de bibliotecas e ferramentas que facilitam a vida. Na verdade, fui direto ao ponto usando Langchain achando que isso facilitaria minha vida, e foi o que aconteceu, até que me perdi um pouco. Então, para nosso benefício coletivo de aprendizado, vamos começar com a maneira manual. Tenho uma matriz de documentos JSON, preciso vetorizar seu conteúdo, armazená-lo no Couchbase e, depois, poderei consultá-los com outro vetor.
Carregando os tweets em um Vector Store como o Couchbase
Vou usar Python porque sinto que preciso me aperfeiçoar nisso, embora possamos ver a implementação do Langchain em Java ou JavaScript. E a primeira coisa que quero abordar é como me conectar ao Couchbase:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
def connect_to_couchbase(connection_string, nome de usuário do db, senha_db): """Conecte-se ao couchbase""" de couchbase.agrupamento importação Aglomerado de couchbase.autenticação importação PasswordAuthenticator de couchbase.opções importação ClusterOptions de data e hora importação timedelta autenticação = PasswordAuthenticator(nome de usuário do db, senha_db) opções = ClusterOptions(autenticação) connect_string = connection_string agrupamento = Aglomerado(connect_string, opções) # Aguarde até que o cluster esteja pronto para uso. agrupamento.wait_until_ready(timedelta(segundos=5)) retorno agrupamento se nome == "__main__": # Carregar variáveis de ambiente DB_CONN_STR = os.getenv("DB_CONN_STR") NOME DE USUÁRIO DO BANCO = os.getenv("DB_USERNAME") DB_PASSWORD = os.getenv("DB_PASSWORD") DB_BUCKET = os.getenv("DB_BUCKET") DB_SCOPE = os.getenv("DB_SCOPE") DB_COLLECTION = os.getenv("DB_COLLECTION") # Conectar-se ao Couchbase Vector Store agrupamento = connect_to_couchbase(DB_CONN_STR, NOME DE USUÁRIO DO BANCO, DB_PASSWORD) balde = agrupamento.balde(DB_BUCKET) escopo = balde.escopo(DB_SCOPE) coleção = escopo.coleção(DB_COLLECTION) |
Nesse código, você pode ver o connect_to_couchbase que aceita um método string de conexão, nome de usuário e senha. Todos eles são fornecidos pelas variáveis de ambiente carregadas no início. Quando tivermos o objeto do cluster, poderemos obter o bucket, o escopo e a coleção associados. Se você não estiver familiarizado com o Couchbase, as coleções são semelhantes a uma tabela RDBMS. Os escopos podem ter tantas coleções e baldes quantos forem os escopos. Essa granularidade é útil por vários motivos (multilocação, sincronização mais rápida, backup etc.).
Mais uma coisa antes de obter a coleção. Precisamos de um código para transformar o texto em vetores. Usando o cliente OpenAI, ele tem a seguinte aparência:
1 2 3 4 5 6 7 |
de openai importação OpenAI def get_embedding(texto, modelo="text-embedding-ada-002"): texto = texto.substituir("\n", " ") retorno cliente.incorporações.criar(entrada = [texto], modelo=modelo).dados[0].incorporação cliente = OpenAI() |
Isso fará um trabalho semelhante ao da chamada de curl anterior. Apenas certifique-se de que você tenha o OPENAI_API_KEY definida para que o cliente funcione.
Agora vamos ver como criar um documento do Couchbase a partir de um tweet JSON, com a incorporação gerada.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# Abra o arquivo JSON e carregue os tweets como uma matriz JSON em dados com aberto('data.json') como f: dados = json.carregar(f) # Loop para criar o objeto a partir do JSON para tuíte em dados: texto = tuíte['texto'] full_text = tuíte['full_text'] id = tuíte['id'] se full_text é não Nenhum: incorporação = get_embedding(full_text) textToEmbed = full_text mais: incorporação = get_embedding(texto) textToEmbed = texto documento = { "metadata": tuíte, "texto": textToEmbed, "embedding" (incorporação): incorporação } coleção.upsert(chave = id, valor = documento) |
O documento tem três campos, metadados contém o tweet inteiro, texto é o texto transformado em uma string e incorporação é a incorporação gerada com o OpenAI. A chave será o ID do tweet. E upsert é usado para atualizar ou inserir o documento, caso ele não exista.
Se eu executar isso e me conectar ao meu servidor Couchbase, verei documentos sendo criados.
Neste ponto, extraí dados do Twitter, carreguei-os no Couchbase como um tweet por documento, com a incorporação do OpenAI gerada e inserida para cada tweet. Estou pronto para fazer perguntas para consultar documentos semelhantes.
Executar o Vector Search em Tweets
E agora é hora de falar sobre o Vector Search. Como pesquisar tweets semelhantes a um determinado texto? A primeira coisa a fazer é transformar o texto em um vetor ou incorporação. Então, vamos fazer a pergunta:
1 2 |
consulta = "Devemos gastar milhões de dólares para comprar SOCKs para o marketing do desenvolvedor?" queryEmbedding = get_embedding(consulta) |
É isso aí. O queryEmbedding contém um vetor que representa a consulta. Para a consulta:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
INDEX_NAME = os.getenv("INDEX_NAME") # Nome do índice de texto completo # Esta é a consulta de pesquisa de vetor search_req = pesquisa.Solicitação de pesquisa.criar( VectorSearch.from_vector_query( VectorQuery( "Incorporação", # Nome da propriedade JSON que contém a incorporação a ser comparada queryEmbedding, # nossa incorporação de consulta 5, # número máximo de resultados ) ) ) # Execute o Vector Search Query em relação ao escopo selecionado resultado = escopo.pesquisa( INDEX_NAME, # Nome do índice de texto completo search_req, Opções de pesquisa( show_request=Verdadeiro, solicitação de registro=Verdadeiro ), ).linhas() para fila em resultado: impressão("Tweet encontrado \"{}\" ".formato(fila)) |
Como quero ver o que estou fazendo, estou ativando os registros do Couchbase SDK configurando essa variável de ambiente:
1 |
exportação PYCBC_LOG_LEVEL=informações |
Se você estiver acompanhando o processo e tudo correr bem, deverá receber uma mensagem de erro!
1 2 3 4 5 6 7 8 9 10 11 12 |
@ldoguin ➜ /espaços de trabalho/trapo-demonstração-x (principal) $ python read_vectorize_store_query_json.py Retrocesso (mais recente chamada último): Arquivo "/workspaces/rag-demo-x/read_vectorize_store_query_json.py", linha 167, em <módulo> para fila em resultado: Arquivo "/home/vscode/.local/lib/python3.11/site-packages/couchbase/search.py", linha 136, em __próximo__ aumentar ex Arquivo "/home/vscode/.local/lib/python3.11/site-packages/couchbase/search.py", linha 130, em __próximo__ retorno autônomo._get_next_row() ^^^^^^^^^^^^^^^^^^^^ Arquivo "/home/vscode/.local/lib/python3.11/site-packages/couchbase/search.py", linha 121, em _get_next_row aumentar Mapeador de erros.build_exception(fila) couchbase.exceções.QueryIndexNotFoundException: QueryIndexNotFoundException(<ec=17, categoria=couchbase.comum, mensagem=index_not_found (17), contexto=SearchErrorContext({'last_dispatched_to': '3.87.133.123:18094', 'last_dispatched_from': '172.16.5.4:38384', 'retry_attempts': 0, 'client_context_id': 'ebcca5-1b2f-c142-ccad-821b0f27e2ce0d', 'método': 'POST', 'caminho': '/api/bucket/default/scope/_default/index/b/query', 'http_status': 400, 'http_body': '{"error":"rest_auth: preparePerms, err: index not found","request":{"ctl":{"timeout":75000},"explain":false,"knn":[{"field":"embedding","k":5,"vector":[0.022349120871154076,..,0.006140850435491819]}],"query":{"match_none":null},"showrequest":true}', 'context_type': 'SearchErrorContext'}), C Fonte=/couchbase-python-cliente/src/pesquisa.cxx:552>) |
E isso é bom porque temos um QueryIndexNotFoundException. Ele está procurando um índice que ainda não existe. Portanto, precisamos criá-lo. Você pode fazer login em seu cluster no Capella e acompanhar o processo:
Quando tiver o índice, você poderá executá-lo novamente e obterá o seguinte resultado:
1 2 3 4 5 6 |
@ldoguin ➜ /espaços de trabalho/trapo-demonstração-x (principal) $ python read_vectorize_store_query_json.py Encontrado tuíte "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775920020377502191', score=0.6803812980651855, fields=None, sort=[], locations=None, fragments={}, explanation={})" Encontrado tuíte "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775925931791745392', score=0.4303199052810669, fields=None, sort=[], locations=None, fragments={}, explanation={})" Encontrado tuíte "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775921934645006471', score=0.3621498942375183, fields=None, sort=[], locations=None, fragments={}, explanation={})" Encontrado tuíte "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1776058836278727024', score=0.3274463415145874, fields=None, sort=[], locations=None, fragments={}, explanation={})" Encontrado tuíte "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775979601862307872', score=0.32539570331573486, fields=None, sort=[], locations=None, fragments={}, explanation={}" |
Obtemos Linha de pesquisa que contêm o índice usado, a chave do documento, a pontuação relacionada e, em seguida, vários campos vazios. Você pode ver que isso também é ordenado por pontuaçãoe está fornecendo o tweet mais próximo da consulta encontrada.
Como podemos saber se funcionou? A coisa mais rápida a fazer é procurar o documento com nosso plug-in do IDE. Se você estiver usando o VSCode ou qualquer JetBrains IDE, deve ser bem fácil. Você também pode fazer login no Couchbase Capella e encontrá-lo lá.
Ou podemos modificar o índice de pesquisa para armazenar o campo de texto e os metadados associados e executar novamente a consulta:
1 2 3 4 5 6 7 8 9 |
resultado = escopo.pesquisa( INDEX_NAME, search_req, Opções de pesquisa( campos=["metadata.text"], show_request=Verdadeiro, solicitação de registro=Verdadeiro ), ).linhas() |
1 2 3 4 5 6 |
@ldoguin ➜ /espaços de trabalho/trapo-demonstração-x (principal) $ python read_vectorize_store_query_json.py Encontrado tuíte "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775920020377502191', score=0.6803812980651855, fields={'metadata.text': '@kelseyhightower SOCKS! Jogarei milhões de dólares na primeira empresa que me oferecer meias!\n\nImportante observar aqui: Eu não tenho milhões de dólares! \n\nAcho que posso ter um problema.'}, sort=[], locations=None, fragments={}, explanation={})" Encontrado tuíte "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775925931791745392', score=0.4303199052810669, fields={'metadata.text': "@kelseyhightower Se seu t-camisa tem a agradável abstrato design em ele onde o logotipo de seu empresa é't muito óbvio, I vontade desgaste que bastante felizmente (agradecimentos, Twilio)\n\nI também realmente como gratuito meias"}, sort=[], locations=None, fragments={}, explanation={})" Encontrado tuíte "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775921934645006471', score=0.3621498942375183, fields={'metadata.text': "@kelseyhightower Para alguns razão, desenvolvedores pensar eles são't influenciados pelo marketing, mesmo que sejam😅\n\nI'm influenciado por social mídia & fomo. Se a lote de desenvolvedores iniciar falante sobre alguns estrutura ou ferramenta, I olhar em ele\n\nI também olhar em coisas que pode benefício meu carreira em o futuro"}, sort=[], locations=None, fragments={}, explanation={})" Encontrado tuíte "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1776058836278727024', score=0.3274463415145874, fields={'metadata.text': "@kelseyhightower Ter a bom produto. Isso's o melhor marketing lá é!"}, sort=[], locations=None, fragments={}, explanation={})" Encontrado tuíte "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775979601862307872', score=0.32539570331573486, fields={'metadata.text': '@kelseyhightower Do ponto de vista da segurança, o marketing que funciona para mim:\n\nMostrar grande conhecimento técnico. Se você é uma das poucas lojas que sempre faz boas pesquisas e escreve artigos de qualidade? Quando estou procurando fornecedores, estou olhando para você. Quando não estou olhando, estou anotando para mais tarde'}, sort=[], locations=None, fragments={}, explanation={})" |
Conclusão
Então funcionou, o tweet de Josh sobre meias aparece no topo da pesquisa. Agora você já sabe como extrair dados do Twitter, transformar tweets em vetores, armazenar, indexar e consultá-los no Couchbase. O que isso tem a ver com LLM e IA? Falaremos mais sobre isso na próxima postagem!