Redis & Go: como criar um filtro simples de palavrões

Eu serei honesto. Sou bastante novo em Go e Redis e, mesmo com meu conjunto limitado de habilidades com essas duas tecnologias, fui capaz de construir um filtro de palavrões bem fodão para minha empresa.

Aviso: esta postagem contém um pequeno conjunto de palavrões que você pode achar ofensivos. Se você fizer isso, simplesmente não é meu público-alvo. 😉

Neste estágio, eu consideraria esta ferramenta um protótipo que ainda está em desenvolvimento, mas vou compartilhar com vocês a receita do meu ferramental e algumas das escolhas que fiz. Também tenha em mente que este sistema é muito simples de construir e nada avançado, mas cara, ele é muito RÁPIDO e oferece um ótimo ponto de partida.

Vamos começar com a lista de ingredientes de que precisaremos:

Porque ir?

Bem, existem muitas opções excelentes por aí. Mas eu gosto de velocidade e simples. E também gosto do incrível suporte de simultaneidade do Go. Acontece que Go for me tem sido uma ótima ferramenta para fazer protótipos de maneira semelhante ao Python, mas você também obtém o benefício de uma velocidade de execução muito rápida logo de cara. Go é uma daquelas tecnologias que realmente se comporta como anunciado, usa pouca memória e é superestável para construir aplicativos baseados em servidor. Eu tive um servidor da web Go em execução na intranet da minha empresa que está em execução há cerca de 3 meses sem uma reinicialização e está muito feliz, utilizando 2,3 MB de RAM e quando uma solicitação de página da web é feita, também fica feliz em responder com um resposta sem hesitação. Eu gosto disso em um produto.

Por que Redis?

Eu precisava de um miniprojeto para me ajudar a aprender mais sobre o Redis. Eles dizem que é rápido. Eles dizem que é estável. Eles dizem que é simples modelar seus dados. Eles dizem que é persistente. Eles dizem que requer configuração zero. Eles dizem que é basicamente memcached e mais. Pronto para usar, você obtém o armazenamento básico de chave / valor como memcached, além de: hashes, conjuntos, conjuntos classificados, script lua, contadores atômicos e um carregamento de outras funções convenientes. Se você quer uma maneira super fácil de brincar com ele, vá para: http://try.redis.io, onde você pode inserir comandos para o conteúdo do seu coração.

Por que Redigo?

O Redis tem uma grande seleção de bibliotecas de cliente disponíveis, mas como estamos usando Go, obviamente precisamos usar uma biblioteca de cliente baseada em Go. Passei algum tempo pesquisando algumas das diferentes bibliotecas cliente do Redis para costuras Go e Redigo para oferecer o máximo de suporte para Redis com uma boa API Gótica. Além disso, o autor Gary Burd entende Go muito bem. Para aqueles que não têm ideia de WTF, eu acabei de dizer: Aparentemente, gótico é a maneira de Go de dizer código Go idiomático, assim como Python faz com Pythonic.

//To install Redigo
go
get github.com/garyburd/redigo/redis

Por que o banco de dados Infochimps de palavrões?

Bem, você não tem que usar este conjunto de dados. No entanto, eles forneceram várias combinações de palavrões que gostaríamos de filtrar. Como exemplo, obviamente precisamos filtrar palavras como: bunda, merda, foda, etc. Mas também estou interessado em filtrar combinações de grafias criativas como: @ss, sh! T, fcuk, etc. A realidade aqui é isso, nossa solução é tão boa quanto nosso conjunto de dados . Se nossa lista de filtros tiver apenas 400 palavrões, isso deixa muito espaço para as pessoas serem criativas e soletram palavras usando símbolos especiais, ou simplesmente lhes dá espaço para criar combinações de palavras como: fuckface.

O fato é que minha solução não é à prova de balas, mas é simples. Não estou fazendo uso de nenhum tipo de heurística ou aprendizado de máquina. Além disso, não estou fazendo nenhum tipo de processamento de linguagem natural ou raízes de palavras, radicais, você entendeu. Minha solução é mais uma natureza de força bruta de apenas alimentar os dados para redis e espero que você pegue cerca de 85% dos casos.

Sinta-se à vontade para usar seu próprio conjunto de dados ou criá-los você mesmo para testar o quão sujo você realmente é. 😉

Para a implementação técnica

É muito simples aqui está minha implementação incrível:

  • Usando um pipeline, faça um loop em todas as suas palavras, uma por uma, adicionando cada uma delas a um conjunto Redis. Um pipeline oferecerá um desempenho extremamente rápido porque não esperará que o Redis responda a cada palavra.
  • Se você tiver vários conjuntos de dados de palavras como o que o Infochimps fornece, com certeza terá duplicatas. Quem se importa? É por isso que estamos usando um conjunto Redis. Ele conterá apenas palavrões distintos.
  • Como uma otimização, certifique-se de adicionar cada palavrão somente em letras minúsculas. Isso eliminará a necessidade de ter um conjunto contendo todas as versões maiúsculas e todas as versões minúsculas, bem como todas as combinações de maiúsculas / minúsculas. Em outras palavras, não queremos isso em nosso conjunto: foda-se foda-se, foda-se, simplesmente queremos: foda-se f * ck fcuk, etc.
  • Depois que seu conjunto for preenchido, o Redis em sua configuração padrão fará backup desse conjunto de dados em disco em um arquivo de despejo do redis. Você pode então levar esse arquivo e copiá-lo onde precisar para mantê-lo seguro. O Redis na reinicialização carregará esse arquivo quando for encontrado no local correto.
  • Se você precisar adicionar ao seu banco de dados, faça com que o Redis acione mais arquivos de palavrões, adicionando cada palavra individualmente. Você pode expandir seu conjunto com o tempo à medida que obtém melhores dados de palavrões.
  • As etapas acima precisam ser executadas apenas uma vez ou conforme necessário, quando você descobrir novos conjuntos de dados de palavrões.
  • Agora que temos um conjunto de redis de centenas de palavras a centenas de milhares, o Redis está preenchido e pronto para fazer seu trabalho.
  • A boa notícia é que o Redis não deve ter problemas para armazenar centenas de milhares de palavrões na memória, então todas as pesquisas serão baseadas na RAM.
  • A outra boa notícia é que, como adicionamos todos os nossos palavrões a um conjunto Redis, o Redis será capaz de fazer correspondências internamente usando um algoritmo de hash super rápido que fará uma correspondência em tempo constante também conhecido como O (1). Portanto, se estivermos verificando uma única palavra para ver se ela existe em nosso conjunto de palavrões no Redis, podemos fazer algo como:
//This will run in constant-time or O(1) resulting in FAST matches
SISMEMBER bad
-words fuck
//Returns: 1 for true, 0 for false
  • Lembre-se de que o Redis não fará uma comparação de hash por palavra em seu conjunto de palavrões, isso seria ruim. Basicamente, isso significaria que o Redis está realmente fazendo uma pesquisa linear, também conhecida como O (n), para encontrar uma correspondência. Não, Redis, é inteligente o suficiente e só precisa fazer uma comparação por palavra que você precisa verificar.

Você ainda está comigo?

Se você ainda está comigo, parabéns, nossa primeira parte acabou. Temos um banco de dados Redis em funcionamento que simplesmente contém uma chave chamada: palavrões. Essa chave é o nome de um conjunto Redis na memória que contém nosso banco de dados de palavrões. Neste ponto, construiremos uma ferramenta baseada em Go que nos permitirá verificar algumas entradas de dados que armazenaremos no Redis como outro conjunto denominado: user-words para ver se algum palavrão está presente nele.

O aplicativo verificador de palavras

Tudo bem, vamos analisar o aplicativo abaixo que realmente pega alguns dados de entrada e verifica se um palavrão foi encontrado. Por favor, considere algumas coisas: Este aplicativo é um protótipo e é um aplicativo simples baseado em console que aceita algum texto do usuário e valida esse texto no Redis quando o usuário pressiona a tecla ENTER.

A estrutura básica do aplicativo é a seguinte:

  1. Defina nosso pacote principal
  2. Importe os módulos necessários incluindo o cliente Redigo
  3. Defina nosso endereço TCP e porta da instância do redis (neste caso, localhost)
  4. Crie uma única conexão redis
  5. Defina nossa função submitData
  6. Defina nossa função principal que inicia o processo

Agora, para os detalhes do que está acontecendo:

O aplicativo fará um loop de bloqueio para sempre usando a construção empty for no Go. O bloqueio é importante porque significa que vai esperar que o usuário insira uma linha de texto. Quando o usuário insere uma linha de texto, o aplicativo vai determinar se o usuário simplesmente inseriu a string “q”, então sairemos do loop for e encerraremos a sessão. Qualquer outra coisa e o aplicativo pegará a entrada do usuário e dividirá a linha em uma fatia de string. Então, basicamente você terá isto: “Algum texto com um palavrão” e será convertido em: [] string de {“Algum”, “texto”, “com”, “a”, “ruim”, ” porra “,” palavra “}.

Uma vez que a fatia da string foi criada, simplesmente chamamos a função submitData (input [] string) que irá validar se temos palavrões em nossa fatia da string com o Redis. Agora temos algumas opções aqui. Podemos fazer um loop individual em cada palavra e enviá-la ao Redis para ver se uma determinada palavra está no conjunto de palavrões ou podemos empacotar todas as palavras e fazer nosso teste em uma espécie de trabalho em lote. Portanto, neste caso, vamos adicionar as palavras enviadas pelo usuário em um conjunto de palavras do usuário no Redis e pegar a interseção de palavras do usuário e palavrões para nos dar um conjunto recém-retornado. Este conjunto recém-retornado terá todas as palavras encontradas em ambos os conjuntos. Se o conjunto devolvido voltar vazio, significa que o cheque saiu limpo. Caso contrário, incluirá o conjunto de palavrões encontrado.

Um diagrama de conjunto com palavras e palavrões do usuário fazendo uma interseção

Observação: no diagrama, o conjunto de palavrões mostra apenas um subconjunto de todos os palavrões, obviamente pode haver centenas de milhares de palavrões. O conjunto de palavras do usuário também armazena as palavras enviadas pelo usuário em uma ordem indefinida. Essa é a natureza dos conjuntos, sua ordem não pode ser confiável. E no meio, temos a interseção de ambos os conjuntos; em outras palavras, quaisquer palavras encontradas em ambos os conjuntos quando o comando SINTER foi executado.

Escolhi fazer isso como uma transação de lote múltiplo do Redis. Aqui está a mecânica central deste script e como funciona:


//1. delete any existing set known as "user-words"
c
.Send("DEL", "user-words")
//2. create and store our newly submitted set of words in "user-words"
c
.Send("SADD", redis.Args{}.Add("user-words").AddFlat(input)...)
//3. take intersection of both sets
c
.Send("SINTER", "user-words", "bad-words")
//4. check the MULTI-BULK response from the EXEC which will also contain any found values
//6. Done!

Aqui está o código completo:

//The code in its entirety
package main

import (
"bufio"
"fmt"
"github.com/garyburd/redigo/redis"
"log"
"os"
"strings"
"time"
)

const (
ADDRESS
= "127.0.0.1:6379"
)

var (
c
, err = redis.Dial("tcp", ADDRESS)
)

/*
Submits data to our redis instance

*/

func submitData
(input []string) {
if err != nil {
log
.Fatal(err)
}

c
.Send("MULTI")

//1. delete from temp set
c
.Send("DEL", "user-words")
//2. store in a temp set
c
.Send("SADD", redis.Args{}.Add("user-words").AddFlat(input)...)
//3. take intersection of both sets
c
.Send("SINTER", "user-words", "bad-words")

reply
, err := c.Do("EXEC")

if err != nil {
fmt
.Println(err)
}

values
, _ := redis.Values(reply, nil)

curse_words
, err := redis.Strings(values[2], nil)
if err != nil {
fmt
.Println(err)
}

if (len(curse_words)) > 0 {
for _, v := range curse_words {
fmt
.Println(">>Found: ", v)
}
} else {
fmt
.Println(">>Nothing found")
}
}
func main
() {
for {
fmt
.Println(">>Please enter some text with swear words than press Enter or \"q\"" to exit"")
bio
:= bufio.NewReader(os.Stdin)
line