JavaScriptでサイト内検索の開発 StaticSeeker

JavaScriptでサイト内検索ができる StaticSeeker というライブラリを作ってみました。といっても、サイト内を直接捜索するわけではなく、あらかじめ用意されたサイトのインデックスファイルをJSON形式で読み込んで、その中から検索ワードとマッチングさせて結果を表示させる仕組みです。実際に作った検索ボックスは、このサイトの下部に設置されています。

ソースコードは意外にも簡潔に書けるので、ぜひご参考ください。

検索ボックスのHTMLの準備

下記の検索ボックスを準備します。searchBox IDの input テキストフィールドと、 searchResults ID の検索結果用の要素が必要です。

html
<div id="staticSeaker">
    <input type="text" id="searchBox" placeholder="検索...">
    <div id="searchResults"></div>
</div>
<script src="/js/static_seeker.js" data-json-path="/json/site_search_index.json"></script>
static_seeker.js が後ほど紹介する検索機能のJavaScriptプログラムになります。site_search_index.json はあらかじめ用意された、サイトのインデックスデータです。

サイトのインデックスデータをJSONで用意

先ほどの site_search_index.json の内容例がこちらです。

json
{
    "vim シェル コマンド linux macos アイデアノート": {
        "article": {
            "title": "よく使う便利なVim操作まとめ",
            "url": "https://101010.fun/programming/vim-usage.html"
        }
    },
    "ラズパイ Raspberry Pi 4 3 SDカード 丸まる 丸ごと コピー 複製 方法 やり方 アイデアノート 電子工作 IoT Arduino esp32": {
        "article": {
            "title": "Raspberry PiのSDカードを丸ごとコピーしてバックアップする方法、やり方",
            "url": "https://101010.fun/iot/raspi-sd-card-copy.html"
        }
    }
}

各ブログページのキーワードとブログタイトル、URLを辞書型の配列で構成されたJSONです。キーワードは形態素解析などで抽出する大袈裟なものではなく、手入力のキーワード情報です。Markdownで記事を管理しているので、ヘッダーにメタ情報を埋め込めるようにしています。ちなみにこのサイトは、Pythonで自作した静的サイトジェネレータで生成されています。MarkdownをHTMLに変換する部分も自作してGitHubで公開しています→ ssgen。ということで、このJSONファイルは自前で用意してください。

JavaScriptの検索プログラム

ここからが本題の JavaScript による検索プログラムです。たったこれだけのソースコードで動いています。

js
document.addEventListener('DOMContentLoaded', function () {
    const searchBox = document.getElementById('searchBox');

    if (!searchBox) return; // searchBoxが存在しない場合は処理を終了

    const jsonPath = document.querySelector('script[data-json-path]').getAttribute('data-json-path');

    searchBox.addEventListener('input', function () {
        const searchText = this.value.toLowerCase();
        if (searchText.trim() === "") {
            document.getElementById('searchResults').innerHTML = "";
            return;
        }

        fetch(jsonPath)
            .then(response => response.json())
            .then(data => {
                let resultsHTML = "";
                const searchWords = searchText.split(/\s+/); // スペースで区切られた検索ワードを配列に変換

                Object.keys(data).forEach(word => {
                    const keywordLowerCase = word.toLowerCase();
                    // すべての検索ワードがキーワードに含まれるかどうかを確認
                    const isMatch = searchWords.every(searchWord => keywordLowerCase.includes(searchWord));

                    if (isMatch) {
                        resultsHTML += `<a href="${data[word].article.url}">${data[word].article.title}</a><br>`;
                    }
                });
                document.getElementById('searchResults').innerHTML = resultsHTML;
            });
    });
});

解説

次のコードは <script>タグのdata-json-path属性から先ほどのJSONファイルのパスを取得します。

js
const jsonPath = document.querySelector('script[data-json-path]').getAttribute('data-json-path');
searchBox.addEventListener('input', function () { で、検索ボックスにテキストを入力するたびに、イベントを発生させています。その後の入力テキストの処理では、入力したテキストを小文字に変換し、入力が空白のみの場合は検索結果をクリアして処理を終了します。fetch APIではJSONファイルを非同期に取得し、そのデータを解析します。

入力されたテキストはスペースで区切ることが可能で、それらのワードがJSONデータ内のキーワードに含まれているかどうかをマッチさせているのが次の部分です。

js
let resultsHTML = "";
const searchWords = searchText.split(/\s+/); // スペースで区切られた検索ワードを配列に変換

Object.keys(data).forEach(word => {
    const keywordLowerCase = word.toLowerCase();
    // すべての検索ワードがキーワードに含まれるかどうかを確認
    const isMatch = searchWords.every(searchWord => keywordLowerCase.includes(searchWord));

    if (isMatch) {
        resultsHTML += `<a href="${data[word].article.url}">${data[word].article.title}</a><br>`;
    }
});

意外と仕組みは単純ですが、実用に十分な検索ボックスを作ることができました。もちろん、JSONデータがページ数やキーワードの量が多い場合は、メモリの消費問題がネックになってきます。とはいえ、400記事程度のこのサイトのインデックスで100kB以下ですから、1MBくらいまで実用できると考えればだいぶ余裕があると思います。

最後にマッチしたキーワードに対応する記事のタイトルとURLをsearchResults要素に動的に表示します。

js
resultsHTML += `<a href="${data[word].article.url}">${data[word].article.title}</a><br>`;

キーボードの「/」スラッシュキーでテキストフィールドにフォーカスが当たるようにする

GitHubなんかで実装されているアレですね。次のようにして簡単に実現できました。

js
document.addEventListener('keydown', function(event) {
    // スラッシュキーが押されたとき、かつテキストフィールドやテキストエリアにフォーカスがない場合に実行
    if (event.key === '/' && document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA') {
        event.preventDefault(); // ブラウザのデフォルト動作をキャンセル
        searchBox.scrollIntoView(); // テキストフィールドがある位置までスクロール
        searchBox.focus(); // 検索ボックスにフォーカスを設定
    }
});

上記のスクリプトをDOMContentLoadedイベントリスナー内に追加することで、テキストフィールドがある最下部までスクロールし、フォーカスを当ててくれます。私のブログ「アイデアノート」はその名の通り、自分用のノートでもあるので、秒で検索できてとても便利になりました(もっと早く作っておけばよかった)。アプリ開発でこういうことを実現させようとすると、ライフサイクルやスレッドセーフの問題で結構面倒ですが、JavaScriptだと本当に簡単で良いですね。これだけJavaScriptが人気があるのも分かる気がします。

まとめ

今回ご紹介した内容のほとんどは、ChatGPTに教えてもらいながら作ったサイト内検索プログラムです。以前からずっと欲しいと思っていたのですが、面倒な作業だと思い込んでおり手付かずにしていました。ChatGPTのおかげで、こんなに簡単に実現できて驚きました。

JavaScriptとJSONの親和性のおかげでもありますね。あまりJSでプログラミングしないので忘れていましたが、JSONってJavaScript Object Notationなので、JavaScriptだとJSONデータがそのままオブジェクトとして扱えるんですよね。他のプログラミング言語だと、JSONをパースしてオブジェクトに変換しなければなりません。GSONなどのマッピングライブラリでだいぶ楽に変換できるようになりましたが、それでもJSONが出てくるとなんか面倒に感じてしまいます。改めてJavaScriptの魅力を感じたプロジェクトでした。

ということで、本記事で紹介した検索ボックス「StaticSeeker」の動きは、このページの下部に設置されている検索ボックスでご確認いただけます。また、StaticSeekerはGitHubリポジトリで公開中ですので、ぜひご参考ください。

関連記事

最後までご覧いただきありがとうございます!

▼ 記事に関するご質問やお仕事のご相談は以下よりお願いいたします。
お問い合わせフォーム