Criando uma funcionalidade de Busca Genérica com JavaScript

← ← ←   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?

Definindo o que é termo de busca

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.

Implementação

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, "");
  }
  

Utilizando a função searchOnArray

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']
  

Entendendo a função searchOnArray

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));
    });
  }
  

Algoritmo final

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;
  }
  

Conclusão

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 ^^