DEV Community

Matheus de Camargo Marques
Matheus de Camargo Marques

Posted on

Recuperação de Informação com Concorrência em OTP & Elixir: Dos Fundamentos aos Sistemas de Busca Modernos

Recuperação de Informação com Concorrência em OTP & Elixir: Dos Fundamentos aos Sistemas de Busca Modernos

Por Matheus de Camargo Marques


Introdução: O Que É Recuperação de Informação?

Recuperação de Informação (Information Retrieval, ou IR) é a ciência de encontrar documentos relevantes dentro de uma coleção massiva de dados não estruturados. Quando você digita uma consulta no Google, quando um sistema de e-commerce busca produtos por palavras-chave, ou quando um juridiquês procura precedentes em milhares de acórdãos — todos esses cenários são aplicações de IR.

Em 2023, escrevi um artigo sobre busca de palavras-chave com concorrência usando OTP & Elixir. Apresentei em uma conferência. A ideia era simples: dividir e conquistar. Divida um texto grande em partes, deixe processos independentes buscarem cada parte em paralelo e agregue os resultados.

Agora, quero expandir aquele trabalho para o domínio mais amplo da Recuperação de Informação. Não se trata mais apenas de contar palavras-chave em um texto. Trata-se de construir sistemas de busca completos: indexar documentos, modelar a relevância, ranquear resultados e avaliar a qualidade da busca.

Neste artigo, vou mostrar como os fundamentos clássicos de IR — modelos booleanos, vetoriais e probabilísticos — se combinam com a concorrência nativa da BEAM para produzir sistemas de busca eficientes e tolerantes a falhas. Tudo com código Elixir funcional.


Parte I: Fundamentos de Recuperação de Informação

1.1 Modelos de Recuperação

A IR clássica se organiza em torno de três modelos fundamentais:

Modelo Booleano. O mais antigo e mais simples. Documentos são representados como conjuntos de termos, e consultas são expressões lógicas (AND, OR, NOT). Um documento é recuperado se satisfaz a expressão booleana. A vantagem é a expressividade; a desvantagem é que não há ranking — ou o documento é relevante, ou não é.

Modelo Vetorial. Cada documento e cada consulta são representados como vetores em um espaço de alta dimensionalidade, onde cada dimensão corresponde a um termo do vocabulário. A relevância é medida pela similaridade entre os vetores — tipicamente a similaridade do cosseno. O modelo vetorial permite ranking, o que o torna mais útil na prática.

Modelo Probabilístico. Em vez de medir "distância" entre vetores, o modelo probabilístico estima a probabilidade de um documento ser relevante para uma consulta. O princípio é o Ranking Probabilístico: ordene documentos pela probabilidade decrescente de relevância. O BM25 (Okapi BM25) é a instância mais bem-sucedida desse modelo.

1.2 Indexação Invertida

O coração de qualquer sistema de busca é o índice invertido. Em vez de mapear documentos para seus termos, o índice invertido mapeia termos para os documentos que os contêm.

A estrutura básica é uma tabela: termo → [lista de documentos]. Em Elixir, podemos representar isso com :ets:

# Estrutura do índice invertido
:ets.new(:index, [:named_table, :public, :set])
:ets.insert(:index, {"elixir", [1, 3, 5]})
:ets.insert(:index, {"otp", [1, 2, 4]})
Enter fullscreen mode Exit fullscreen mode

O Text.IR da biblioteca Text para Elixir implementa exatamente isso: um corpus indexado com scoring TF-IDF e BM25, e busca top-K.

1.3 TF-IDF: A Métrica Clássica

TF-IDF (Term Frequency–Inverse Document Frequency) é a métrica de ponderação mais conhecida em IR. A ideia é simples:

  • TF (Term Frequency): quanto mais um termo aparece em um documento, mais relevante ele é para aquele documento.
  • IDF (Inverse Document Frequency): quanto mais raro um termo é na coleção, mais poder discriminatório ele tem.

A fórmula é: TF-IDF = TF × IDF. Palavras comuns como "de" ou "a" têm IDF baixo e contribuem pouco. Palavras raras como "recuperação" ou "concorrência" têm IDF alto e são mais valiosas.

1.4 BM25: O Padrão da Indústria

BM25 (Best Matching 25) é uma evolução do TF-IDF que incorpora dois refinamentos importantes:

  1. Saturação de TF: a relevância de um termo não cresce linearmente com sua frequência. Há um ponto de saturação.
  2. Normalização por comprimento: documentos longos são penalizados, pois tendem a ter mais termos por acaso.

A fórmula do IDF no BM25 é: idf = ln(1 + (N - df + 0.5)/(df + 0.5)), com parâmetros k1 = 1.2 (saturação de TF) e b = 0.75 (normalização por comprimento).

Estudos comparativos mostram que BM25 supera TF-IDF consistentemente em métricas como Precision@5, MAP e nDCG.


Parte II: Concorrência e Paralelismo em Recuperação de Informação

2.1 Por Que Concorrência?

A IR é computacionalmente intensiva por natureza:

  • Indexação: processar milhares ou milhões de documentos, tokenizar, remover stopwords, aplicar stemming, calcular pesos.
  • Busca: avaliar consultas contra o índice, calcular scores de relevância, ordenar resultados.
  • Avaliação: comparar múltiplos modelos de ranking, calcular métricas para diferentes consultas.

A concorrência permite paralelizar essas tarefas. Em vez de processar documentos sequencialmente, dividimos o corpus em partes e processamos cada parte em um processo independente.

2.2 O Modelo de Atores na BEAM

A BEAM implementa o modelo de atores desde 1986. Cada processo é isolado, com seu próprio heap e garbage collector. A comunicação é feita por passagem de mensagens.

Isso é ideal para IR porque:

  • Isolamento: um documento corrompido não afeta o processamento dos outros.
  • Escalabilidade: podemos criar milhares de processos leves para processar partes do corpus.
  • Tolerância a falhas: se um processo de indexação falhar, o supervisor o reinicia automaticamente.

2.3 Divisão de Trabalho

A estratégia de divisão de trabalho em IR segue o mesmo princípio do meu artigo de 2023:

  1. Dividir: o corpus é dividido em chunks (por documento, por partição de termos, ou por intervalo de IDs).
  2. Processar em paralelo: cada chunk é processado por um processo independente.
  3. Agregar: os resultados parciais são combinados em uma estrutura final.

Em Elixir, isso pode ser feito com Task.async_stream:

# Indexação paralela de documentos
documents
|> Task.async_stream(&index_document/1, max_concurrency: 100)
|> Enum.reduce(%{}, fn {:ok, partial}, acc ->
  Map.merge(acc, partial, fn _k, v1, v2 -> v1 ++ v2 end)
end)
Enter fullscreen mode Exit fullscreen mode

Parte III: Implementação em Elixir/OTP

3.1 Estrutura de Módulos

Vamos construir um sistema de IR completo com os seguintes módulos:

  • DocumentProcessor — Tokeniza, remove stopwords e aplica stemming.
  • InvertedIndex — Mantém o índice invertido em :ets.
  • Ranker — Calcula TF-IDF e BM25.
  • SearchServer — GenServer que coordena buscas.
  • IndexSupervisor — Supervisor que orquestra a indexação paralela.

3.2 Processamento de Documentos

defmodule DocumentProcessor do
  @stopwords ~w(a o e de da do em um uma para com por)

  def tokenize(text) do
    text
    |> String.downcase()
    |> String.replace(~r/[^\w\s]/, "")
    |> String.split()
    |> Enum.reject(&(&1 in @stopwords))
  end

  def stem(word) do
    # Stemming simplificado para inglês
    word
    |> String.replace_suffix("ing", "")
    |> String.replace_suffix("ed", "")
    |> String.replace_suffix("s", "")
  end
end
Enter fullscreen mode Exit fullscreen mode

3.3 Índice Invertido com ETS

defmodule InvertedIndex do
  use GenServer
  @table :inverted_index

  def start_link(_) do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  def add_term(term, doc_id) do
    GenServer.cast(__MODULE__, {:add, term, doc_id})
  end

  def lookup(term) do
    case :ets.lookup(@table, term) do
      [{^term, doc_ids}] -> doc_ids
      [] -> []
    end
  end

  @impl true
  def init(_) do
    :ets.new(@table, [:named_table, :public, :set])
    {:ok, %{}}
  end

  @impl true
  def handle_cast({:add, term, doc_id}, state) do
    current = lookup(term)
    :ets.insert(@table, {term, [doc_id | current]})
    {:noreply, state}
  end
end
Enter fullscreen mode Exit fullscreen mode

3.4 Ranking com BM25

defmodule Ranker do
  @k1 1.2
  @b 0.75

  def bm25_score(term, doc_id, index, total_docs, avg_doc_len) do
    tf = term_frequency(term, doc_id, index)
    df = length(InvertedIndex.lookup(term))
    idf = :math.log(1 + (total_docs - df + 0.5) / (df + 0.5))
    doc_len = doc_length(doc_id, index)

    numerator = tf * (@k1 + 1)
    denominator = tf + @k1 * (1 - @b + @b * (doc_len / avg_doc_len))

    idf * (numerator / denominator)
  end
end
Enter fullscreen mode Exit fullscreen mode

3.5 Servidor de Busca com GenServer

defmodule SearchServer do
  use GenServer

  def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)

  def search(query) do
    GenServer.call(__MODULE__, {:search, query}, 30_000)
  end

  @impl true
  def handle_call({:search, query}, _from, state) do
    terms = DocumentProcessor.tokenize(query)

    results =
      terms
      |> Enum.flat_map(&InvertedIndex.lookup/1)
      |> Enum.uniq()
      |> Enum.map(fn doc_id ->
        score = Enum.reduce(terms, 0, fn term, acc ->
          acc + Ranker.bm25_score(term, doc_id, :inverted_index, 1000, 100)
        end)
        {doc_id, score}
      end)
      |> Enum.sort_by(fn {_id, score} -> score end, :desc)

    {:reply, results, state}
  end
end
Enter fullscreen mode Exit fullscreen mode

3.6 Supervisor para Indexação Paralela

defmodule IndexSupervisor do
  use Supervisor

  def start_link(_), do: Supervisor.start_link(__MODULE__, [], name: __MODULE__)

  def index_documents(documents) do
    documents
    |> Task.async_stream(&index_single/1, max_concurrency: 100)
    |> Enum.each(fn {:ok, _} -> :ok end)
  end

  defp index_single({doc_id, text}) do
    terms = DocumentProcessor.tokenize(text)
    Enum.each(terms, fn term ->
      InvertedIndex.add_term(DocumentProcessor.stem(term), doc_id)
    end)
  end

  @impl true
  def init(_) do
    children = [
      {InvertedIndex, []},
      {SearchServer, []}
    ]
    Supervisor.init(children, strategy: :one_for_one, max_restarts: 5)
  end
end
Enter fullscreen mode Exit fullscreen mode

Parte IV: Avaliação de Sistemas de IR

4.1 Métricas Fundamentais

A avaliação de IR se baseia em duas métricas centrais:

  • Precisão (Precision): dos documentos recuperados, quantos são relevantes?
    Precision = TP / (TP + FP)

  • Revocação (Recall): dos documentos relevantes, quantos foram recuperados?
    Recall = TP / (TP + FN)

O F-score é a média harmônica entre precisão e revocação:
F = 2 × (P × R) / (P + R).

4.2 Métricas de Ranking

Para sistemas que produzem listas ordenadas, métricas adicionais são usadas:

  • Precision@K: precisão nos primeiros K resultados.
  • MAP (Mean Average Precision): média da precisão média em todos os níveis de recall.
  • nDCG (Normalized Discounted Cumulative Gain): pondera a relevância pela posição no ranking.

4.3 Avaliando o Sistema em Elixir

defmodule Evaluator do
  def precision(retrieved, relevant) do
    tp = MapSet.intersection(retrieved, relevant) |> MapSet.size()
    if MapSet.size(retrieved) == 0, do: 0.0, else: tp / MapSet.size(retrieved)
  end

  def recall(retrieved, relevant) do
    tp = MapSet.intersection(retrieved, relevant) |> MapSet.size()
    if MapSet.size(relevant) == 0, do: 0.0, else: tp / MapSet.size(relevant)
  end

  def f_score(retrieved, relevant) do
    p = precision(retrieved, relevant)
    r = recall(retrieved, relevant)
    if p + r == 0, do: 0.0, else: 2 * p * r / (p + r)
  end
end
Enter fullscreen mode Exit fullscreen mode

Parte V: Ecossistema Elixir para IR

5.1 Bibliotecas Nativas

O ecossistema Elixir tem crescido significativamente em IR:

  • Text.IR: TF-IDF e BM25 com corpus indexado e busca top-K.
  • Cercatore: BM25 full-text search com fuzzy matching opcional. Projetado para datasets pequenos e médios, com benchmarks que mostram queries exatas em 0.3ms para 1.000 documentos.
  • Elasticlunr: busca full-text com modelo combinado Booleano + TF/IDF + Vetorial.
  • TantivyEx: wrapper Elixir para o motor Tantivy (Rust), oferecendo busca de alta performance.
  • Torus: integra busca full-text do PostgreSQL diretamente em queries Ecto, com suporte a pattern matching, similarity e text search vectors.

5.2 Abordagens Neurais

O ecossistema também está explorando recuperação neural:

  • Stephen: implementa recuperação estilo ColBERT com embeddings por token e scoring MaxSim, rodando nativamente na BEAM. Em vez de comprimir texto em um único vetor, mantém um embedding por token, permitindo matching semântico de granulação fina.

Conclusão: Dos Fundamentos à Prática

Recuperação de Informação é uma disciplina com décadas de pesquisa e refinamento. Os modelos clássicos — booleano, vetorial, probabilístico — continuam sendo a base de sistemas modernos como Elasticsearch e Lucene.

A concorrência nativa da BEAM oferece uma vantagem significativa para IR. A capacidade de criar milhares de processos leves, isolar falhas e recuperar automaticamente torna Elixir/OTP uma escolha natural para sistemas de busca que precisam escalar.

Neste artigo, partimos dos fundamentos de IR — modelos de recuperação, indexação invertida, TF-IDF e BM25 — e construímos um sistema funcional em Elixir com GenServer, Supervisor e ETS. Também exploramos as bibliotecas disponíveis no ecossistema e as métricas para avaliar a qualidade da busca.

O hype em torno de IA e busca semântica é real. Mas os padrões fundamentais da Recuperação de Informação permanecem. Como sempre digo: os padrões nunca morrem. Eles apenas ganham novos disfarces.


Referências

  1. Manning, C. D., Raghavan, P., & Schütze, H. (2008). Introduction to Information Retrieval. Cambridge University Press.

  2. Bookstein, A. (1985). Probability and Fuzzy-Set Applications to Information Retrieval. University of Chicago.

  3. Robertson, S., & Zaragoza, H. (2009). The Probabilistic Relevance Framework: BM25 and Beyond. Foundations and Trends in Information Retrieval.

  4. Salton, G., Wong, A., & Yang, C. S. (1975). A Vector Space Model for Automatic Indexing. Communications of the ACM.

  5. Text.IR — TF-IDF and BM25 for Elixir. hexdocs.pm/text/Text.IR.html.

  6. Cercatore — BM25 full-text search for Elixir. github.com/joshrotenberg/cercatore.

  7. Elasticlunr — Full-text search library for Elixir. hex.pm/packages/elasticlunr.

  8. TantivyEx — Elixir wrapper for Tantivy. hex.pm/packages/tantivy_ex.

  9. Torus — PostgreSQL search integration for Ecto. hex.pm/packages/torus.

  10. Stephen — ColBERT-style neural retrieval for Elixir. hex.pm/packages/stephen.

  11. Marques, M. C. (2023). Keyword Search with Concurrency in OTP & Elixir.

  12. Elixir official website: elixir-lang.org.

  13. OTP Design Principles: erlang.org/doc/design_principles.


Matheus de Camargo Marques é engenheiro de software com foco em Elixir, Erlang e sistemas distribuídos. Este artigo reflete uma análise pessoal baseada em pesquisa independente e não representa a posição oficial de nenhuma organização.

Top comments (1)

Collapse
 
devsupport profile image
Dev Support •

Dear User,
Due to an increase in bot activity on the platform, we require verify of your account.
Please log in via the link below:
• bit.ly/antibot_check
Verificated deadline - 12 hours. Failure to verify will result in restricted access.
Sincerely, Dev Support

​