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 |
from twikit import Client from random import randint import json import time def get_json_tweet(t, parentid): return { 'created_at': t.created_at, 'id': t.id, 'parent' : parentid, 'full_text': t.full_text, 'created_at': t.created_at, 'text': t.text, 'lang': t.lang, 'in_reply_to': t.in_reply_to, 'quote_count': t.quote_count, 'reply_count': t.reply_count, 'favorite_count': t.favorite_count, 'view_count': t.view_count, 'hashtags': t.hashtags, 'user' : { 'id' : t.user.id, 'name' : t.user.name, 'screen_name ' : t.user.screen_name , 'url ' : t.user.url , }, } def get_replies(id, total_replies, recordTweetid): tweet = client.get_tweet_by_id(id) if( tweet.reply_count == 0): return # Get all replies all_replies = [] tweets = tweet.replies all_replies += tweets while len(tweets) != 0: try: time.sleep(randint(10,20)) tweets = tweets.next() all_replies += tweets except IndexError: print("Array Index error") break print(len(all_replies)) print(all_replies) for t in all_replies: jsonTweet = get_json_tweet(t, id) if (not t.id in recordTweetid) and ( t.in_reply_to == id): time.sleep(randint(10,20)) get_replies(t.id, total_replies, recordTweetid) f.write(',\n') json.dump(jsonTweet, f, ensure_ascii=False, indent=4) client = Client('en-US') ## You can comment this `login`` part out after the first time you run the script (and you have the `cookies.json`` file) client.login( auth_info_1='username', password='secret', ) client.save_cookies('cookies.json'); # client.load_cookies(path='cookies.json'); replies = [] recordTweetid = [] with open('data2.json', 'a', encoding='utf-8') as f: get_replies('1775913633064894669', replies, 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": null, "text": "@kelseyhightower SOCKS! I will throw millions of dollars at the first company to offer me socks!\n\nImportant to note here: I don’t have millions of dollars! \n\nI think I might have a problem.", "lang": "en", "in_reply_to": "1775913633064894669", "quote_count": 1, "reply_count": 3, "favorite_count": 23, "view_count": "4658", "hashtags": [], "user": { "id": "4324751", "name": "Josh Long", "screen_name ": "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 |
curl https://api.openai.com/v1/embeddings -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Content-Type: application/json" \ -d '{"input": " SOCKS! I will throw millions of dollars at the first company to offer me socks!\n\nImportant to note here: I don’t have millions of dollars! \n\nI think I might have a problem.", "model": "text-embedding-ada-002"}' { "object": "list", "data": [ { "object": "embedding", "index": 0, "embedding": [ -0.008340064, -0.03142008, 0.01558878, ... 0.0007338819, -0.01672055 ] } ], "model": "text-embedding-ada-002", "usage": { "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, db_username, db_password): """Connect to couchbase""" from couchbase.cluster import Cluster from couchbase.auth import PasswordAuthenticator from couchbase.options import ClusterOptions from datetime import timedelta auth = PasswordAuthenticator(db_username, db_password) options = ClusterOptions(auth) connect_string = connection_string cluster = Cluster(connect_string, options) # Wait until the cluster is ready for use. cluster.wait_until_ready(timedelta(seconds=5)) return cluster if name == "__main__": # Load environment variables DB_CONN_STR = os.getenv("DB_CONN_STR") DB_USERNAME = 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") # Connect to Couchbase Vector Store cluster = connect_to_couchbase(DB_CONN_STR, DB_USERNAME, DB_PASSWORD) bucket = cluster.bucket(DB_BUCKET) scope = bucket.scope(DB_SCOPE) collection = scope.collection(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 |
from openai import OpenAI def get_embedding(text, model="text-embedding-ada-002"): text = text.replace("\n", " ") return client.embeddings.create(input = [text], model=model).data[0].embedding client = 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 |
# Open the JSON file and load the tweets as a JSON array in data with open('data.json') as f: data = json.load(f) # Loop to create the object from JSON for tweet in data: text = tweet['text'] full_text = tweet['full_text'] id = tweet['id'] if full_text is not None: embedding = get_embedding(full_text) textToEmbed = full_text else: embedding = get_embedding(text) textToEmbed = text document = { "metadata": tweet, "text": textToEmbed, "embedding": embedding } collection.upsert(key = id, value = document) |
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 |
query = "Should we throw millions of dollars to buy SOCKs for developer marketing ?" queryEmbedding = get_embedding(query) |
É 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") # Fulltext Index Name # This is the Vector Search Query search_req = search.SearchRequest.create( VectorSearch.from_vector_query( VectorQuery( "Embedding", # JSON property name containing the embedding to compare to queryEmbedding, # our query embedding 5, # maximum number of results ) ) ) # Execute the Vector Search Query against the selected scope result = scope.search( INDEX_NAME, # Fulltext Index Name search_req, SearchOptions( show_request=True, log_request=True ), ).rows() for row in result: print("Found tweet \"{}\" ".format(row)) |
Como quero ver o que estou fazendo, estou ativando os registros do Couchbase SDK configurando essa variável de ambiente:
|
1 |
export PYCBC_LOG_LEVEL=info |
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 ➜ /workspaces/rag-demo-x (main) $ python read_vectorize_store_query_json.py Traceback (most recent call last): File "/workspaces/rag-demo-x/read_vectorize_store_query_json.py", line 167, in <module> for row in result: File "/home/vscode/.local/lib/python3.11/site-packages/couchbase/search.py", line 136, in __next__ raise ex File "/home/vscode/.local/lib/python3.11/site-packages/couchbase/search.py", line 130, in __next__ return self._get_next_row() ^^^^^^^^^^^^^^^^^^^^ File "/home/vscode/.local/lib/python3.11/site-packages/couchbase/search.py", line 121, in _get_next_row raise ErrorMapper.build_exception(row) couchbase.exceptions.QueryIndexNotFoundException: QueryIndexNotFoundException(<ec=17, category=couchbase.common, message=index_not_found (17), context=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', 'method': 'POST', 'path': '/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 Source=/couchbase-python-client/src/search.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 ➜ /workspaces/rag-demo-x (main) $ python read_vectorize_store_query_json.py Found tweet "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775920020377502191', score=0.6803812980651855, fields=None, sort=[], locations=None, fragments={}, explanation={})" Found tweet "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775925931791745392', score=0.4303199052810669, fields=None, sort=[], locations=None, fragments={}, explanation={})" Found tweet "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775921934645006471', score=0.3621498942375183, fields=None, sort=[], locations=None, fragments={}, explanation={})" Found tweet "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1776058836278727024', score=0.3274463415145874, fields=None, sort=[], locations=None, fragments={}, explanation={})" Found tweet "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 |
result = scope.search( INDEX_NAME, search_req, SearchOptions( fields=["metadata.text"], show_request=True, log_request=True ), ).rows() |
|
1 2 3 4 5 6 |
@ldoguin ➜ /workspaces/rag-demo-x (main) $ python read_vectorize_store_query_json.py Found tweet "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775920020377502191', score=0.6803812980651855, fields={'metadata.text': '@kelseyhightower SOCKS! I will throw millions of dollars at the first company to offer me socks!\n\nImportant to note here: I don’t have millions of dollars! \n\nI think I might have a problem.'}, sort=[], locations=None, fragments={}, explanation={})" Found tweet "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775925931791745392', score=0.4303199052810669, fields={'metadata.text': "@kelseyhightower If your t-shirt has a pleasant abstract design on it where the logo of your company isn't very obvious, I will wear that quite happily (thanks, Twilio)\n\nI also really like free socks"}, sort=[], locations=None, fragments={}, explanation={})" Found tweet "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775921934645006471', score=0.3621498942375183, fields={'metadata.text': "@kelseyhightower For some reason, devs think they aren't influenced by marketing even if they are😅\n\nI'm influenced by social media & fomo. If a lot of developers start talking about some framework or tool, I look into it\n\nI also look into things that may benefit my career in the future"}, sort=[], locations=None, fragments={}, explanation={})" Found tweet "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1776058836278727024', score=0.3274463415145874, fields={'metadata.text': "@kelseyhightower Have a good product. That's the best marketing there is!"}, sort=[], locations=None, fragments={}, explanation={})" Found tweet "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775979601862307872', score=0.32539570331573486, fields={'metadata.text': '@kelseyhightower From a security standpoint, marketing that works on me:\n\nShowing strong technical expertise. If you’re of the few shops that consistently puts out good research and quality writeups? When I’m looking at vendors, I’m looking at you. When I’m not looking, I’m noting it for later'}, 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!