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で自作した静的サイトジェネレータで生成されてます。MarkdwonをHTML変換する部分も自作してGitHubで公開してます→ https://github.com/aragig/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だとほんと簡単で良いですよね。なんかこれだけJSが人気あるのも分かる気がします。

まとめ

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

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

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

関連記事

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

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