Treinamento de modelos LTR no Elasticsearch com listas de julgamento baseadas em dados de comportamento do usuário.
Aprenda como usar dados de UBI (Urban Business Information - Informação Baseada no Usuário) para criar listas de julgamento e automatizar o treinamento de seus modelos de Learning to Rank (LTR) no Elasticsearch.
Um grande desafio ao usar modelos de aprendizado para classificação é criar uma lista de julgamentos de alta qualidade para treinar o modelo. Tradicionalmente, esse processo envolve uma avaliação manual da relevância do documento de consulta para atribuir uma nota a cada um. Este é um processo lento, que não é escalável e é difícil de manter (imagine ter que atualizar manualmente uma lista com centenas de entradas).
E se pudéssemos usar interações reais de usuários com nosso aplicativo de busca para criar esses dados de treinamento? Utilizar dados de Renda Básica Universal (RBU) nos permite fazer exatamente isso. Criar um sistema automático capaz de capturar e usar nossas buscas, cliques e outras interações para gerar uma lista de julgamentos. Esse processo pode ser dimensionado e repetido com muito mais facilidade do que uma interação manual e tende a produzir melhores resultados. Neste blog, exploraremos como podemos consultar dados de UBI armazenados no Elasticsearch para calcular sinais relevantes e gerar um conjunto de dados de treinamento para um modelo LTR .
Você pode encontrar o experimento completo aqui.
Por que os dados de UBI podem ser úteis para treinar seu modelo LTR?
Os dados UBI oferecem diversas vantagens em relação à anotação manual:
Volume: Dado que os dados da Renda Básica Universal (RBU) provêm de interações reais, podemos coletar muito mais dados do que poderíamos gerar manualmente. Isso pressupõe que tenhamos tráfego suficiente para gerar esses dados, é claro.
Intenção real do usuário: Tradicionalmente, uma lista de julgamento manual provém de uma avaliação especializada dos dados disponíveis. Por outro lado, os dados da UBI refletem o comportamento real do usuário. Isso significa que podemos gerar melhores dados de treinamento que aprimorarão a precisão do nosso sistema de busca, pois ele se baseia em como os usuários realmente interagem com o seu conteúdo e encontram valor nele, em vez de suposições teóricas sobre o que deveria ser relevante.
Atualizações contínuas: As listas de julgamentos precisam ser atualizadas periodicamente. Se criarmos essas listas a partir de dados da Renda Básica Universal (RBU), poderemos obter dados atuais que resultarão em listas de julgamento atualizadas.
Relação custo-benefício: Sem o trabalho extra de criar manualmente uma lista de julgamento, o processo pode ser repetido de forma eficiente quantas vezes forem necessárias.
Distribuição natural de consultas: os dados UBI representam consultas reais de usuários, o que pode gerar mudanças mais profundas. Por exemplo, nossos usuários utilizam linguagem natural para pesquisar em nosso sistema? Nesse caso, talvez devêssemos implementar uma abordagem de busca semântica ou de busca híbrida.
No entanto, isso vem com alguns avisos:
Amplificação de viés: conteúdo popular tem maior probabilidade de receber cliques, simplesmente por ter mais visibilidade. Isso pode acabar amplificando os itens populares e possivelmente ofuscando opções melhores.
Cobertura incompleta: O conteúdo novo não possui interações, portanto, pode ser difícil para ele aparecer em posições elevadas nos resultados. Consultas raras também podem não ter pontos de dados suficientes para criar dados de treinamento significativos.
Variações sazonais: Se você espera que o comportamento do usuário mude drasticamente ao longo do tempo, os dados históricos podem não lhe dizer muito sobre o que é um bom resultado.
Ambiguidade da tarefa: um clique nem sempre garante que o usuário encontrou o que procurava.
Cálculo das notas
Notas para treinamento LTR
Para treinar modelos LTR, precisamos fornecer alguma representação numérica da relevância de um documento para uma consulta. Em nossa implementação, esse número é uma pontuação contínua que varia de 0,0 a 5,0+, onde pontuações mais altas indicam maior relevância.
Para demonstrar como funciona esse sistema de avaliação, considere este exemplo criado manualmente:
Consulta | Conteúdo do documento | Nota | Explicação |
|---|---|---|---|
"Melhor receita de pizza" | "Receita autêntica de massa de pizza italiana com fotos passo a passo" | 4.0 | Altamente relevante, exatamente o que o usuário está procurando. |
"Melhor receita de pizza" | "História da Pizza na Itália" | 1.0 | Ainda que relacionado ao assunto, trata-se de pizza, mas não é uma receita. |
"Melhor receita de pizza" | "Receita rápida de pizza em 15 minutos para iniciantes" | 3.0 | Relevante, um bom resultado, mas talvez não chegue a ser a "melhor" receita. |
"Melhor receita de pizza" | "Guia de Manutenção Automotiva" | 0,0 | Completamente irrelevante, sem qualquer relação com a pergunta. |
Como podemos ver aqui, a nota é uma representação numérica da relevância de um documento para nossa consulta de exemplo: "melhor receita de pizza". Com essas pontuações, nosso modelo LTR pode aprender quais documentos devem ser apresentados em posições mais altas nos resultados.
A forma de calcular as notas é o ponto central do nosso conjunto de dados de treinamento. Existem várias abordagens para fazer isso, cada uma com seus pontos fortes e fracos. Por exemplo, poderíamos atribuir uma pontuação binária de 1 para relevante e 0 para irrelevante, ou poderíamos simplesmente contar o número de cliques em um documento resultante para cada consulta.
Neste post do blog, usaremos uma abordagem diferente, considerando o comportamento do usuário como entrada e calculando uma nota como saída. Também corrigiremos o viés que pode ocorrer devido ao fato de que resultados mais altos tendem a receber mais cliques, independentemente da relevância do documento.
Cálculo das notas - Algoritmo COEC
O algoritmo COEC (Clicks over Expected Clicks) é uma metodologia para calcular notas de avaliação a partir dos cliques do usuário. Como já mencionamos, os usuários tendem a clicar nos resultados posicionados mais acima, mesmo que o documento não seja o mais relevante para a consulta; isso é chamado de Viés de Posição. A ideia central por trás do uso do algoritmo COEC é que nem todos os cliques têm a mesma importância; um clique em um documento na posição 10 indica que o documento é muito mais relevante para a consulta do que um clique em um documento na posição 1. Citando o artigo de pesquisa sobre o algoritmo COEC (link acima):
“É sabido que a taxa de cliques (CTR) dos resultados de pesquisa ou anúncios diminui significativamente dependendo da posição dos resultados.”
Você pode ler mais sobre viés de posição aqui.
Para resolver isso com o algoritmo COEC, seguimos estes passos:
1. Estabelecer linhas de base de posicionamento: Calculamos a taxa de cliques (CTR) para cada posição de pesquisa de 1 a 10. Isso significa que determinamos qual a porcentagem de usuários que normalmente clicam na posição 1, na posição 2 e assim por diante. Esta etapa captura a tendência natural de posicionamento dos usuários. Calculamos a CTR usando:
Onde:
p = Posição. De 1 a 10 Cp = Total de cliques (em qualquer documento) na posição p em todas as consultas Ip = Impressões totais: Quantas vezes um documento apareceu na posição p em todas as consultas.
Aqui, esperamos que posições mais altas recebam mais cliques.
2. Calcular os cliques esperados (CE):
Essa métrica estabelece quantos cliques um documento "deveria" ter recebido com base nas posições em que apareceu e na taxa de cliques (CTR) dessas posições. Calculamos o EC usando:
Onde:
Qd = Todas as consultas em que o documento d apareceu pos(d,q) = Posição do documento d nos resultados da consulta q
3. Contagem de cliques reais: Contamos o total real de cliques que um documento recebeu em todas as consultas em que apareceu, daqui em diante denominado A(d).
4. Calcule a pontuação COEC: Esta é a razão entre os cliques reais (A(d)) e os cliques esperados (EC(d)):
Essa métrica normaliza o viés de posição da seguinte forma:
Uma pontuação de 1,0 significa que o documento teve o desempenho exatamente como esperado, considerando as posições em que foi apresentado.
Uma pontuação acima de 1,0 significa que o documento teve um desempenho melhor do que o esperado, considerando suas posições. Portanto, este documento é mais relevante para a consulta.
Uma pontuação inferior a 1,0 significa que o documento teve um desempenho pior do que o esperado, considerando suas posições. Portanto, este documento é menos relevante para a consulta.
O resultado final é uma nota que reflete o que os usuários procuram, levando em consideração as expectativas baseadas na posição, extraídas de interações reais com nosso sistema de busca.
Implementação técnica
Criaremos um script para gerar uma lista de julgamentos para treinar um modelo LTR.
A entrada para este script são os dados UBI indexados no Elastic (consultas e eventos).
O resultado é uma lista de julgamentos em um arquivo CSV gerado a partir desses documentos de Renda Básica Universal (RBU) usando o algoritmo COEC. Essa lista de julgamentos pode ser usada com o Eland para extrair características relevantes e treinar um modelo LTR.
Início rápido
Para gerar uma lista de julgamentos a partir dos dados de exemplo deste blog, você pode seguir estes passos:
1. Clone o repositório:
git clone https://github.com/Alex1795/elastic-ltr-judgement_list-blog.git
cd elastic-ltr-judgement_list-blog2. Instale as bibliotecas necessárias
Para este script, precisamos das seguintes bibliotecas:
pandas: para salvar a lista de julgamentos
elasticsearch: Para obter os dados UBI da nossa implementação do Elasticsearch.
Também precisamos do Python 3.11.
pip install -r requirements.txt3. Atualize as variáveis de ambiente para sua implantação do Elasticsearch em um arquivo .env.
ES_HOST
API_KEY
Para adicionar as variáveis de ambiente, utilize:
source .env4. Crie os índices ubi_queries e ubi_events e carregue os dados de exemplo. Execute o arquivo setup.py:
python setup.py5. Execute o script Python:
python judgement_list-generator.pySeguindo esses passos, você deverá ver um novo arquivo chamado judgment_list.csv com a seguinte aparência:

Este script calcula as notas aplicando o algoritmo COEC discutido anteriormente, utilizando a função calculate_relevance_grade() mostrada abaixo.
Arquitetura de dados
Consultas Ubi
Nosso índice de consultas UBI contém informações sobre as consultas executadas em nosso sistema de busca. Este é um documento de exemplo:
{
"client_id": "client_002",
"query": "italian pasta recipes",
"query_attributes": {
"search_type": "recipe",
"category": "food",
"cuisine": "italian"
},
"query_id": "q002",
"query_response_id": "qr002",
"query_response_object_ids": [
"doc_011",
"doc_012",
"doc_013",
"doc_014",
"doc_015",
"doc_016",
"doc_017",
"doc_018",
"doc_019",
"doc_020"
],
"timestamp": "2024-08-14T11:15:00Z",
"user_query": "italian pasta recipes"
}Aqui podemos ver dados do usuário (client_id), dos resultados da consulta (query_response_object_ids) e da própria consulta (timestamp, user_query).
Eventos de clique Ubi
Nosso índice ubi_events contém dados de cada vez que um usuário clicou em um documento nos resultados. Este é um documento de exemplo:
{
"action_name": "click",
"application": "recipe_search",
"client_id": "client_001",
"event_attributes": {
"object": {
"description": "Authentic Italian Pizza Dough Recipe with Step-by-Step Photos",
"device": "desktop",
"object_id": "doc_001",
"position": {
"ordinal": 1,
"page_depth": 1
},
"user": {
"city": "New York",
"country": "USA",
"ip": "192.168.1.100",
"location": {
"lat": 40.7128,
"lon": -74.006
},
"region": "NY"
}
}
},
"message": "User clicked on document doc_001",
"message_type": "click",
"query_id": "q001",
"timestamp": "2024-08-14T10:31:00Z",
"user_query": "best pizza recipe"
}Script de geração de lista de julgamentos
Visão geral do roteiro
Este script automatiza a geração da lista de julgamento usando dados UBI de consultas e eventos de clique armazenados no Elasticsearch. Ele executa estas tarefas:
Busca e processa os dados UBI no Elasticsearch.
Correlaciona eventos de UBI com suas consultas.
Calcula o CTR para cada posição.
Calcula os cliques esperados (CE) para cada documento.
Contabiliza os cliques reais em cada documento.
Calcula a pontuação COEC para cada par consulta-documento.
Gera uma lista de julgamentos e a grava em um arquivo CSV.
Vamos analisar cada função:
conectar_ao_elasticsearch()
def connect_to_elasticsearch(host, api_key):
"""Create and return Elasticsearch client"""
try:
es = Elasticsearch(
hosts=[host],
api_key=api_key,
request_timeout=60
)
# Test the connection
if es.ping():
print(f"✓ Successfully connected to Elasticsearch at {host}")
return es
else:
print("✗ Failed to connect to Elasticsearch")
return None
except Exception as e:
print(f"✗ Error connecting to Elasticsearch: {e}")
return NoneEssa função retorna um objeto cliente Elasticsearch usando o host e a chave da API.
buscar_dados_ubi()
def fetch_ubi_data(es_client: Elasticsearch, queries_index: str, events_index: str,
size: int = 10000) -> Tuple[List[Dict], List[Dict]]:
"""
Fetch UBI queries and events data from Elasticsearch indices.
Args:
es_client: Elasticsearch client
queries_index: Name of the UBI queries index
events_index: Name of the UBI events index
size: Maximum number of documents to fetch
Returns:
Tuple of (queries_data, events_data)
"""
logger.info(f"Fetching data from {queries_index} and {events_index}")
# Fetch queries with error handling
try:
queries_response = es_client.search(
index=queries_index,
body={
"query": {"match_all": {}},
"size": size
}
)
queries_data = [hit['_source'] for hit in queries_response['hits']['hits']]
logger.info(f"Fetched {len(queries_data)} queries")
except Exception as e:
logger.error(f"Error fetching queries from {queries_index}: {e}")
raise
# Fetch events (only click events for now) with error handling
try:
events_response = es_client.search(
index=events_index,
body={
"query": {
"term": {"message_type.keyword": "CLICK_THROUGH"}
},
"size": size
}
)
events_data = [hit['_source'] for hit in events_response['hits']['hits']]
logger.info(f"Fetched {len(events_data)} click events")
except Exception as e:
logger.error(f"Error fetching events from {events_index}: {e}")
raise
logger.info(f"Data fetch completed successfully - Queries: {len(queries_data)}, Events: {len(events_data)}")
return queries_data, events_dataEsta função é a camada de extração de dados; ela se conecta ao Elasticsearch para buscar consultas UBI usando uma consulta match_all e filtra os eventos UBI para obter apenas os eventos 'CLICK_THROUGH'.
processar_dados_ubi()
def process_ubi_data(queries_data: List[Dict], events_data: List[Dict]) -> pd.DataFrame:
"""
Process UBI data and generate judgment list.
Args:
queries_data: List of query documents from UBI queries index
events_data: List of event documents from UBI events index
Returns:
DataFrame with judgment list (qid, docid, grade, keywords)
"""
logger.info("Processing UBI data to generate judgment list")
# Group events by query_id
clicks_by_query = {}
for event in events_data:
query_id = event['query_id']
if query_id not in clicks_by_query:
clicks_by_query[query_id] = {}
# Extract clicked document info
object_id = event['event_attributes']['object']['object_id']
position = event['event_attributes']['object']['position']['ordinal']
clicks_by_query[query_id][object_id] = {
'position': position,
'timestamp': event['timestamp']
}
judgment_list = []
# Process each query
for query in queries_data:
query_id = query['query_id']
user_query = query['user_query']
document_ids = query['query_response_object_ids']
# Get clicks for this query
query_clicks = clicks_by_query.get(query_id, {})
# Generate judgment for each document shown
for doc_id in document_ids:
grade = calculate_relevance_grade(doc_id, query_clicks, document_ids, queries_data, events_data)
judgment_list.append({
'qid': query_id,
'docid': doc_id,
'grade': grade,
'query': user_query
})
df = pd.DataFrame(judgment_list)
logger.info(f"Generated {len(df)} judgment entries for {df['qid'].nunique()} unique queries")
return dfEsta função é responsável pela geração da lista de julgamentos. O processamento dos dados UBI começa por associar eventos e consultas UBI. Em seguida, chama a função calculate_relevance_grade() para cada par documento-consulta, a fim de obter as entradas para a lista de julgamento. Por fim, retorna a lista resultante como um dataframe do pandas.
calcular_nota_de_relevância()
def calculate_relevance_grade(document_id: str, clicks_data: Dict,
query_response_ids: List[str], all_queries_data: List[Dict] = None,
all_events_data: List[Dict] = None) -> float:
"""
Calculate COEC (Click Over Expected Clicks) relevance score for a document.
Args:
document_id: ID of the document
clicks_data: Dictionary of clicked documents with their positions for current query
query_response_ids: List of document IDs shown in search results (ordered by position)
all_queries_data: All queries data for calculating position CTR averages
all_events_data: All events data for calculating position CTR averages
Returns:
COEC relevance score (continuous value, typically 0.0 to 5.0+)
"""
# If no global data provided, fall back to simple position-based grading
if all_queries_data is None or all_events_data is None:
logger.warning("No global data provided, falling back to position-based grading")
# Simple fallback logic
if document_id in clicks_data:
position = clicks_data[document_id]['position']
if position > 3:
return 4.0
elif position >= 1 and position <= 3:
return 3.0
if document_id in query_response_ids:
position = query_response_ids.index(document_id) + 1
if position <= 5:
return 2.0
elif position >= 6 and position <= 10:
return 1.0
return 0.0
# Calculate rank-aggregated click-through rates
position_ctr_averages = {}
position_impression_counts = {}
position_click_counts = {}
# Initialize counters
for pos in range(1, 11): # Positions 1-10
position_impression_counts[pos] = 0
position_click_counts[pos] = 0
# Count impressions (every document shown contributes)
for query in all_queries_data:
for i, doc_id in enumerate(query['query_response_object_ids'][:10]): # Top 10 positions
position = i + 1
position_impression_counts[position] += 1
# Count clicks by position
for event in all_events_data:
if event.get('action_name') == 'click':
position = event['event_attributes']['object']['position']['ordinal']
if position <= 10:
position_click_counts[position] += 1
# Calculate average CTR per position
for pos in range(1, 11):
if position_impression_counts[pos] > 0:
position_ctr_averages[pos] = position_click_counts[pos] / position_impression_counts[pos]
else:
position_ctr_averages[pos] = 0.0
# Calculate expected clicks for this specific document
expected_clicks = 0.0
# Count how many times this document appeared at each position for any query
for query in all_queries_data:
if document_id in query['query_response_object_ids']:
position = query['query_response_object_ids'].index(document_id) + 1
if position <= 10:
expected_clicks += position_ctr_averages[position]
# Count total actual clicks for this document across all queries
actual_clicks = 0
for event in all_events_data:
if (event.get('action_name') == 'click' and
event['event_attributes']['object']['object_id'] == document_id):
actual_clicks += 1
# Calculate COEC score
if expected_clicks > 0:
coec_score = actual_clicks / expected_clicks
else:
coec_score = 0.0
logger.debug(
f"Document {document_id}: {actual_clicks} clicks / {expected_clicks:.3f} expected = {coec_score:.3f} COEC")
return coec_scoreEsta é a função que implementa o algoritmo COEC. Ele calcula a CTR para cada posição, depois compara os cliques reais para um par documento-consulta e, finalmente, calcula a pontuação COEC real para cada um.
gerar_estatísticas_de_julgamento()
def generate_judgment_statistics(df: pd.DataFrame) -> Dict:
"""Generate statistics about the judgment list."""
stats = {
'total_judgments': len(df),
'unique_queries': df['qid'].nunique(),
'unique_documents': df['docid'].nunique(),
'grade_distribution': df['grade'].value_counts().to_dict(),
'avg_judgments_per_query': len(df) / df['qid'].nunique() if df['qid'].nunique() > 0 else 0,
'queries_with_clicks': len(df[df['grade'] > 1]['qid'].unique()),
'click_through_rate': len(df[df['grade'] > 1]) / len(df) if len(df) > 0 else 0
}
return statsEle gera estatísticas úteis a partir da lista de julgamentos, como o total de consultas, o total de documentos únicos ou a distribuição de notas. Esta informação é meramente informativa e não altera a lista de julgamentos resultante.
Resultados e impacto
Seguindo as instruções da seção Início rápido, você deverá obter um arquivo CSV contendo uma lista de julgamentos com 320 entradas (você pode ver um exemplo de saída no repositório). Com estes campos:
qid: ID único da consulta
docid: identificador único para um documento resultante
nota: a nota calculada para o par consulta-documento.
consulta: A consulta do usuário
Vejamos os resultados da pesquisa “receitas italianas”:
qid | docid | nota | Consulta |
|---|---|---|---|
q1-receitas-italianas | receita_básica_de_massa | 0,0 | receitas italianas |
q1-receitas-italianas | receita_pizza_margherita | 3,333333 | receitas italianas |
q1-receitas-italianas | guia_de_receitas_de_risoto | 10.0 | receitas italianas |
q1-receitas-italianas | receita_croissant_francês | 0,0 | receitas italianas |
q1-receitas-italianas | receita_paella_espanhola | 0,0 | receitas italianas |
q1-receitas-italianas | receita_moussaka_grega | 1,875 | receitas italianas |
Podemos ver pelos resultados que para a consulta “receitas italianas”:
A receita de risoto é definitivamente o melhor resultado para a pesquisa, recebendo 10 vezes mais cliques do que o esperado.
A pizza Margherita também é um ótimo resultado.
A mousaka grega (surpreendentemente) também obteve um bom resultado e teve um desempenho melhor do que sua posição nos resultados sugeriria. Isso significa que alguns usuários que procuravam receitas italianas se interessaram por esta receita em vez desta. Talvez esses usuários estejam interessados em pratos mediterrâneos em geral. Em suma, isso nos indica que esse poderia ser um bom resultado para ser apresentado entre as outras duas partidas "melhores" que discutimos anteriormente.
Conclusão
Utilizar dados UBI nos permite automatizar o treinamento de modelos LTR, criando listas de julgamento de alta qualidade a partir de nossos próprios usuários. Os dados do UBI fornecem um grande conjunto de dados que reflete como nosso sistema de busca está sendo usado. Ao usar o algoritmo COEC para gerar as notas, levamos em consideração o viés inerente e, ao mesmo tempo, refletimos o que um usuário considera um resultado melhor. O método descrito aqui pode ser aplicado a casos de uso reais para proporcionar uma melhor experiência de busca que evolua com as tendências reais de uso.




