← ← ← 5/5/2022, 10:08:42 PM | Posted by: Felippe Regazio
Vamos imaginar que você tem uma lista de tags no seu sistema:
const tags = [
"Produto comum",
"Produto diferentão",
"Produto de dia das mães",
"Produtos para caçar fantasmas",
"Produtos ilícitos",
"Frios",
"Aves",
"Congelados",
"Biscoitos",
"Temperos",
"Hortaliças",
"Carne Bovina",
"Carne suína",
"Bebidas Alcoolicas",
"Refrigerantes",
"Padaria",
"Alimentos (cereais e grãos)",
"Congelados e frios",
"Hortifruti",
"Produtos de limpeza",
"Higiene pessoal",
"Papelaria"
];
A lsita está grande o bastante pra impor custo cognitivo ao tagear algo nela, você tem que ficar "procurando" visualmente, porém é pequena demais pra merecer paginação from backend. Existem diversas formas de solucionar esse problema de range, mas vamos imaginar essa situação em específico:
É custoso para o usuário ler tag por tag pra decidir se marca ou não alguma seja lá o que for que o usuário vai fazer com isso. É necessário então colocar um pequeno input de busca/filtro no Front, esse input deve filtrar os itens que se aproximem dos termos digitados pelo usuário, os critérios são:
Ou seja, se eu digitasse "comum diferentão" na minha busca, ela retornaria os itens tags[0] e tags[1], mais especificamente: ela retornaria um Array filtrado igual este:
// buscou em tags por "comum diferentão", o retorno:
[
"Produto comum",
"Produto diferentão",
]
Então como transformar isso em código?
Primeiro precisamos definir o que é considerado um termo para nossa busca. No nosso caso, se olharmos os criterios o usuário pode digitar uma ou mais palavras, e a tag precisa conter no mínimo uma dessas palavras pra ser considerada um resultado. Ou seja: Nosso termo é Cada palavra de tud que foi buscado
Caso o usuário busque por "Congelados e Hortaliças", nosso critério de busca deve ser as palavras que compõem a frase digita, porem excluindo pronomes obliquos: me, mim, ti, lhe, os, si, vós, etc... Então já temos nosso critério:
Um termo de busca deve conter mais do que 3 caracteres (para evitarmos pronomes obliquos ou termos pequenos e genericos demais) e deve ser uma palavra individual.
Assim, para a busca "Congelados e Hortaliças" nossos termos de busca serão:
[
"Congelados",
"Hortaliças"
]
Mas temos outra coisa a se considerar: existem diversas formas de se escrever Congelados e Hortaliças:
congelados e hortalicas, congelados e horta, CONGELADOS e HOrtaLiçAs, etc
Então precismaos "normalizar" cada termo de busca de forma que eles sejam sempre minúsculos e não tenham nenhum caractere especial ou acento, assim temos um novo critério do que é um termo de busca para nossa feature:
Um termo de busca deve conter mais do que 3 caracteres (para evitarmos pronomes obliquos ou termos pequenos e genericos demais) e deve ser uma palavra individual, deve ser normalizada para lowercase (em minúsculo) e não deve conter nenhum caractere especial ou acento.
Assim, para a busca "Congelados e Hortaliças" nossos termos de busca agora serão:
[
"congelados",
"hortalicas"
]
Agora que temos nosso termo de busca, precisamos definir também que: A mesma normalização sofrida pelos termos de busca deve ser aplicada nos termos buscados. Isso garante que teremos normalizado todas as palavras de forma que as comparações sejam mais assertivas.
Vamos então criar o código de-facto que faça a busca/filtro de acordo com todos os critérios acima. Para tal continuemos a imaginar que o usuário buscou por Congelados e Hortaliças
Para nossa implementação vamos fazer o seguinte: uma função chamada searchOnArray que deve receber a palavra-frase buscada e retornar os itens filtrados que foram encontrados para tal palavra-frase em nosso array. Nossa função terá então a seguinte assinatura:
function searchOnArray(phrase: string, list: string[]): array
Embora pareça, o código acima não é TS, é apenas uma assinatura comum de função. Para nossos exemplos vamos usar JS puro. Nesse caso vemos que nossa função deve receber a frase buscada, receber um array de strings e retornar esse array filtrado de acordo com a frase e nossos critérios de busca. Então de início teremos:
/*
Veja o array de tags presentes no primeiro exemplo desse
post para evitar-mos reescreve-las aqui
*/
const tags = [...];
/*
Imagine que foi isso que o usuario digitou no input
de busca e vc coletou para uma constante no contexto atual
*/
const phrase = 'Congelados e Hortaliças';
/*
Chamamos nossa função passando o que foi buscado e nossa
lista de tags, e esperaremos um array de volta
*/
const results = searchOnArray(phrase, list);
Ok, temos a parte de input (entrada de dados), agora precisamos fazer nosso output (saída). Ou seja: vamos escrever a função que executa o filtro. Esse é o código completo, dê uma lida sem panico, ele será explicado tim tim por tim tim aqui:
function searchOnArray(phrase, list) {
const hasPhrase = phrase && typeof phrase === 'string';
const hasList = Array.isArray(list) && list.length;
if (!hasPhrase || !hasList) {
return [];
}
const terms = phrase.split(' ')
.filter(term => term.length > 3)
.map(term => normalize(term));
const normalizedList = list
.filter(term => term.length > 3)
.map(term => normalize(term));
if (!terms.length || !normalizedList.length) {
return [];
}
return normalizedList.filter(item => {
return terms.some(term => item.includes(term));
});
}
function createTerms(words) {
return arr
.filter(word => word.length > 3)
.map(word => normalize(word));
}
function normalize(str) {
return str
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "");
}
Utilizar nossa função é bem simples, você passa a frase a ser buscada e os itens para filtro.
const tags = [
"Produto comum",
"Produto diferentão",
"Produto de dia das mães",
"Produtos para caçar fantasmas",
"Produtos ilícitos",
"Frios",
"Aves",
"Congelados",
"Biscoitos",
"Temperos",
"Hortaliças",
"Carne Bovina",
"Carne suína",
"Bebidas Alcoolicas",
"Refrigerantes",
"Padaria",
"Alimentos (cereais e grãos)",
"Congelados e frios",
"Hortifruti",
"Produtos de limpeza",
"Higiene pessoal",
"Papelaria"
];
const filteredTags = searchOnArray('Congelados e Hortaliças', tags);
// filteredTags: (3) ['congelados', 'hortalicas', 'congelados e frios']
Vamos entender nossa função pedacinho por pedacinho. Para fazer isso, começaremos dos helpers. Começaremos com a normalizeStr:
/*
Esta função utiliza a prototype chain para chamar
diversos metodos para um string (str). Primeiro passamos
a string para lower case (minúscula) com o metodo toLowerCase(),
depois normalizamos a string para remover acentos e caracteres
compostos, depois removemos diacriticos via regex limitando
o range de caracteres que a string deve utilizar. Você encontra
uma explicação detalhada desse método neste
LINK
*/
function normalize(str) {
return str
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "");
}
Agora vamos ver o que faz a createTerms:
/*
Esta função recebe um array de strings e filtra
esse array mantendo apenas palavras maiores que
3 caracteres (.filter), depois mapeia os itens
resultantes normalizando cada um através da nossa
função normalize
*/
function createTerms(words) {
return arr
.filter(word => word.length > 3)
.map(word => normalize(word));
}
E por fim, a mágica:
function searchOnArray(phrase, list) {
/*
Inicialmente verificamos se a frase passada é valida,
depois também verificamos se a lista para filtro é
realmente um array e se tem algo nela. Caso alguma
dessas condições não seja verdadeira, é inviável fazer
a busca, então já retornamos um []. O nome dessa tecnica
é "early return"
*/
const hasPhrase = phrase && typeof phrase === 'string';
const hasList = Array.isArray(list) && list.length;
if (!hasPhrase || !hasList) {
return [];
}
/*
Agora nós criamos nossa lista de termos que utilizaremos
pra filtrar a lista e itens. Primeiro fazemos um split
em nossa string passando ' ' (espaço) como delimitador,
assim convertemos a string num array de palavras. Depois
filtramos este array mantendo apenas os itens dele que
tenham mais do que 3 caracteres, e finalmente mapeamos
cada item do nosso array de palavras normalizando a string
com nossa função normalize
*/
const terms = phrase.split(' ')
.filter(term => term.length > 3)
.map(term => normalize(term));
/*
A mesma coisa que fizemos acima vamos fazer com cada item
da lista de itens a ser filtrada. É importante dizer que esses
métodos não modificam os dados originais, ele retornam novos
dados, assim estamos seguindo um princípio de imutabilidade.
Após filtrar e normalizar nossa lista de itens, temos tanto
terms quato normalizedList prontos para serem comparados e
filtrados
*/
const normalizedList = list
.filter(term => term.length > 3)
.map(term => normalize(term));
/*
Fazemos então uma nova verificação, precisamos saber se os
termos que geramos retornou alguma coisa no array após serem
tratados, e se normalizedList também retornou algo. Se algum
desses estiver vazio, a busca é inviável e retornamos um []
*/
if (!terms.length || !normalizedList.length) {
return [];
}
/*
Aqui é onde a mágica acontece, vamos primeiro filtrar os
itens em normalizedItens. Se vc notar, criamos uma função
para fazer esse filtro, dentro dessa função está nosso
critério de filtro:
*/
return normalizedList.filter(item => {
/*
Aqui é o critério do nosso filtro, se essa condição retornar true
o item permanece no array resultanto, do contrario ele é removido.
A condição é a seguinte: Ao menos um item em terms (.some) deve
ser true para a condição item.includes(term), essa segunda condição
verifica se o term em questão existe na string sendo comparada.
Confuso né? Em outras palavras, essa linha passa termo por termo em
terms verificando se o term (item atual) existe dentro do item de
normalizedList, se ao menos um termo existir, temos um match.
return terms.some(term => item.includes(term));
});
}
Para cumprir todos os nossos criterios, temos então
cosnt tags = [ /* imagine a lista de tags aqui pra economizar espaço visual */ ];
const phrase = 'Congelados e Hortaliças';
/*
Funcão simples que verifica a frase passada e uma lista de tags,
caso a frase exista filtramos o array de tags, do contrário retornamos
todas as tags pois um dos critérios é que uma busca vazia retornaria
todos os itens.
*/
function filterTags(phrase, tags) {
return phrase ? searchOnArray(phrase, tags) : tags;
}
Após ler esse post você deve conseguir fazer seu algoritmo de busca genérica numa boa. É relativamente simples usar os conceitos aqui para modificiar esse algoritmo de acordo com suas necessidades, você pode: buscar por tipos diferentes de strings, ou por tipos multiplos; você pode modificar a stretura para iterar um objeto ao invés de apenas array, você pode adicionar um parametro "keywords" nesse objeto e fazer a busca olhando também para palavras chave relacionadas a cada item... Enfim, as possibilidades são infinitas. Happy Coding ^^