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.
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
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.
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.
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:
gzip
é 6.9KB
(para comparação, o tamanho de lunrjs
é 25KB
)IndexedDB
, ocupando menos memória e sendo rápidoAbaixo, detalharemos os detalhes técnicos de implementação do i18n.site
.
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:
/\p{P}/
é uma expressão regular que corresponde a sinais de pontuação. Os símbolos correspondentes específicos incluem: ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _
{ | } .</p><ul><li>
split('.')é porque a segmentação de palavras do navegador
Firefoxnão segmenta
.` .
Foram criadas cinco tabelas de armazenamento de objetos no IndexedDB
:
word
: id - palavradoc
: id - URL do documento - versão do documentodocWord
: id do documento - array de id de palavraprefix
: prefixo - array de id de palavrarindex
: id de palavra - id do documento : array de números de linhaÉ 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.
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()
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.
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
.
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);
};
}
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.
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.
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.