ICC222  ·  RECUPERAÇÃO DA INFORMAÇÃO  ·  PROF. EDLENO  ·  UFAM

Motor de Busca Musical

Pipeline lexical offline sobre metadados do Spotify

Disciplina de Recuperação da Informação · UFAM
Integrantes do grupo: Carsio Eddyo · Carlos Alexandre · Raquel de Sá · Lelson Nascimento
Python BM25 + TF-IDF Índice Invertido pytest · ty · ruff
Visão Geral

Pipeline de Ponta a Ponta

01–02
Dataset
Spotify
03
Loader
TrackDocument
04
Pré-
processamento
05–06
Índice
Invertido
07–08
Ranking
BM25 / TF-IDF
9
etapas entregues
2
modelos de ranking
3
suítes de testes
CI
gate automático
01
Etapa 1

Setup e Engenharia de Software

// pyproject.toml
[tool.ty.src]
include = ["src", "tests"]

[tool.ruff]
line-length = 100

[tool.pytest]
testpaths = ["tests"]
uv resolução de dependências
ruff lint e formatação
ty checagem de tipos
CI gate em push e pull_request
Etapa 2

Aquisição de Dados e EDA

// download_spotify_metadata.sh
# modo truncado (dev)
MODE=truncated ./download.sh

# modo full (produção)
MODE=full ./download.sh

# saída invariante
data/spotify-metadata/
spotify_clean_parquet/

Ambos os modos convergem para o mesmo layout em parquet, desacoplando o código do tamanho do corpus.

4
tabelas relacionais
1
notebook de EDA
PT/EN
idiomas no corpus
03
Etapa 3

Modelagem de Documentos

// datasets.py — TrackDocument
@dataclass
class TrackDocument:
  id: str
  title: str
  album: str
  artist: str

class SpotifyTracksLoader:
  def iter_docs() Iterator[TrackDocument]
  def count() int
// exemplo
TrackDocument(
  id="6rqhFgbbKwnb9MLmUQDhG6",
  title="As Canções de Amor",
  album="Emoções",
  artist="Roberto Carlos"
)

DuckDB faz os joins em runtime — sem carregar tudo em memória.

Etapa 4

Pré-processamento Textual

1
normalize()
lowercase · remove acentos (NFD) · limpa non-alphanum
2
tokenize()
segmentação com NLTK
3
remove_stopwords()
listas PT + EN
4
stem()
RSLP (PT) · Snowball (EN)
// transformação de exemplo
"As Canções de Amor!!!"
→ normalize  "as cancoes de amor"
→ tokenize  ["as","cancoes","de","amor"]
→ stopwords ["cancoes", "amor"]
→ stem     ["canco", "am"]

preprocess() é chamada igualmente em build e em query — garantia de consistência.

05
Etapa 5

Índice Invertido Multi-campo

// estrutura — postings[field][term] = [(doc_id, tf), ...]
campo: title
am
(0, 1)
(1, 1)
(3, 2)
...
canco
(0, 1)
(4, 3)
...
rock
(2, 1)
(5, 2)
(7, 1)
...

Também armazena doc_lengths[field][doc_id] — essencial para normalização do BM25.

Etapa 6

Construção e Persistência do Índice

// build_index.py
loader = SpotifyTracksLoader(path)
builder = IndexBuilder()

for doc in loader.iter_docs():
  builder.add(doc)

index = builder.build()
InvertedIndex.save(index, path)
dev (rápido)
--limit 10000
produção acadêmica
corpus completo

Persistência via pickle — artefato reutilizável sem re-indexar.

07
Etapa 7

Ranking com BM25

Fórmula BM25 por campo
score(q, d) = Σ IDF(t) ·
tf · (k1 + 1)
─────────────────────
tf + k1 · (1 − b + b · |d|/avgdl)
Saturação de TF
Repetir "amor" 20× não vale 20× mais — retorno decrescente.
Candidatos via posting lists
Rank não varre todos os documentos — só os que contêm termos da query.
Hiperparâmetros: k1 (saturação) e b (normalização de comprimento)
Etapa 8

Ranking com TF-IDF e Cosseno

Peso por termo
w(t, d) = tf_weight(t, d) · idf(t)

Variantes de TF
raw · log · augmented

Similaridade de cosseno
sim(q, d) =
q · d
──────────
q‖ · ‖d
IDF discriminante
Termos ubíquos como "de" têm IDF ≈ 0 — não discriminam.
Normas memoizadas
cached_property — normas de documentos pré-calculadas, evita recompute por query.
Query idêntica ao documento → cosseno ≈ 1
Etapa 9

Recuperação Esparsa vs Densa

Esparso — BM25 / TF-IDF

Vetor é a contagem de termos.

Acerta nomes próprios, termos raros, match lexical.

Falha com sinônimo, paráfrase e descrição conceitual.

Denso — Embeddings

Vetor é uma representação semântica aprendida.

Acerta conceito, sinônimo, tradução, descrição.

Menos preciso em nomes próprios e termos OOV.

Query "música animada para treinar" → BM25 devolve zero (nenhum título tem esses termos). Denso traz house, EDM, trap de alta energia.
Etapa 10

Pipeline Vetorial — Embeddings + Milvus

1. Documento enriquecido
SpotifyTracksLoader.iter_rich_docs()
title + artistas + gêneros + álbum + label

2. Embedding de texto
nomic-embed-text (Ollama, 768d)
text-embedding-3-small (OpenAI, 1536d)

3. Índice no Milvus
IVF_FLAT · métrica COSINE
Extra opcional
uv sync --extra vector — só quem quiser rodar a busca vetorial instala pymilvus e openai.
Milvus Lite
Banco vetorial embutido — arquivo .db local, sem Docker, sem servidor.
Mesmo modelo na indexação e na busca — dimensões têm que bater.
Etapa 10

Busca Semântica — Exemplo

Query
"rock clássico dos anos 70 com guitarra"
$ python -m music_search.vector.search "rock clássico dos anos 70 com guitarra" --top 5
#01 score=0.8721 Stairway to Heaven — Led Zeppelin
#02 score=0.8634 Smoke on the Water — Deep Purple
#03 score=0.8502 Comfortably Numb — Pink Floyd
#04 score=0.8411 Hotel California — Eagles
#05 score=0.8287 Sweet Child O' Mine — Guns N' Roses
Nenhum termo da query aparece nos títulos. O match vem dos gêneros associados (classic rock, hard rock, progressive rock) e da semântica aprendida pelo modelo.
09
Etapa 9

Testes e Validação

PASS
test_preprocessing.py
normalização, tokenização, stopwords, stemming em PT e EN; entradas inválidas
PASS
test_indexer.py
isolamento por campo, ordenação de posting lists, round-trip de persistência
PASS
test_ranking.py
score BM25 vs. fórmula manual; cosseno = 1 para documento idêntico; ordenação estável

Os testes funcionam como especificação executável do motor — pequenos erros de fórmula geram resultados silenciosamente ruins.

Estado Atual e Próximos Passos

O que já temos · O que vem a seguir

// entregue
  • Setup e CI automático
  • Dataset + EDA
  • Loader e modelagem
  • Pré-processamento PT/EN
  • Índice invertido multi-campo
// entregue
  • Build + persistência do índice
  • Ranking BM25
  • Ranking TF-IDF + cosseno
  • Busca vetorial (Milvus + embeddings)
  • Suítes de teste

Tweaks