Porque ¿quién tiene tiempo ? (también la parte 1 porque me llevó más lejos de lo que esperaba 😬)
Couchbase ha introducido recientemente compatibilidad con la búsqueda vectorial. Y he estado buscando una excusa para jugar con él. Resulta que hace poco hubo un gran hilo en Twitter sobre el marketing para desarrolladores. Me identifico con la mayor parte de lo que hay allí. Es un hilo fantástico. Podría resumirlo para asegurarme de que mis compañeros de equipo puedan sacarle el máximo partido en poco tiempo. Podría escribir ese resumen manualmente. O esa podría ser la excusa que estaba buscando.
Pidamos a un LLM, Large Language Model, que resuma este brillante hilo para mí, y en beneficio de los demás. En teoría, las cosas deberían ir como sigue:
-
- Obtener los tweets
- Transformarlos en vectores gracias a un LLM
- Almacenamiento de tuits y vectores en Couchbase
- Crear un índice para consultarlas
- Pregunta algo al LLM
- Transfórmalo en un vector
- Realiza una búsqueda vectorial para contextualizar el LLM
- Crear la pregunta LLM a partir de la pregunta y el contexto
- Obtenga una respuesta fantástica
Se trata básicamente de un flujo de trabajo RAG. RAG son las siglas en inglés de Generación Aumentada de Recuperación. Permite a los desarrolladores crear aplicaciones basadas en LLM más precisas y robustas proporcionando contexto.
Extracción de datos de Twitter
Lo primero es lo primero, obtener datos de Twitter. Esta es la parte más difícil si no estás suscrito a su API. Pero con un poco de buen desguace de edad, todavía se puede hacer algo decente. Probablemente no 100% exacto, pero decente. Así que vamos a ello.
Conseguir mi IDE favorito, con el Plugin Couchbase instalado, creo un nuevo script de Python y empiezo a jugar con twikituna biblioteca de Twitter scraper. Todo funciona muy bien hasta que rápidamente recibo un error HTTP 429. Demasiadas peticiones. He desguazado demasiado. Me han pillado. Un par de cosas para mitigar eso.
-
- En primer lugar, asegúrate de almacenar tu cookie de autenticación en un archivo y reutilizarla, en lugar de volver a iniciar sesión frenéticamente como hice yo.
- Segundo, cámbiate a un IDE online, podrás cambiar de IP más fácilmente.
- Tercero, introduce el tiempo de espera y hazlo aleatorio. No sé si la parte aleatoria ayuda, pero por qué no, es fácil.
El guión final tiene este aspecto:
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 importar Cliente de al azar importar randint importar json importar tiempo def get_json_tweet(t, parentid): devolver { "fecha_creada: t.fecha_de_creación, id: t.id, "padre : parentid, texto_completo: t.texto_completo, "fecha_creada: t.fecha_de_creación, texto: t.texto, lang: t.lang, in_reply_to: t.en_respuesta_a, recuento_citas: t.recuento_de_citas, 'reply_count': t.recuento_de_respuestas, cuenta_favoritos: t.recuento_de_favoritos, 'view_count': t.view_count, hashtags: t.hashtags, usuario : { id : t.usuario.id, nombre : t.usuario.nombre, 'nombre_de_pantalla ' : t.usuario.pantalla_nombre , url : t.usuario.url , }, } def obtener_respuestas(id, respuestas_totales, recordTweetid): tuitee = cliente.get_tweet_by_id(id) si( tuitee.recuento_de_respuestas == 0): devolver # Obtener todas las respuestas todas_las_respuestas = [] tuitea = tuitee.responde todas_las_respuestas += tuitea mientras que len(tuitea) != 0: pruebe: tiempo.dormir(randint(10,20)) tuitea = tuitea.siguiente() todas_las_respuestas += tuitea excepto Error de índice: imprimir("Array Index error") romper imprimir(len(todas_las_respuestas)) imprimir(todas_las_respuestas) para t en todas_las_respuestas: jsonTweet = get_json_tweet(t, id) si (no t.id en recordTweetid) y ( t.en_respuesta_a == id): tiempo.dormir(randint(10,20)) obtener_respuestas(t.id, respuestas_totales, recordTweetid) f.escriba a(',\n') json.vertedero(jsonTweet, f, garantizar_ascii=Falso, sangría=4) cliente = Cliente(es-US) ## Puedes comentar esta parte `login`` después de la primera vez que ejecutes el script (y tengas el archivo `cookies.json``) cliente.inicio de sesión( auth_info_1=nombre de usuario, contraseña=secreto, ) cliente.guardar_galletas(cookies.json); # client.load_cookies(path='cookies.json'); responde = [] recordTweetid = [] con abra(datos2.json, 'a', codificación=utf-8) como f: obtener_respuestas('1775913633064894669', responde, recordTweetid) |
Fue un poco doloroso evitar el 429, pasé por varias iteraciones pero al final conseguí algo que en su mayor parte funciona. Sólo necesitaba añadir el corchete de inicio y final para convertirlo en un array JSON válido:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
[ { "fecha_creada": "Thu Apr 04 16:15:02 +0000 2024", "id": "1775920020377502191", "texto_completo": null, "texto": "@kelseyhightower ¡CALCETINES! Tiraré millones de dólares a la primera empresa que me ofrezca calcetines: ¡No tengo millones de dólares! \Creo que tengo un problema"., "lang": "es", "in_reply_to": "1775913633064894669", "quote_count": 1, "reply_count": 3, "cuenta_favoritos": 23, "view_count": "4658", "hashtags": [], "usuario": { "id": "4324751", "nombre": "Josh Long", "nombre_de_pantalla": "starbuxman", " url ": "https://t.co/PrSomoWx53" } }, ... ] |
Es evidente que Josh tiene razón, los calcetines están en el centro de lo que hacemos en marketing para desarrolladores, junto con la ironía.
Ahora tengo un archivo que contiene una serie de documentos JSON, todos con dev marketing hot takes. ¿Y ahora qué?
Convertir tuits en vectores
Para que pueda ser utilizado por un LLM como contexto adicional, es necesario transformarlo en un vector, o incrustación. Básicamente es una matriz de valores decimales entre 0 y 1. Todo esto permitirá RAG, Retrieval Augmented Generation. No es universal, cada LLM tiene su propia representación de un objeto (como datos de texto, audio o vídeo). Siendo extremadamente perezoso e ignorante de lo que ocurre en ese espacio, elegí OpenAI/ChatGPT. Es como si cada semana salieran más modelos de los que teníamos frameworks de JavaScript en 2017.
De todos modos, creé mi cuenta OpenAI, creé una clave API, añadí un par de dólares porque aparentemente no puedes usar su API si no lo haces, incluso las cosas gratis. Entonces estaba listo para transformar los tweets en vectores. El camino más corto para obtener la incrustación a través de la API es usar curl. Se verá así:
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 |
rizo https://api.openai.com/v1/incrustaciones -H "Autorización: Portador $OPENAI_API_KEY" \ -H "Content-Type: application/json" \ -d '{"input": "¡CALCETINES! Tiraré millones de dólares a la primera compañía que me ofrezca calcetines: ¡No tengo millones de dólares! \Creo que tengo un problema.", "model": "text-embedding-ada-002"}' { "objeto": "lista", "datos": [ { "objeto": "incrustación", "índice": 0, "incrustación": [ -0.008340064, -0.03142008, 0.01558878, ... 0.0007338819, -0.01672055 ] } ], "modelo": "text-embedding-ada-002", "uso": { "prompt_tokens": 40, "total_tokens": 40 } } |
Aquí se puede ver que la entrada JSON tiene un campo de entrada que se transformará en un vector, y el campo de modelo que hace referencia al modelo que se utilizará para transformar el texto en un vector. La salida devuelve el vector, el modelo utilizado y las estadísticas de uso de la API.
Fantástico, ¿y ahora qué? Convertirlos en vectores no es barato. Es mejor almacenarlos en una base de datos para reutilizarlos más tarde. Además, puedes conseguir fácilmente algunas bonitas funciones añadidas, como la búsqueda híbrida.
Hay un par de maneras de verlo. Hay una manera manual tediosa que es genial para aprender cosas. Y luego está el uso de bibliotecas y herramientas que hacen la vida más fácil. Yo en realidad fui directamente usando Langchain pensando que me haría la vida más fácil, y así fue, hasta que me perdí un "poco". Así que, para nuestro beneficio colectivo de aprendizaje, vamos a empezar con la forma manual. Tengo un array de documentos JSON, necesito vectorizar su contenido, almacenarlo en Couchbase, y luego podré consultarlos con otro vector.
Cargar los tweets en un almacén vectorial como Couchbase
Voy a usar Python porque siento que tengo que mejorar en ello, aunque podemos ver implementaciones de Langchain en Java o JavaScript. Y lo primero que quiero abordar es cómo conectarse a 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(cadena_de_conexión, db_username, db_contraseña): """Conectarse a couchbase"""" de couchbase.grupo importar Grupo de couchbase.auth importar PasswordAuthenticator de couchbase.opciones importar ClusterOptions de datetime importar timedelta auth = PasswordAuthenticator(db_username, db_contraseña) opciones = ClusterOptions(auth) conectar_cadena = cadena_de_conexión grupo = Grupo(conectar_cadena, opciones) # Espere hasta que el clúster esté listo para su uso. grupo.wait_until_ready(timedelta(segundos=5)) devolver grupo si nombre == "__main__": # Cargar variables de entorno DB_CONN_STR = os.getenv("DB_CONN_STR") NOMBRE DE USUARIO DE BASE DE DATOS = os.getenv("DB_USERNAME) DB_PASSWORD = os.getenv("DB_PASSWORD") DB_BUCKET = os.getenv("DB_BUCKET") DB_SCOPE = os.getenv("DB_SCOPE") DB_COLECCIÓN = os.getenv("DB_COLLECTION") # Conectarse al almacén vectorial Couchbase grupo = connect_to_couchbase(DB_CONN_STR, NOMBRE DE USUARIO DE BASE DE DATOS, DB_PASSWORD) cubo = grupo.cubo(DB_BUCKET) alcance = cubo.alcance(DB_SCOPE) colección = alcance.colección(DB_COLECCIÓN) |
En este código se puede ver el connect_to_couchbase que acepta un método cadena de conexión, nombre de usuario y contraseña. Todos ellos son proporcionados por las variables de entorno cargadas al principio. Una vez que tenemos el objeto cluster podemos obtener el bucket, scope y collection asociados. Si no estás familiarizado con Couchbase, las colecciones son similares a una tabla RDBMS. Los ámbitos pueden tener tantas colecciones y cubos como ámbitos. Esta granularidad es útil por varias razones (multi-tenancy, sincronización más rápida, backup, etc.).
Una cosa más antes de obtener la colección. Necesitamos código para transformar el texto en vectores. Usando el cliente OpenAI se ve así:
1 2 3 4 5 6 7 |
de openai importar OpenAI def get_embedding(texto, modelo="text-embedding-ada-002"): texto = texto.sustituir("\n", " ") devolver cliente.incrustaciones.crear(entrada = [texto], modelo=modelo).datos[0].incrustación cliente = OpenAI() |
Esto hará un trabajo similar a la llamada anterior rizo. Sólo asegúrese de que tiene el OPENAI_API_KEY para que el cliente funcione.
Ahora vamos a ver cómo crear un documento Couchbase a partir de un tweet JSON, con la incrustación generada.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# Abre el fichero JSON y carga los tweets como un array JSON en data con abra(data.json) como f: datos = json.carga(f) # Bucle para crear el objeto a partir de JSON para tuitee en datos: texto = tuitee[texto] texto_completo = tuitee[texto_completo] id = tuitee[id] si texto_completo es no Ninguno: incrustación = get_embedding(texto_completo) textToEmbed = texto_completo si no: incrustación = get_embedding(texto) textToEmbed = texto documento = { "metadatos": tuitee, "texto": textToEmbed, "incrustación": incrustación } colección.upsert(clave = id, valor = documento) |
El documento tiene tres campos, metadatos contiene el tuit completo, texto es el texto transformado en cadena y incrustación es la incrustación generada con OpenAI. La clave será el id del tweet. Y upsert se utiliza para actualizar o insertar el documento si no existe.
Si ejecuto esto y me conecto a mi servidor Couchbase, veré que se crean documentos.
En este punto he extraído datos de Twitter, los he subido a Couchbase como un tweet por documento, con la incrustación OpenAI generada e insertada para cada tweet. Estoy listo para hacer preguntas para consultar documentos similares.
Ejecutar una búsqueda vectorial en Tweets
Y ahora es el momento de hablar de la búsqueda vectorial. ¿Cómo buscar tweets similares a un texto dado? Lo primero que hay que hacer es transformar el texto en un vector o embedding. Así que vamos a hacer la pregunta:
1 2 |
consulta = "¿Deberíamos tirar millones de dólares para comprar SOCKs para marketing de desarrolladores?" queryEmbedding = get_embedding(consulta) |
Ya está. El queryEmbedding contiene un vector que representa la consulta. A la consulta:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
NOMBRE_DEL_ÍNDICE = os.getenv("INDEX_NAME") # Índice de texto completo Nombre # Esta es la consulta de búsqueda de vectores búsqueda_requerida = busque en.Petición de búsqueda.crear( VectorSearch.from_vector_query( VectorQuery( "Incrustación", # Nombre de la propiedad JSON que contiene la incrustación con la que comparar queryEmbedding, # nuestra incrustación de consulta 5, # número máximo de resultados ) ) ) # Ejecutar la consulta de búsqueda vectorial en el ámbito seleccionado resultado = alcance.busque en( NOMBRE_DEL_ÍNDICE, # Índice de texto completo Nombre búsqueda_requerida, Opciones de búsqueda( mostrar_petición=Verdadero, log_request=Verdadero ), ).filas() para fila en resultado: imprimir("Encontrado tweet \"{}\" ".formato(fila)) |
Como quiero ver lo que estoy haciendo, estoy activando los logs del SDK de Couchbase configurando esta variable de entorno:
1 |
exportar PYCBC_LOG_LEVEL=información |
Si has seguido el proceso y todo va bien, deberías recibir un mensaje de error.
1 2 3 4 5 6 7 8 9 10 11 12 |
@ldoguin ➜ /espacios de trabajo/trapo-demo-x (principal) $ python read_vectorize_store_query_json.py Traceback (más reciente llame a último): Archivo "/workspaces/rag-demo-x/read_vectorize_store_query_json.py", línea 167, en <módulo> para fila en resultado: Archivo "/home/vscode/.local/lib/python3.11/site-packages/couchbase/search.py", línea 136, en __next__ subir ex Archivo "/home/vscode/.local/lib/python3.11/site-packages/couchbase/search.py", línea 130, en __next__ devolver auto._get_next_row() ^^^^^^^^^^^^^^^^^^^^ Archivo "/home/vscode/.local/lib/python3.11/site-packages/couchbase/search.py", línea 121, en _get_next_row subir ErrorMapper.build_exception(fila) couchbase.excepciones.QueryIndexNotFoundException: QueryIndexNotFoundException(<ec=17, categoría=couchbase.común, mensaje=indice_no_encontrado (17), contexto=SearchErrorContext({'ultimo_enviado_a': '3.87.133.123:18094', "último_despacho_desde: '172.16.5.4:38384', retry_attempts: 0, client_context_id: 'ebcca5-1b2f-c142-ccad-821b0f27e2ce0d', método: POST, camino: '/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}', tipo_contexto: SearchErrorContext}), C Fuente=/couchbase-python-cliente/src/busque en.cxx:552>) |
Y esto está bien porque obtenemos un QueryIndexNotFoundException. Está buscando un índice que aún no existe. Así que tenemos que crearlo. Puede iniciar sesión en su clúster en Capella y seguir adelante:
Una vez que tengas el índice, puedes ejecutarlo de nuevo y deberías obtener esto:
1 2 3 4 5 6 |
@ldoguin ➜ /espacios de trabajo/trapo-demo-x (principal) $ python read_vectorize_store_query_json.py Encontrado tuitee "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775920020377502191', score=0.6803812980651855, fields=None, sort=[], locations=None, fragments={}, explanation={})" Encontrado tuitee "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775925931791745392', score=0.4303199052810669, fields=None, sort=[], locations=None, fragments={}, explanation={})" Encontrado tuitee "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775921934645006471', score=0.3621498942375183, fields=None, sort=[], locations=None, fragments={}, explanation={})" Encontrado tuitee "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1776058836278727024', score=0.3274463415145874, fields=None, sort=[], locations=None, fragments={}, explanation={})" Encontrado tuitee "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775979601862307872', score=0.32539570331573486, fields=None, sort=[], locations=None, fragments={}, explanation={}" |
Obtenemos Fila de búsqueda que contienen el índice utilizado, la clave del documento, la puntuación relacionada y un montón de campos vacíos. Puede ver que esto también está ordenado por puntuacióny da el tweet más cercano a la consulta que ha encontrado.
¿Cómo sabemos si ha funcionado? Lo más rápido es buscar el documento con nuestro plugin IDE. Si utiliza VSCode o cualquier JetBrains IDE, debería ser bastante fácil. También puedes iniciar sesión en Couchbase Capella y encontrarlo allí.
O podemos modificar el índice de búsqueda para almacenar el campo de texto y los metadatos asociados, y volver a ejecutar la consulta:
1 2 3 4 5 6 7 8 9 |
resultado = alcance.busque en( NOMBRE_DEL_ÍNDICE, búsqueda_requerida, Opciones de búsqueda( campos=["metadatos.texto"], mostrar_petición=Verdadero, log_request=Verdadero ), ).filas() |
1 2 3 4 5 6 |
@ldoguin ➜ /espacios de trabajo/trapo-demo-x (principal) $ python read_vectorize_store_query_json.py Encontrado tuitee "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775920020377502191', score=0.6803812980651855, fields={'metadata.text': '@kelseyhightower SOCKS! Tiraré millones de dólares a la primera empresa que me ofrezca calcetines: ¡No tengo millones de dólares! \Creo que tengo un problema.'}, sort=[], locations=None, fragments={}, explanation={})" Encontrado tuitee "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775925931791745392', score=0.4303199052810669, fields={'metadata.text': "@kelseyhightower Si su t-camisa tiene a agradable abstracto diseño en it donde el logotipo de su empresa es't muy obvio, I se desgaste que muy felizmente (gracias, Twilio)\n\nI también realmente como gratis calcetines"}, sort=[], locations=None, fragments={}, explanation={})" Encontrado tuitee "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775921934645006471', score=0.3621498942375183, fields={'metadata.text': "@kelseyhightower Para algunos motivo, devs piense en ellos aren't influenciado por la comercialización, incluso si se😅n\nI'm influenciado por social medios de comunicación & fomo. Si a lote de desarrolladores iniciar hablando acerca de algunos marco o herramienta, I mira en it\n\nI también mira en cosas que mayo beneficio mi carrera en el futuro"}, sort=[], locations=None, fragments={}, explanation={})" Encontrado tuitee "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1776058836278727024', score=0.3274463415145874, fields={'metadata.text': "@kelseyhightower Visite a bien producto. Que's el mejor marketing allí es!"}, sort=[], locations=None, fragments={}, explanation={})" Encontrado tuitee "SearchRow(index='default._default.my_index_6933ea565b622355_4c1c5584', id='1775979601862307872', score=0.32539570331573486, fields={'metadata.text': '@kelseyhightower Desde el punto de vista de la seguridad, el marketing que me funciona:\n\nMostrar una gran experiencia técnica. Si usted es de las pocas tiendas que constantemente pone a cabo una buena investigación y escritos de calidad? Cuando busco proveedores, te busco a ti. Cuando no estoy mirando, estoy tomando nota para más tarde'}, sort=[], locations=None, fragments={}, explanation={})" |
Conclusión
Así que funcionó, el tweet de Josh sobre calcetines aparece en la parte superior de la búsqueda. Ahora ya sabes cómo scrapear twitter, transformar tweets en vectores, almacenarlos, indexarlos y consultarlos en Couchbase. ¿Qué tiene esto que ver con LLM y la IA? Más información en el próximo post.