brief: | i18n.site はサーバーレスの全文検索をサポートするようになりました。

この記事では、IndexedDB によって構築された転置インデックス、接頭辞検索、単語分割の最適化、多言語サポートなど、純粋なフロントエンド全文検索テクノロジの実装について紹介します。

既存のソリューションと比較して、i18n.site の純粋なフロントエンド全文検索はサイズが小さく高速であり、ドキュメントやブログなどの中小規模の Web サイトに適しており、オフラインで利用できます。


純粋なフロントエンド逆引き全文検索

順序

数週間の開発を経て、 i18n.site (純粋に静的なmarkdown multilingualtranslation &ウェブサイト構築ツール) は、純粋なフロントエンド全文検索をサポートするようになりました。

この記事では、純粋i18n.siteフロントエンド全文検索の技術的な実装について説明します。検索効果を体験してi18n.site

オープン:のコード検索カーネル/インタラクティブ インターフェイス

サーバーレス全文検索ソリューションのレビュー

ドキュメントや個人のブログなど、中小規模の純粋に静的な Web サイトの場合、独自に構築した全文検索バックエンドを構築するのは重すぎるため、サービス不要の全文検索がより一般的な選択肢となります。

サーバーレス全文検索ソリューションは、次の 2 つの大きなカテゴリに分類されます。

まず、同様algolia.comサードパーティ検索サービス プロバイダーが全文検索用のフロントエンド コンポーネントを提供しています。

このようなサービスは検索ボリュームに基づいて料金を支払う必要があり、Web サイトのコンプライアンスなどの問題により、中国本土のユーザーは利用できないことがよくあります。

オフラインでもイントラネットでも使用できず、大きな制限があります。 この記事ではあまり議論しません。

2 つ目は、純粋なフロントエンドの全文検索です。

現在、一般的な純粋なフロントエンドの全文検索には、 lunrjsElasticLunr.js ( lunrjs二次開発に基づく) が含まれます。

lunrjsインデックスを構築するには 2 つの方法があり、どちらにも独自の問題があります。

  1. 事前に構築されたインデックスファイル

    索引にはすべての文書の単語が含まれているため、サイズが大きくなります。 ドキュメントが追加または変更されるたびに、新しいインデックス ファイルをロードする必要があります。 ユーザーの待ち時間が長くなり、大量の帯域幅が消費されます。

  2. ドキュメントをロードし、その場でインデックスを構築します

    インデックスの構築は、大量の計算を必要とするタスクであり、アクセスするたびにインデックスを再構築すると、明らかな遅延が発生し、ユーザー エクスペリエンスが低下します。


lunrjsに加えて、 :などの他の全文検索ソリューションもいくつかあります。

fusejs 、検索する文字列間の類似性を計算します。

このソリューションのパフォーマンスは非常に低く、全文検索には使用できません (「 Fuse.js長いクエリには10秒以上かかります。最適化するには?」を参照してください)。

TinySearchは、ブルーム フィルターを使用して検索しますが、接頭辞検索 (たとえば、 gooを入力し、 goodgoogle検索) には使用できず、同様の自動補完効果は得られません。

既存のソリューションには欠点があるため、 i18n.site次の特徴を持つ新しい純粋なフロントエンド全文検索ソリューションを開発しました:

  1. 6.9KB gzip (比較のために、 lunrjsのサイズは25KBです)。
  2. indexedbに基づいて転置インデックスを構築します。これはメモリの使用量が少なく、高速です。
  3. ドキュメントが追加/変更された場合、追加または変更されたドキュメントのみが再インデックスされるため、計算量が削減されます。
  4. 接頭辞検索をサポートし、ユーザーが入力中にリアルタイムで検索結果を表示できます。
  5. オフラインで利用可能

以下では、 i18n.site的な実装の詳細について詳しく紹介します。

多言語単語の分割

単語のセグメンテーションでは、ブラウザーのネイティブの単語セグメンテーションIntl.Segmenterが使用され、すべての主流ブラウザーがこのインターフェイスをサポートしています。

単語分割coffeescriptコードは次のとおりです。

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()

で:

インデックスの構築

IndexedDBで 5 つのオブジェクト ストレージ テーブルが作成されました:

document urlとバージョン番号verの配列を渡し、テーブルdocにドキュメントが存在するかどうかを検索します。存在しない場合は、転置インデックスを作成します。同時に、渡されなかったドキュメントの逆索引を削除します。

このようにして、インクリメンタルなインデックス作成が実現され、計算量が削減されます。

フロントエンド対話では、初回ロード時の遅延を避けるために、インデックスのロード進行状況バーを表示できます。「単一のprogress + Pure css実装に基づくアニメーション付きの進行状況バー」 English / Chinese を参照してください。

IndexedDB の高い同時書き込み

このプロジェクトは、 IndexedDBの非同期カプセル化に基づいてidb

IndexedDB の読み取りと書き込みは非同期です。インデックスを作成するときは、ドキュメントが同時にロードされてインデックスが作成されます。

競合書き込みによる部分的なデータ損失を回避するには、以下のcoffeescriptコードを参照し、読み取りと書き込みの間にingキャッシュを追加して競合書き込みを遮断します。

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()

精度と再現率

検索では、まずユーザーが入力したキーワードがセグメント化されます。

単語分割後の単語がNであると仮定します。結果を返す場合、最初にすべてのキーワードを含む結果が返され、次にN-1N-2 、...、 1キーワードを含む結果が返されます。

最初に表示される検索結果によってクエリの正確さが保証され、その後読み込まれる結果 ([さらに読み込む] ボタンをクリック) によって再現率が保証されます。

オンデマンドでロード

応答速度を向上させるために、検索ではyieldジェネレーターを使用してオンデマンド読み込みを実装し、結果がクエリされるたびlimit結果を返します。

yield後に再度検索するたびに、 IndexedDBのクエリ トランザクションを再度開く必要があることに注意してください。

プレフィックスリアルタイム検索

ユーザーが入力中に検索結果を表示するため、たとえばworを入力すると、 wordsworkなど先頭にworが付く単語が表示されます。

検索カーネルは、単語の分割後の最後の単語に対してprefixテーブルを使用して、その接頭辞が付いたすべての単語を検索し、順番に検索します。

手ぶれ補正機能debounceは、検索をトリガーするユーザー入力の頻度を減らし、計算量を減らすために、フロントエンド インタラクション (次のように実装) でも使用されます。

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

オフラインで利用可能

インデックス テーブルには元のテキストは保存されず、単語のみが保存されるため、ストレージの量が削減されます。

検索結果を強調表示するには、元のテキストを再ロードする必要があり、 service workerと一致すると、ネットワーク要求が繰り返されることを回避できます。

同時に、 service workerすべての記事をキャッシュするため、ユーザーが検索を実行すると、検索を含む Web サイト全体がオフラインで利用できるようになります。

MarkDown ドキュメントの表示の最適化

i18n.siteの純粋なフロントエンド検索ソリューションは、 MarkDownドキュメント用に最適化されています。

検索結果を表示すると章名が表示され、クリックすると章へ移動します。

要約する

逆引き全文検索は純粋にフロントエンドに実装されており、サーバーは必要ありません。ドキュメントや個人のブログなどの中小規模の Web サイトに非常に適しています。

i18n.siteオープンソースの自社開発の純粋なフロントエンド検索は、サイズが小さく応答が速く、現在の純粋なフロントエンド全文検索の欠点を解決し、より良いユーザー エクスペリエンスを提供します。