brief: | i18n.site agora suporta pesquisa de texto completo sem servidor.

Este artigo apresenta a implementação de tecnologia de pesquisa de texto completo puramente front-end, incluindo a construção de índice invertido com IndexedDB, pesquisa de prefixo, otimização de segmentação de palavras e suporte multilíngue.

Em comparação com as soluções existentes, a pesquisa de texto completo puramente front-end do i18n.site é menor e mais rápida, adequada para sites de pequeno e médio porte, como documentos e blogs, e está disponível offline.


Pesquisa de texto completo invertida puramente front-end

Resumo

Após várias semanas de desenvolvimento, i18n.site (ferramenta de construção de sites estáticos e multilíngue para markdown) agora suporta pesquisa de texto completo puramente front-end.

Este artigo compartilha a implementação técnica da pesquisa de texto completo i18n.site puramente front-end. Acesse i18n.site para experimentar o efeito da pesquisa.

Código aberto: Núcleo de pesquisa / Interface interativa

Visão geral das soluções de pesquisa de texto completo sem servidor

Para sites puramente estáticos de pequeno e médio porte, como documentos/blogs pessoais, construir um back-end de pesquisa de texto completo autoconstruído é muito pesado, e a pesquisa de texto completo sem serviço é a escolha mais comum.

As soluções de pesquisa de texto completo sem servidor podem ser divididas em duas grandes categorias:

Primeiro, provedores de serviços de pesquisa terceirizados, como algolia.com, oferecem componentes front-end para pesquisa de texto completo.

Esses serviços cobram por volume de pesquisa e muitas vezes não estão disponíveis para usuários na China continental devido a questões de conformidade do site.

Não são usáveis off-line e não podem ser usadas na rede interna, o que limita muito. Este artigo não entrará em detalhes.

A segunda é a pesquisa de texto completo puramente front-end.

Atualmente, as soluções comuns de pesquisa de texto completo front-end incluem lunrjs e ElasticLunr.js (desenvolvido a partir do lunrjs).

O lunrjs oferece duas maneiras de construir índices, mas ambas têm suas próprias deficiências.

  1. Arquivos de índice pré-construídos

    Como o índice contém palavras de todos os documentos, ele é grande. Quando um documento é adicionado ou modificado, um novo arquivo de índice deve ser carregado. Isto aumentará o tempo de espera do usuário e consumirá muita largura de banda.

  2. Carregar documentos e criar índices dinamicamente

    A criação de índices é uma tarefa intensiva em termos de computação. A reconstrução do índice sempre que acessado causará atrasos óbvios e uma má experiência do usuário.


Além do lunrjs, há outras soluções de pesquisa de texto completo, como:

fusejs, que calcula a semelhança entre strings a serem pesquisadas.

A performance dessa solução é extremamente ruim e não pode ser usada para pesquisa de texto completo (veja Como otimizar a consulta longa do Fuse.js que leva mais de 10 segundos?).

TinySearch, que usa o filtro Bloom para pesquisa, não pode ser usado para pesquisa de prefixo (por exemplo, inserir goo e pesquisar good, google) e não pode implementar efeitos de conclusão automática semelhantes.

Dada as deficiências das soluções existentes, o i18n.site desenvolveu uma nova solução de pesquisa de texto completo puramente front-end, com as seguintes características:

  1. Suporta pesquisa em vários idiomas e é pequeno. O tamanho do kernel de pesquisa após o empacotamento gzip é 6.9KB (para comparação, o tamanho de lunrjs é 25KB )
  2. Cria um índice invertido baseado em IndexedDB, ocupando menos memória e sendo rápido
  3. Quando documentos são adicionados/modificados, apenas os documentos adicionados ou modificados são reindexados, reduzindo a quantidade de cálculos
  4. Suporta pesquisa de prefixo, exibindo resultados de pesquisa em tempo real enquanto o usuário digita
  5. Disponível off-line

Abaixo, detalharemos os detalhes técnicos de implementação do i18n.site.

Segmentação de palavras multilíngue

A segmentação de palavras utiliza a interface nativa Intl.Segmenter do navegador, que é suportada por todos os navegadores convencionais.

O código de segmentação coffeescript é o seguinte:

SEG = new Intl.Segmenter 0, granularity: "word"

seg = (txt) =>
  r = []
  for {segment} from SEG.segment(txt)
    for i from segment.split('.')
      i = i.trim()
      if i and !'|`'.includes(i) and !/\p{P}/u.test(i)
        r.push i
  r

export default seg

export segqy = (q) =>
  seg q.toLocaleLowerCase()

Entre outras coisas:

Construção de índice

Foram criadas cinco tabelas de armazenamento de objetos no IndexedDB:

É passado um array de url do documento e o número da versão ver, e é verificado se o documento existe na tabela doc. Se não existir, é criado um índice invertido. Ao mesmo tempo, são removidos os índices invertidos dos documentos não transmitidos.

Desta forma, é alcançada a indexação incremental, reduzindo a quantidade de cálculos.

No interação front-end, pode ser exibida uma barra de progresso de carregamento do índice para evitar o atraso ao carregar pela primeira vez. Veja "Barra de progresso com animação, baseada em um único progress + Implementação css pura" Inglês / Chinês.

Escrita simultânea alta do IndexedDB

O projeto é idb com base no encapsulamento assíncrono de IndexedDB

As leituras e gravações do IndexedDB são assíncronas. Ao criar um índice, os documentos serão carregados simultaneamente para criar o índice.

Para evitar perda parcial de dados devido à escrita competitiva, você pode consultar o código coffeescript abaixo e adicionar um cache ing entre leitura e gravação para interceptar escritas concorrentes.

pusher = =>
  ing = new Map()
  (table, id, val)=>
    id_set = ing.get(id)
    if id_set
      id_set.add val
      return

    id_set = new Set([val])
    ing.set id, id_set
    pre = await table.get(id)
    li = pre?.li or []

    loop
      to_add = [...id_set]
      li.push(...to_add)
      await table.put({id,li})
      for i from to_add
        id_set.delete i
      if not id_set.size
        ing.delete id
        break
    return

rindexPush = pusher()
prefixPush = pusher()

Precisão e recall

A pesquisa primeiramente segmenta as palavras-chave inseridas pelo usuário.

Suponha que haja N palavras após a segmentação de palavras. Ao retornar resultados, os resultados contendo todas as palavras-chave serão retornados primeiro e, em seguida, os resultados contendo N-1, N-2, ..., 1 palavras-chave serão retornados.

Os resultados da pesquisa exibidos primeiro garantem a precisão da consulta, e os resultados carregados posteriormente (ao clicar no botão "Carregar mais") garantem a taxa de recuperação.

Carregamento sob demanda

Para melhorar a velocidade de resposta, a busca utiliza o gerador yield para implementar o carregamento sob demanda e retorna limit resultados consultados.

Observe que cada vez que você pesquisar novamente após yield, será necessário reabrir uma transação de consulta do IndexedDB.

Pesquisa de prefixo em tempo real

Para exibir os resultados da pesquisa enquanto o usuário digita, por exemplo, ao inserir wor, palavras com prefixo wor como words e work são exibidas.

O kernel de pesquisa usará a tabela prefix para a última palavra após a segmentação de palavras para encontrar todas as palavras com esse prefixo e pesquisar em sequência.

A função anti-vibração debounce também é usada na interação front-end (implementada da seguinte forma) para reduzir a frequência de entradas do usuário que acionam pesquisas e reduzir a quantidade de cálculos.

export default (wait, func) => {
  var timeout;
  return function(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(func.bind(this, ...args), wait);
  };
}

Disponível off-line

A tabela de índice não armazena o texto original, apenas as palavras, o que reduz a quantidade de armazenamento.

Destacar os resultados da pesquisa requer recarregar o texto original, e combinar service worker pode evitar solicitações repetidas de rede.

Ao mesmo tempo, como service worker armazena em cache todos os artigos, assim que o usuário realizar uma pesquisa, todo o site, incluindo a pesquisa, fica disponível off-line.

Otimização de exibição de documentos Markdown

A solução de busca front-end pura do i18n.site é otimizada para documentos Markdown.

Ao exibir os resultados da pesquisa, o nome do capítulo será exibido e o capítulo será navegado ao clicar.

Resumo

Pesquisa invertida de texto completo implementada exclusivamente no front-end, sem necessidade de servidor. É muito adequada para sites de pequeno e médio porte, como documentos e blogs pessoais.

i18n.site A pesquisa de front-end pura autodesenvolvida de código aberto, pequena em tamanho e rápida, resolve as deficiências da pesquisa de texto completo puramente front-end atual e fornece uma melhor experiência do usuário.