Chat Towards Data Science: Construindo um Chatbot com Zilliz Cloud
Este artigo é a primeira parte da série de blog Chat Towards Data Science.
Na era da IA generativa, os desenvolvedores aproveitam grandes modelos de linguagem (LLMs) para aplicações mais inteligentes. No entanto, os LLMs são propensos a alucinações devido ao seu conhecimento limitado. Retrieval Augmented Generation (RAG) aborda esse problema de forma eficaz ao complementar os LLMs com conhecimento externo. Na minha série de blog Chat Towards Data Science, vou guiá-lo na criação de um chatbot baseado em RAG usando seu conjunto de dados como a espinha dorsal do conhecimento.
Na primeira parte da minha série de blog, vamos guiá-lo na criação de um chatbot para o site Towards Data Science, utilizando web scraping para criar uma base de conhecimento armazenada no Zilliz Cloud, um serviço de banco de dados vetorial totalmente gerenciado construído sobre o Milvus.
Coletando dados da web com BeautifulSoup4
O primeiro passo em qualquer projeto de machine learning (ML) é reunir os dados necessários. Para este projeto, empregamos técnicas de web scraping para coletar dados para nossa base de conhecimento. Utilizamos a biblioteca requests para buscar páginas da web e, em seguida, usamos BeautifulSoup4 (uma biblioteca que extrai informações de páginas da web) para analisar as informações HTML e extrair os parágrafos.
Preparando BeautifulSoup4 e Requests para Web Scraping
Para começar, instale BeautifulSoup executando pip install beautifulsoup4 sentence-transformers. Precisamos apenas de duas importações para esta seção: requests e BeautifulSoup. Em seguida, criamos um dicionário de URLs que queremos extrair. Neste exemplo, extraímos apenas conteúdo do Towards Data Science, mas você também pode extrair de outros sites. Recuperamos dados de cada página de arquivo usando o formato mostrado no código abaixo.
import requests
from bs4 import BeautifulSoup
urls = {
'Towards Data Science': 'https://towardsdatascience.com/archive/{0}/{1:02d}/{2:02d}',
}
Também precisamos de duas funções auxiliares para nosso web scraping. A primeira função converte o número de dias do ano para um formato de mês e dia. A segunda recupera o número de aplausos (os aplausos mostram apoio a um artigo do Medium) de um artigo.
A função de conversão de dias é relativamente simples. Codificamos diretamente o número de dias de cada mês e usamos essa lista para realizar a conversão. Como este projeto de web scraping tem como alvo explicitamente o ano de 2023, não precisamos levar em conta anos bissextos. Sinta-se à vontade para modificá-la para anos diferentes, se preferir.
A função de contagem de aplausos representa os aplausos de um artigo do Medium. Alguns artigos podem receber milhares de aplausos, que o Medium denota com a letra "K." Portanto, precisamos considerar essa representação em nossa função.
# takes the number day of the year and returns month, day
def convert_day(day):
month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
m = 0
d = 0
while day > 0:
d = day
day -= month_days[m]
m += 1
return (m, d)
# converts the claps string
def get_claps(claps_str):
if (claps_str is None) or (claps_str == '') or (claps_str.split is None):
return 0
split = claps_str.split('K')
claps = float(split[0])
claps = int(claps*1000) if len(split) == 2 else int(claps)
return claps
Analisando a resposta de web scraping do BeautifulSoup4
Agora que configuramos os componentes necessários, podemos prosseguir com o web scraping. Nesta seção, importamos uma biblioteca adicional chamada "time.” Adicionei esta biblioteca porque encontrei erros 429 (muitas solicitações) durante o processo. Usando a biblioteca time, podemos introduzir um atraso entre o envio de solicitações. Além disso, utilizamos a biblioteca sentence transformers para obter nosso modelo de embeddings, especificamente um modelo MiniLM, do Hugging Face.
Como mencionado anteriormente, estamos extraindo dados apenas de 2023, portanto definimos o ano como 2023. Além disso, precisamos apenas dos dados do dia 1 (1º de janeiro) ao dia 243 (30 de agosto). No momento da extração, era início de setembro. Em retrospectiva, teria sido melhor extrair até o dia 244, mas, por enquanto, vamos prosseguir com o que eu já fiz. Na primeira parte da nossa função, até a primeira chamada de time.sleep(), configuramos os componentes necessários. Convertemos o iterador em um mês e dia, convertemos isso em uma string de data e então recuperamos a resposta HTML da URL do arquivo.
Depois de obter o HTML, nós o analisamos usando BeautifulSoup e procuramos elementos div com um nome de classe específico (indicado no código), o que indica que se trata de um artigo. A partir daí, analisamos o título, subtítulo, URL do artigo, número de aplausos, tempo de leitura e número de respostas. Em seguida, usamos requests novamente para recuperar o artigo a partir da URL do artigo.
Chamamos time.sleep() pela segunda vez para concluir a segunda seção deste loop for. Neste ponto, obtivemos a maior parte dos metadados necessários para cada artigo. Extraímos cada parágrafo e seu embedding correspondente usando nosso modelo Hugging Face. Então criamos um dicionário que contém todos os metadados daquele parágrafo e artigo específicos.
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("sentence-transformers/all-MiniLM-L12-v2")
import time
# data_batch = []
year = 2023
for i in range(1, 243):
month, day = convert_day(i)
date = '{0}-{1:02d}-{2:02d}'.format(year, month, day)
for publication, url in urls.items():
response = requests.get(url.format(year, month, day), allow_redirects=True)
if not response.url.startswith(url.format(year, month, day)):
continue
time.sleep(8)
page = response.content
soup = BeautifulSoup(page, 'html.parser')
articles = soup.find_all(
"div",
class_="postArticle postArticle--short js-postArticle js-trackPostPresentation js-trackPostScrolls")
for article in articles:
title = article.find("h3", class_="graf--title")
# print(title.contents)
if title is None:
continue
title = str(title.contents[0]).replace(u'\xA0', u' ').replace(u'\u200a', u' ')
# title = title.contents[0]
subtitle = article.find("h4", class_="graf--subtitle")
subtitle = str(subtitle.contents[0]).replace(u'\xA0', u' ').replace(u'\u200a', u' ') if subtitle is not None else ''
article_url = article.find_all("a")[3]['href'].split('?')[0]
try:
claps = get_claps(article.find_all("button")[1].contents[0])
except:
claps = 0
reading_time = article.find("span", class_="readingTime")
reading_time = 0 if reading_time is None else int(reading_time['title'].split(' ')[0])
responses = article.find_all("a")
if len(responses) == 7:
responses = responses[6].contents[0].split(' ')
if len(responses) == 0:
responses = 0
else:
responses = responses[0]
else:
responses = 0
article_res = requests.get(article_url)
time.sleep(8)
soup = bs4.BeautifulSoup(article_res.text)
paragraphs = soup.select('[class*="pw-post-body-paragraph"]')
for i, paragraph in enumerate(paragraphs):
embedding = model.encode(paragraph.text)
data_batch.append({
"_id": f"{article_url}+{i}",
"article_url": article_url,
"title": title,
"subtitle": subtitle,
"claps": claps,
"responses": responses,
"reading_time": reading_time,
"publication": publication,
"date": date,
"paragraph": paragraph.text,
"embedding": embedding
})
O último passo é serializar nosso arquivo com pickle.
import pickle
filename="TDS_8_30_2023"
with open(f'{filename}.pkl', 'wb') as f:
pickle.dump(data_batch, f)
Como são os nossos dados?
É sempre útil visualizar os dados. Abaixo está como os dados aparecem no Zilliz Cloud. Preste atenção aos embeddings, que representam o vetor. Geramos esses embeddings vetoriais a partir do texto na seção a seguir.
Ingerir dados do TDS em seu banco de dados vetorial
Depois que temos os dados, o próximo passo é enviá-los para um banco de dados vetorial. Neste projeto, utilizei um notebook separado para ingerir os dados no Zilliz Cloud, um serviço de banco de dados vetorial totalmente gerenciado construído sobre o Milvus, em vez de extraí-los do Towards Data Science.
Para inserir nossos dados no Zilliz Cloud, seguimos estes passos:
Conectar-se ao Zilliz Cloud.
Definir os parâmetros para nossa coleção.
Inserir os dados no Zilliz Cloud.
Configurando o Jupyter Notebook
Execute o comando pip install pymilvus python-dotenv para configurar nosso Jupyter Notebook e iniciar o processo de ingestão de dados. Uso a biblioteca dotenv para gerenciar minhas variáveis de ambiente como de costume. Para o pacote pymilvus, precisamos importar os seguintes módulos:
utility- para verificar o status das suas coleçõesconnections- para conectar-se à sua instância do MilvusFieldSchema- para definir o schema de um campoCollectionSchema- para definir o schema de uma coleçãoDataType- tipos de dados armazenados em um campoCollection- a forma como acessamos uma coleção
Então, abrimos os dados que serializamos anteriormente com pickle, recuperamos nossas variáveis de ambiente e nos conectamos ao Zilliz Cloud.
import pickle
import os
from dotenv import load_dotenv
from pymilvus import utility, connections, FieldSchema, CollectionSchema, DataType, Collection
filename="TDS_8_30_2023"
with open(f'{filename}.pkl', 'rb') as f:
data_batch = pickle.load(f)
load_dotenv()
zilliz_uri = os.getenv("ZILLIZ_URI")
zilliz_token = os.getenv("ZILLIZ_TOKEN")
connections.connect(
uri= zilliz_uri,
token= zilliz_token
)
Configurando seu banco de dados vetorial Zilliz e a ingestão de dados
Agora, precisamos configurar o Zilliz Cloud. Devemos criar uma coleção para armazenar e organizar os dados que extraímos do site TDS. Duas constantes são necessárias: dimensão e nome da coleção. A dimensão refere-se ao número de dimensões que nosso vetor tem. Neste caso, usamos o modelo MiniLM com 384 dimensões. O nome da coleção é autoexplicativo.
Com o novo recurso de schema dinâmico do Milvus, podemos simplesmente definir o campo de id e de embedding para uma coleção sem nos preocuparmos com o número de outros campos armazenados ou seus tipos de dados obrigatórios. No entanto, é vital lembrar os nomes específicos dos campos que mantivemos para recuperá-los corretamente.
Para este exemplo de projeto, usamos o recurso de schema dinâmico para definir apenas os campos ID e embedding. Em seguida, atribuímos esses campos a um schema e o schema a uma coleção. Depois, especificamos os parâmetros para o índice. Neste caso, usamos um índice de arquivo invertido (IVF) sem quantização (FLAT), uma distância euclidiana (L2) e 128 centroides para o índice.
IVF sem quantização é o método de indexação mais intuitivo. Essencialmente, estamos agrupando os vetores em 128 seções para consulta. 128 é um número arbitrário neste caso. É um hiperparâmetro que podemos ajustar dependendo de como forem nossos resultados. Heuristicamente, é um bom ponto de partida porque pode fornecer uma boa diversidade de clusters, além de reduzir a complexidade inicial da consulta. Depois de definirmos esses parâmetros, criamos o índice no campo de embedding e carregamos a coleção na memória.
DIMENSION=384
COLLECTION_NAME="tds_articles"
fields = [
FieldSchema(name='id', dtype=DataType.VARCHAR, max_length=200, is_primary=True),
FieldSchema(name='embedding', dtype=DataType.FLOAT_VECTOR, dim=DIMENSION)
]
schema = CollectionSchema(fields=fields, enable_dynamic_field=True)
collection = Collection(name=COLLECTION_NAME, schema=schema)
index_params = {
"index_type": "IVF_FLAT",
"metric_type": "L2",
"params": {"nlist": 128},
}
collection.create_index(field_name="embedding", index_params=index_params)
collection.load()
Depois de definirmos a coleção e carregá-la na memória, temos duas opções para inserir os dados:
Percorrer o lote de dados e inserir cada item individualmente
Inserir os dados em lotes
Depois de inserirmos todos os dados, é importante descarregar a coleção para indexá-la e garantir a consistência. A ingestão de grandes quantidades de dados pode levar algum tempo.
for data in data_batch:
collection.insert([data])
collection.flush()
Consultar segmentos de artigos da TDS
Agora, tudo está configurado e pronto para consultas. Neste post, manteremos esta parte simples vetorizando nossa consulta e procurando a correspondência mais próxima no Zilliz Cloud. Nos meus próximos posts, também usaremos LlamaIndex e LangChain.
Obtenha seu modelo HuggingFace e configure-o para consultar o Zilliz
Você deve obter o modelo de embeddings e configurar o banco de dados vetorial para consultar nossa base de conhecimento da Towards Data Science. Esta etapa requer a execução de um notebook separado. Usarei a biblioteca dotenv para gerenciar variáveis de ambiente. Além disso, precisamos usar o modelo MiniLM da Sentence Transformers. Para esta etapa, você pode reutilizar o código fornecido na seção Web Scraping.
import os
from dotenv import load_dotenv
from pymilvus import connections, Collection
load_dotenv()
zilliz_uri = os.getenv("ZILLIZ_URI")
zilliz_token = os.getenv("ZILLIZ_TOKEN")
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("sentence-transformers/all-MiniLM-L12-v2")
Executando uma consulta de busca vetorial
Agora, você precisa se conectar ao seu banco de dados vetorial e realizar uma busca. Neste projeto, nos conectaremos a uma instância do Zilliz Cloud e recuperaremos a coleção que criamos anteriormente, tds_articles. Para obter nossa consulta, solicitamos ao usuário que insira sua pergunta sobre a TDS até setembro de 2023.
Em seguida, usamos o modelo de embeddings da Hugging Face para codificar a consulta. Esse processo converte a pergunta do usuário em uma representação vetorial de 384 dimensões. Então usamos essa consulta codificada para pesquisar no banco de dados vetorial. Durante a busca, precisamos especificar o campo de vizinho mais próximo aproximado (anns_field), os parâmetros do índice, o limite desejado para o número de resultados de busca e os campos de saída que queremos.
Anteriormente, utilizamos o recurso de esquema dinâmico do Milvus para simplificar a definição do esquema de campos. Agora, ao pesquisarmos no banco de dados vetorial, é essencial incluir os campos dinâmicos desejados nos resultados da busca. Este cenário específico envolve solicitar o campo paragraph, que contém o texto de cada parágrafo nos artigos.
connections.connect(uri=zilliz_uri, token=zilliz_token)
collection = Collection(name="tds_articles")
query = input("What would you like to ask Towards Data Science's 2023 publications up to September? ")
embedding = model.encode(query)
closest = collection.search([embedding],
anns_field='embedding',
param={"metric_type": "L2",
"params": {"nprobe": 16}},
limit=2,
output_fields=["paragraph"])
print(closest[0][0])
print(closest[0][1])
Pedi ao aplicativo que fornecesse informações sobre grandes modelos de linguagem, e ele retornou as duas respostas a seguir. Embora essas respostas mencionem "modelos de linguagem" e incluam algumas informações relacionadas, elas não fornecem uma explicação detalhada sobre grandes modelos de linguagem. A segunda resposta é semanticamente semelhante, mas precisa ser suficientemente próxima do que solicitamos.
Adicionando a uma base de conhecimento de banco de dados vetorial
Até agora, criamos uma base de conhecimento sobre artigos do TDS usando Zilliz como nosso banco de dados vetorial. Embora tenhamos conseguido recuperar facilmente resultados de busca semanticamente semelhantes, eles só às vezes são o que precisamos. O próximo passo é aprimorar nossos resultados incorporando novos frameworks e tecnologias.
Na próxima seção, exploraremos a adição do LlamaIndex para rotear nossos resultados, semelhante ao roteamento que realizamos no meu blog anterior sobre Consultar múltiplos documentos usando LlamaIndex, LangChain e Milvus.
Resumo
Este tutorial aborda a criação de um chatbot para a publicação Towards Data Science. Ilustramos o processo de web scraping para criar e armazenar uma base de conhecimento em um banco de dados vetorial (especificamente, Zilliz Cloud). Em seguida, demonstramos como solicitar uma consulta ao usuário, vetorizar a consulta e consultar o banco de dados vetorial.
No entanto, descobrimos que, embora os resultados sejam semanticamente semelhantes, eles não são exatamente o que desejamos. Na próxima parte desta série de blogs, exploraremos o uso do LlamaIndex para rotear consultas e ver se conseguimos obter melhores resultados. Além das etapas discutidas aqui, você também pode experimentar o Zilliz Cloud substituindo o modelo, combinando textos ou usando um conjunto de dados diferente!
Para ler novamente toda a série Chat Towards Data Science, aqui estão os links:
Continue lendo

Why We Built Vector Lakebase: Rethinking Unstructured Data Architecture for AI
Vector Lakebase: a unified, lake-native data foundation for AI workloads — and an answer to what happens after vector databases succeed.

Zilliz Cloud Audit Logs Goes GA: Security, Compliance, and Transparency at Scale
Zilliz Cloud Audit Logs are now GA, giving enterprises real-time visibility, compliance-ready trails, and stronger security across AWS, GCP, and Azure.

The Real Bottlenecks in Autonomous Driving — And How AI Infrastructure Can Solve Them
Autonomous driving faces a data bottleneck. Learn how AI-native vector databases like Zilliz solve scale, cost, and insight challenges across AV pipelines.



