情シス申請なしで高精度な日本語検索システムが構築できるのです！

## 背景

日本語の文書検索をWebアプリケーションで実現しようとしたとき、「形態素解析」というキーワードにぶつかる方は多いのではないでしょうか。Janomeのような強力なライブラリを使えば高精度な処理が可能ですが、一方でサーバーサイドでの実行やライブラリの管理、処理速度のオーバーヘッドなど、導入には課題も伴います。

特に、**ブラウザだけで完結するWebアプリケーションで日本語検索を実装したい場合、この依存性が大きな壁となるでしょう。**

しかし、ご安心ください！
実は、**あのJANOMEを完全に回避しつつ、ブラウザだけで、それ以上の精度を実現できる画期的な方法があるんです。**
今回は、その「JANOME回避の秘密」を深掘りしていきます。

### 🔍 なぜ形態素解析（JANOME）を回避するのか？

Janomeなどの形態素解析ライブラリは、日本語の文章を「意味を持つ最小単位（形態素）」に分割し、品詞などの情報を付与する強力なツールです。その精度は非常に高いですが、以下のような制約があります。

*   **サーバーサイド実行の必要性**: Pythonなどの実行環境が必要。ブラウザ単体では動作しない。
*   **ライブラリの依存性**: 環境構築やデプロイが複雑になる要因。
*   **辞書ベース**: 辞書の更新やメンテナンスが必要な場合がある。未知語への対応が難しいケースも。
*   **処理速度**: 辞書参照などの処理で、テキスト量によっては時間がかかる。

これらの課題を解決し、より軽量で柔軟なシステムを構築するために、「形態素解析を回避する」というアプローチが考案されました。

### 🧠 形態素解析回避の核心技術：HTML版の賢いアプローチ

では、どのようにしてJANOMEを回避し、かつ高精度な検索を実現しているのでしょうか？ その秘密は、日本語の特性を最大限に活用した **「文字種別による自動分割」** と **「N-gramによる境界推定」** 、そして **「重み付け」** の組み合わせにあります。

提供されたJavaScript（HTML版）のコードを例に見ていきましょう。

#### 1. 品詞別パターンマッチング（文字種別による自動分割）

日本語の文章は、ひらがな、カタカナ、漢字、英数字など、多様な文字種が混在しています。この文字種の特性を活かし、正規表現で「それっぽい単語」を抽出します。これは形態素解析の「品詞判定」とは異なりますが、実用上は非常に有効です。

```javascript
// 🎯 文字種別による自動分割
const englishWords = clean.match(/\b[a-zA-Z]{2,}\b/g) || [];     // 例: "python", "scikit-learn"
const katakanaWords = clean.match(/[ァ-ヶー]{2,}/g) || [];       // 例: "ライブラリ", "フレームワーク"
const kanjiSequences = clean.match(/[一-龥]{1,}/g) || [];        // 例: "機械", "学習", "入門", "最適化"
```

*   **英語**: `\b[a-zA-Z]{2,}\b` で単語の境界を自動検出し、英単語（2文字以上）を抽出します。
*   **カタカナ**: `[ァ-ヶー]{2,}` で連続するカタカナ（2文字以上）を一つの単語として抽出します。特に技術文書では、カタカナは外来語や技術用語を指すことが多く、重要なキーワードとなります。
*   **漢字**: `[一-龥]{1,}` で連続する漢字を意味単位として抽出します。漢字はそれ自体が概念を表すため、連続する漢字列を抽出するだけでも十分な意味を持つことが多いです。

#### 2. N-gramによる境界推定

上記の文字種別抽出だけでは取りこぼしが出る場合や、より網羅性を高めたい場合にN-gramが活躍します。特に日本語の結合名詞（例：「機械学習入門」）において、形態素解析なしでも単語境界を推定する強力な手法です。

```javascript
// 🎯 N-gramで単語境界を推定
// 例: 「機械学習入門」というテキストに対して
// バイグラム (n=2): 機械, 械学, 学習, 習入, 入門
// トリグラム (n=3): 機械学, 械学習, 学習入, 習入門
for (let n = 2; n <= 4; n++) { // 2文字から4文字のN-gramを生成
    for (let i = 0; i <= japanese.length - n; i++) {
        tokens.push(japanese.substring(i, i + n));
    }
}
```

例えば「機械学習入門」という文字列があった場合、形態素解析では「機械」「学習」「入門」と分かれることが多いでしょう。N-gramでは「機械」「械学」「学習」「習入」「入門」といったトークンが生成されます。一見ノイズが多いように見えますが、検索時には「機械」「学習」「入門」といったキー単語が確実に含まれるため、**部分一致による高いスコア**を生み出すことができます。

#### 3. 重み付けによる精度向上

さらに、特定の文字種（特に技術文書で重要なカタカナ語）に重み付けをすることで、検索精度を向上させることが可能です。これは、形態素解析とは独立した、検索スコアリングの工夫と言えます。

```javascript
// 🎯 重み付けによる精度向上（例: カタカナ語を1.5倍重要視）
// カタカナ語を複数回カウントすることで、類似度計算で有利にする
katakanaWords.forEach(word => {
    for (let i = 0; i < Math.round(katakanaWeight); i++) { // katakanaWeightが1.5なら1回、2.5なら2回追加
        tokens.push(word);  // 例: "ライブラリ"が複数回カウントされる
    }
});
```

### 📊 JANOME vs HTML版の比較：なぜこの手法が有効なのか

| 要素         | JANOME版（Pythonなど）       | HTML版（JavaScript）         | 回避手法・メリット                                |
| :----------- | :--------------------------- | :--------------------------- | :------------------------------------------------ |
| **単語分割** | 形態素解析                   | 文字種別 + N-gram            | ✅ **完全回避**：特別なライブラリ不要             |
| **品詞判定** | 辞書ベース                   | パターンマッチング           | ✅ **正規表現で代替**：柔軟性が高い               |
| **未知語処理** | 辞書にない単語は困難な場合あり | N-gramで確実にカバー         | ✅ **むしろ有利**：あらゆる文字列に対応            |
| **処理速度** | 辞書参照で重くなる傾向       | 正規表現と文字列処理で高速   | ✅ **大幅高速化**：ブラウザ上でサクサク動く      |
| **依存関係** | JANOMEなどのライブラリ必須   | **完全自前実装**             | ✅ **ゼロ依存**：どこでも簡単にデプロイ可能       |
| **日本語の特性** | 辞書に基づいた厳密な分析     | **文字種の意味性・N-gramの網羅性**をフル活用 | ✅ **日本語特化**：特定の文字種の意味を重視        |

このアプローチが有効な理由は、日本語の特性を最大限に活用している点にあります。

1.  **文字種の意味性**: 日本語のカタカナは外来語や技術用語、漢字は概念語を表すことが多いため、文字種自体に意味を持つという特性を活かせます。
2.  **技術文書の特徴**: 英語やカタカナのキーワードが重要語彙となる技術文書において、この抽出方法は特に有効です。
3.  **N-gramの網羅性**: どのような単語でも、その部分文字列をN-gramとして確実にカバーできるため、辞書にない未知語に対しても高い検出能力を発揮します。

例えば、クエリ「機械学習」で検索する場合を考えてみましょう。

*   **JANOME版**: トークンは `['機械', '学習']` の2語でマッチングされます。
*   **HTML版**: トークンは `['機械', '械学', '学習', '学習入', ...]` のように、より多くのトークンでヒットします。

これにより、部分一致も含めた網羅性が高まり、結果として高い類似度スコアが得られることがあります。

### 🚀 結論：形態

HTML版は **「形態素解析を使わずに形態素解析以上の効果」** を実現しています：

* JANOME完全不要
* 辞書メンテナンス不要
* 未知語に強い
* 高速処理
* 依存関係ゼロ

これにより、情シス申請なしで高精度な日本語検索システムが構築できるのです！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212914/f7c3edb8-b258-430d-a74d-114174722890.png)


```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>高精度TF-IDF文書検索システム（HTML版）</title>
    <style>
        body { 
            font-family: Arial, sans-serif; 
            margin: 20px; 
            background-color: #f5f5f5;
        }
        .container { 
            max-width: 1200px; 
            margin: 0 auto; 
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        .section { 
            margin: 20px 0; 
            padding: 15px; 
            border: 1px solid #ddd; 
            border-radius: 5px; 
            background: #fafafa;
        }
        .algorithm-selector {
            background: #e8f4f8;
            border: 2px solid #0066cc;
        }
        input[type="file"], input[type="text"] { 
            width: 400px; 
            padding: 8px; 
            margin: 5px; 
            border: 1px solid #ccc;
            border-radius: 4px;
        }
        button { 
            padding: 10px 20px; 
            margin: 5px; 
            background: #007bff; 
            color: white; 
            border: none; 
            border-radius: 4px; 
            cursor: pointer; 
            font-size: 14px;
        }
        button:hover { background: #0056b3; }
        button.algorithm-btn { background: #28a745; }
        button.algorithm-btn:hover { background: #1e7e34; }
        button.demo-btn { background: #6f42c1; }
        button.demo-btn:hover { background: #5a2d91; }
        
        #results { margin-top: 20px; }
        .result-item { 
            border: 1px solid #eee; 
            margin: 10px 0; 
            padding: 15px; 
            border-radius: 5px; 
            background: white;
        }
        .similarity { font-weight: bold; color: #007bff; }
        .algorithm-info {
            background: #fff3cd;
            border: 1px solid #ffeaa7;
            padding: 10px;
            border-radius: 4px;
            margin: 10px 0;
        }
        .performance-info {
            background: #d1ecf1;
            border: 1px solid #bee5eb;
            padding: 10px;
            border-radius: 4px;
            margin: 10px 0;
            font-family: monospace;
            font-size: 12px;
        }
        .comparison-table {
            width: 100%;
            border-collapse: collapse;
            margin: 10px 0;
        }
        .comparison-table th, .comparison-table td {
            border: 1px solid #ddd;
            padding: 8px;
            text-align: left;
        }
        .comparison-table th {
            background-color: #f2f2f2;
        }
        
        .tabs {
            display: flex;
            border-bottom: 2px solid #ddd;
            margin-bottom: 20px;
        }
        .tab {
            padding: 10px 20px;
            cursor: pointer;
            border: none;
            background: #f8f9fa;
            margin-right: 5px;
            border-radius: 5px 5px 0 0;
        }
        .tab.active {
            background: #007bff;
            color: white;
        }
        .tab-content {
            display: none;
        }
        .tab-content.active {
            display: block;
        }
        
        .vocab-analysis {
            background: #f8f9fa;
            padding: 15px;
            border-radius: 5px;
            margin: 10px 0;
        }
        
        .precision-info {
            background: #d4edda;
            border: 1px solid #c3e6cb;
            padding: 10px;
            border-radius: 4px;
            margin: 10px 0;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>📄 高精度TF-IDF文書検索システム（HTML版）</h1>
        
        <div class="precision-info">
            <strong>🎯 精度向上ポイント:</strong> L2正規化、改良IDF、ストップワード除去、重み調整を実装
        </div>
        
        <div class="tabs">
            <button class="tab active" onclick="switchTab('search')">検索</button>
            <button class="tab" onclick="switchTab('comparison')">アルゴリズム比較</button>
            <button class="tab" onclick="switchTab('analysis')">詳細分析</button>
            <button class="tab" onclick="switchTab('precision')">📊 精度比較</button>
        </div>
        
        <!-- 検索タブ -->
        <div id="search-tab" class="tab-content active">
            <div class="section">
                <h3>ファイル選択</h3>
                <input type="file" id="fileInput" accept=".csv,.txt" />
                <button onclick="loadDemo()" class="demo-btn">デモデータ</button>
            </div>
            
            <div class="section algorithm-selector">
                <h3>アルゴリズム選択</h3>
                <label>
                    <input type="radio" name="algorithm" value="jaccard"> 
                    Jaccard係数（軽量版）
                </label><br>
                <label>
                    <input type="radio" name="algorithm" value="tfidf_basic"> 
                    TF-IDF（基本実装）
                </label><br>
                <label>
                    <input type="radio" name="algorithm" value="tfidf_advanced" checked> 
                    TF-IDF（高精度版・sklearn相当）
                </label><br>
                <label>
                    <input type="radio" name="algorithm" value="tfidf_ultra"> 
                    TF-IDF（Ultra版・Python超越）
                </label>
            </div>
            
            <div class="section">
                <h3>検索オプション</h3>
                <label><input type="checkbox" id="useStopwords" checked> ストップワード除去</label>
                <label><input type="checkbox" id="useL2Norm" checked> L2正規化</label>
                <label><input type="checkbox" id="useImprovedIDF" checked> 改良IDF</label><br>
                <label>カタカナ重み: <input type="number" id="katakanaWeight" value="1.5" min="1" max="3" step="0.1" style="width:60px"></label>
                <label>英語重み: <input type="number" id="englishWeight" value="1.2" min="1" max="3" step="0.1" style="width:60px"></label>
            </div>
            
            <div class="section">
                <h3>検索</h3>
                <input type="text" id="queryInput" placeholder="検索語を入力" />
                <button onclick="search()" class="algorithm-btn">検索実行</button>
                <label>件数: <input type="number" id="topN" value="5" min="1" max="20" style="width:60px"></label>
            </div>
            
            <div id="results"></div>
        </div>
        
        <!-- 比較タブ -->
        <div id="comparison-tab" class="tab-content">
            <div class="section">
                <h3>アルゴリズム一括比較</h3>
                <input type="text" id="comparisonQuery" placeholder="比較用検索語" value="Python機械学習" />
                <button onclick="runComparison()" class="algorithm-btn">比較実行</button>
            </div>
            <div id="comparisonResults"></div>
        </div>
        
        <!-- 分析タブ -->
        <div id="analysis-tab" class="tab-content">
            <div class="section">
                <h3>前処理分析</h3>
                <input type="text" id="analysisText" placeholder="分析するテキスト" value="Python機械学習入門！scikit-learnライブラリを使った分析。" />
                <button onclick="runAnalysis()" class="algorithm-btn">分析実行</button>
            </div>
            <div id="analysisResults"></div>
        </div>
        
        <!-- 精度比較タブ -->
        <div id="precision-tab" class="tab-content">
            <div class="section">
                <h3>🎯 Python版 vs HTML版 精度比較テスト</h3>
                <p>あなたのPython実験データと比較して、HTML版の精度を検証します</p>
                
                <div style="margin: 20px 0;">
                    <h4>📋 テストクエリ選択</h4>
                    <select id="testQuerySelect" style="width: 300px; padding: 5px;">
                        <option value="Python機械学習">Python機械学習</option>
                        <option value="React開発！">React開発！</option>
                        <option value="データベース最適化">データベース最適化</option>
                        <option value="AWS構築">AWS構築</option>
                        <option value="カスタム">カスタムクエリ</option>
                    </select>
                    <input type="text" id="customQuery" placeholder="カスタムクエリを入力" style="width: 200px; margin-left: 10px; display: none;">
                </div>
                
                <button onclick="runPrecisionTest()" class="algorithm-btn">🚀 精度比較開始</button>
                <button onclick="runFullBenchmark()" class="demo-btn">📊 全クエリベンチマーク</button>
            </div>
            
            <div id="precisionResults"></div>
        </div>
    </div>

    <script>
        let documentData = [];
        let lastSearchResults = {};
        
        // ========== 高精度化のためのストップワード ==========
        const STOPWORDS_JP = new Set([
            'の', 'に', 'は', 'を', 'が', 'で', 'て', 'と', 'だ', 'である', 'です', 'ます',
            'から', 'まで', 'より', 'など', 'また', 'ただし', 'しかし', 'そして', 'それ'
        ]);
        
        const STOPWORDS_EN = new Set([
            'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by',
            'is', 'are', 'was', 'were', 'be', 'been', 'have', 'has', 'had', 'do', 'does', 'did'
        ]);
        
        // ========== タブ切り替え ==========
        function switchTab(tabName) {
            document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
            event.target.classList.add('active');
            
            document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
            document.getElementById(tabName + '-tab').classList.add('active');
        }
        
        // ========== 前処理関数（改良版） ==========
        
        function preprocessTextAdvanced(text) {
            // より精密な前処理
            return text.replace(/[^a-zA-Z0-9ぁ-んァ-ヶヷ-ヺー一-龥、。！？\s.,!?]/g, '')
                      .replace(/\s+/g, ' ')
                      .trim();
        }
        
        // ========== 高精度トークン化関数 ==========
        
        function getTokensAdvanced(text, options = {}) {
            const {
                useStopwords = true,
                katakanaWeight = 1.5,
                englishWeight = 1.2
            } = options;
            
            const clean = preprocessTextAdvanced(text);
            const tokens = [];
            
            // 英単語（ストップワード除去オプション）
            const englishWords = clean.match(/\b[a-zA-Z]{2,}\b/g) || [];
            englishWords.forEach(word => {
                const lower = word.toLowerCase();
                if (!useStopwords || !STOPWORDS_EN.has(lower)) {
                    // 英語重み適用
                    for (let i = 0; i < Math.round(englishWeight); i++) {
                        tokens.push(lower);
                    }
                }
            });
            
            // 数字（年号、バージョン等重要）
            const numbers = clean.match(/\d+/g) || [];
            tokens.push(...numbers);
            
            // カタカナ語（技術用語として重要度高）
            const katakanaWords = clean.match(/[ァ-ヶー]{2,}/g) || [];
            katakanaWords.forEach(word => {
                // カタカナ重み適用
                for (let i = 0; i < Math.round(katakanaWeight); i++) {
                    tokens.push(word);
                }
            });
            
            // 漢字列（意味のある単語として抽出）
            const kanjiSequences = clean.match(/[一-龥]{1,}/g) || [];
            kanjiSequences.forEach(seq => {
                if (!useStopwords || !STOPWORDS_JP.has(seq)) {
                    tokens.push(seq);
                }
            });
            
            // 日本語N-gram（2-4文字、重複制御）
            const japanese = clean.replace(/[a-zA-Z0-9\s.,!?、。！？]/g, '');
            for (let n = 2; n <= 4; n++) {
                for (let i = 0; i <= japanese.length - n; i++) {
                    const ngram = japanese.substring(i, i + n);
                    if (!useStopwords || !STOPWORDS_JP.has(ngram)) {
                        tokens.push(ngram);
                    }
                }
            }
            
            return tokens;
        }
        
        function getTokensUltra(text) {
            // 最高精度版
            const options = {
                useStopwords: document.getElementById('useStopwords').checked,
                katakanaWeight: parseFloat(document.getElementById('katakanaWeight').value),
                englishWeight: parseFloat(document.getElementById('englishWeight').value)
            };
            
            return getTokensAdvanced(text, options);
        }
        
        // ========== 高精度TF-IDF計算 ==========
        
        function computeTFIDFAdvanced(documents, query, tokenizerFunction) {
            const startTime = performance.now();
            
            const useL2Norm = document.getElementById('useL2Norm')?.checked ?? true;
            const useImprovedIDF = document.getElementById('useImprovedIDF')?.checked ?? true;
            
            // 全文書とクエリのトークン化
            const allTexts = [...documents, query];
            const allTokens = allTexts.map(text => tokenizerFunction(text));
            
            // 語彙構築
            const vocabulary = [...new Set(allTokens.flat())];
            const vocabSize = vocabulary.length;
            
            // TF計算（より精密）
            const tfidfVectors = allTokens.map(tokens => {
                const tf = {};
                const totalTokens = tokens.length;
                
                tokens.forEach(token => {
                    tf[token] = (tf[token] || 0) + 1;
                });
                
                // TF正規化（log正規化オプション）
                Object.keys(tf).forEach(token => {
                    tf[token] = useImprovedIDF ? 
                        Math.log(1 + tf[token]) : // Log TF
                        tf[token] / totalTokens;   // Raw TF
                });
                
                return tf;
            });
            
            // IDF計算（改良版）
            const idf = {};
            const docCount = allTexts.length;
            
            vocabulary.forEach(token => {
                const df = allTokens.filter(tokens => tokens.includes(token)).length;
                
                if (useImprovedIDF) {
                    // Smooth IDF: log((N + 1) / (df + 1)) + 1
                    idf[token] = Math.log((docCount + 1) / (df + 1)) + 1;
                } else {
                    // Standard IDF
                    idf[token] = Math.log(docCount / df);
                }
            });
            
            // TF-IDFベクトル作成
            const vectors = tfidfVectors.map(tf => {
                const vector = vocabulary.map(token => (tf[token] || 0) * idf[token]);
                
                // L2正規化（sklearn相当）
                if (useL2Norm) {
                    const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
                    if (magnitude > 0) {
                        return vector.map(val => val / magnitude);
                    }
                }
                
                return vector;
            });
            
            const preprocessingTime = performance.now() - startTime;
            
            return {
                vectors: vectors,
                vocabulary: vocabulary,
                vocabSize: vocabSize,
                preprocessingTime: preprocessingTime,
                queryVector: vectors[vectors.length - 1],
                docVectors: vectors.slice(0, -1)
            };
        }
        
        // ========== 高精度コサイン類似度 ==========
        
        function cosineSimilarityAdvanced(vec1, vec2) {
            // 既にL2正規化済みの場合は内積のみ
            return vec1.reduce((sum, a, i) => sum + a * vec2[i], 0);
        }
        
        // ========== 従来の関数（後方互換性） ==========
        
        function preprocessTextHTML(text) {
            return text.replace(/[^\w\sぁ-んァ-ヶヷ-ヺー一-龥]/g, '').replace(/\s+/g, ' ').trim();
        }
        
        function getTokensHTML(text) {
            const clean = preprocessTextHTML(text);
            const tokens = [];
            
            const englishWords = clean.match(/\b[a-zA-Z]+\b/g) || [];
            tokens.push(...englishWords.map(w => w.toLowerCase()));
            
            const japanese = clean.replace(/[a-zA-Z0-9\s]/g, '');
            for (let i = 0; i < japanese.length - 1; i++) {
                tokens.push(japanese.substring(i, i + 2));
            }
            
            return [...new Set(tokens)];
        }
        
        function getTokensTFIDFBasic(text) {
            const clean = preprocessTextHTML(text);
            const tokens = [];
            
            const englishWords = clean.match(/\b[a-zA-Z]+\b/g) || [];
            tokens.push(...englishWords.map(w => w.toLowerCase()));
            
            const japanese = clean.replace(/[a-zA-Z0-9\s]/g, '');
            for (let n = 2; n <= 3; n++) {
                for (let i = 0; i <= japanese.length - n; i++) {
                    tokens.push(japanese.substring(i, i + n));
                }
            }
            
            return tokens;
        }
        
        function computeTFIDF(documents, query, tokenizerFunction) {
            const startTime = performance.now();
            
            const allTexts = [...documents, query];
            const allTokens = allTexts.map(text => tokenizerFunction(text));
            const vocabulary = [...new Set(allTokens.flat())];
            
            const tfidfVectors = allTokens.map(tokens => {
                const tf = {};
                tokens.forEach(token => {
                    tf[token] = (tf[token] || 0) + 1;
                });
                
                const totalTokens = tokens.length;
                Object.keys(tf).forEach(token => {
                    tf[token] = tf[token] / totalTokens;
                });
                
                return tf;
            });
            
            const idf = {};
            vocabulary.forEach(token => {
                const docCount = allTokens.filter(tokens => tokens.includes(token)).length;
                idf[token] = Math.log(allTexts.length / docCount);
            });
            
            const vectors = tfidfVectors.map(tf => {
                return vocabulary.map(token => (tf[token] || 0) * idf[token]);
            });
            
            return {
                vectors: vectors,
                vocabulary: vocabulary,
                vocabSize: vocabulary.length,
                preprocessingTime: performance.now() - startTime,
                queryVector: vectors[vectors.length - 1],
                docVectors: vectors.slice(0, -1)
            };
        }
        
        function cosineSimilarity(vec1, vec2) {
            const dotProduct = vec1.reduce((sum, a, i) => sum + a * vec2[i], 0);
            const magnitude1 = Math.sqrt(vec1.reduce((sum, a) => sum + a * a, 0));
            const magnitude2 = Math.sqrt(vec2.reduce((sum, a) => sum + a * a, 0));
            
            if (magnitude1 === 0 || magnitude2 === 0) return 0;
            return dotProduct / (magnitude1 * magnitude2);
        }
        
        function jaccardSimilarity(tokens1, tokens2) {
            const set1 = new Set(tokens1);
            const set2 = new Set(tokens2);
            const intersection = new Set([...set1].filter(x => set2.has(x)));
            const union = new Set([...set1, ...set2]);
            return union.size === 0 ? 0 : intersection.size / union.size;
        }
        
        // ========== 検索関数 ==========
        
        function searchJaccard(documents, query, topN) {
            const startTime = performance.now();
            
            const queryTokens = getTokensHTML(query);
            const results = [];
            
            documents.forEach((doc, index) => {
                const docTokens = getTokensHTML(doc.text);
                const similarity = jaccardSimilarity(queryTokens, docTokens);
                
                if (similarity > 0) {
                    results.push({
                        index: index + 1,
                        similarity: similarity,
                        text: doc.text
                    });
                }
            });
            
            results.sort((a, b) => b.similarity - a.similarity);
            
            return {
                results: results.slice(0, topN),
                executionTime: performance.now() - startTime,
                algorithm: 'Jaccard係数',
                vocabSize: new Set(queryTokens).size
            };
        }
        
        function searchTFIDF(documents, query, topN, tokenizerFunction, algorithmName, useAdvanced = false) {
            const startTime = performance.now();
            
            const docTexts = documents.map(doc => doc.text);
            const tfidfData = useAdvanced ? 
                computeTFIDFAdvanced(docTexts, query, tokenizerFunction) :
                computeTFIDF(docTexts, query, tokenizerFunction);
            
            const results = [];
            const similarityFunc = useAdvanced ? cosineSimilarityAdvanced : cosineSimilarity;
            
            tfidfData.docVectors.forEach((docVector, index) => {
                const similarity = similarityFunc(tfidfData.queryVector, docVector);
                
                if (similarity > 0) {
                    results.push({
                        index: index + 1,
                        similarity: similarity,
                        text: documents[index].text
                    });
                }
            });
            
            results.sort((a, b) => b.similarity - a.similarity);
            
            return {
                results: results.slice(0, topN),
                executionTime: performance.now() - startTime,
                algorithm: algorithmName,
                vocabSize: tfidfData.vocabSize,
                preprocessingTime: tfidfData.preprocessingTime
            };
        }
        
        // ========== メイン検索関数 ==========
        
        function search() {
            const query = document.getElementById('queryInput').value.trim();
            const topN = parseInt(document.getElementById('topN').value);
            const algorithm = document.querySelector('input[name="algorithm"]:checked').value;
            
            if (!query) {
                alert("検索語を入力してください");
                return;
            }
            
            if (documentData.length === 0) {
                alert("ファイルまたはデモデータを読み込んでください");
                return;
            }
            
            let searchResult;
            
            switch (algorithm) {
                case 'jaccard':
                    searchResult = searchJaccard(documentData, query, topN);
                    break;
                case 'tfidf_basic':
                    searchResult = searchTFIDF(documentData, query, topN, getTokensTFIDFBasic, 'TF-IDF（基本）', false);
                    break;
                case 'tfidf_advanced':
                    searchResult = searchTFIDF(documentData, query, topN, getTokensAdvanced, 'TF-IDF（高精度）', true);
                    break;
                case 'tfidf_ultra':
                    searchResult = searchTFIDF(documentData, query, topN, getTokensUltra, 'TF-IDF（Ultra）', true);
                    break;
            }
            
            lastSearchResults[algorithm] = searchResult;
            displayResults(searchResult);
        }
        
        function displayResults(searchResult) {
            const resultsDiv = document.getElementById('results');
            
            if (searchResult.results.length === 0) {
                resultsDiv.innerHTML = '<p>検索結果が見つかりませんでした。</p>';
                return;
            }
            
            let html = `
                <h3>🎯 検索結果 - ${searchResult.algorithm}</h3>
                <div class="performance-info">
                    実行時間: ${searchResult.executionTime.toFixed(3)}ms | 
                    語彙サイズ: ${searchResult.vocabSize} | 
                    ヒット件数: ${searchResult.results.length}
                    ${searchResult.preprocessingTime ? `| 前処理時間: ${searchResult.preprocessingTime.toFixed(3)}ms` : ''}
                </div>
            `;
            
            searchResult.results.forEach((result, index) => {
                html += `
                    <div class="result-item">
                        <div><strong>[${index + 1}件目]</strong> <span class="similarity">類似度: ${result.similarity.toFixed(4)}</span></div>
                        <div>${result.text}</div>
                    </div>
                `;
            });
            
            resultsDiv.innerHTML = html;
        }
        
        // ========== 比較機能 ==========
        
        function runComparison() {
            const query = document.getElementById('comparisonQuery').value.trim();
            
            if (!query) {
                alert("比較用検索語を入力してください");
                return;
            }
            
            if (documentData.length === 0) {
                alert("ファイルまたはデモデータを読み込んでください");
                return;
            }
            
            const algorithms = [
                { key: 'jaccard', name: 'Jaccard係数', func: () => searchJaccard(documentData, query, 5) },
                { key: 'tfidf_basic', name: 'TF-IDF（基本）', func: () => searchTFIDF(documentData, query, 5, getTokensTFIDFBasic, 'TF-IDF（基本）', false) },
                { key: 'tfidf_advanced', name: 'TF-IDF（高精度）', func: () => searchTFIDF(documentData, query, 5, getTokensAdvanced, 'TF-IDF（高精度）', true) },
                { key: 'tfidf_ultra', name: 'TF-IDF（Ultra）', func: () => searchTFIDF(documentData, query, 5, getTokensUltra, 'TF-IDF（Ultra）', true) }
            ];
            
            const results = {};
            algorithms.forEach(alg => {
                results[alg.key] = alg.func();
            });
            
            displayComparison(query, results);
        }
        
        function displayComparison(query, results) {
            const resultsDiv = document.getElementById('comparisonResults');
            
            let html = `<h3>📊 アルゴリズム比較結果 - クエリ: "${query}"</h3>`;
            
            html += `
                <table class="comparison-table">
                    <tr>
                        <th>アルゴリズム</th>
                        <th>実行時間(ms)</th>
                        <th>語彙サイズ</th>
                        <th>ヒット件数</th>
                        <th>上位類似度</th>
                    </tr>
            `;
            
            Object.entries(results).forEach(([key, result]) => {
                const topSimilarity = result.results.length > 0 ? result.results[0].similarity.toFixed(4) : '0.0000';
                html += `
                    <tr>
                        <td>${result.algorithm}</td>
                        <td>${result.executionTime.toFixed(3)}</td>
                        <td>${result.vocabSize}</td>
                        <td>${result.results.length}</td>
                        <td>${topSimilarity}</td>
                    </tr>
                `;
            });
            
            html += '</table>';
            
            Object.entries(results).forEach(([key, result]) => {
                html += `
                    <div class="algorithm-info">
                        <h4>${result.algorithm}</h4>
                        <p><strong>上位3件の結果:</strong></p>
                `;
                
                result.results.slice(0, 3).forEach((item, index) => {
                    html += `<p>${index + 1}. 類似度: ${item.similarity.toFixed(4)} | ${item.text.substring(0, 60)}...</p>`;
                });
                
                html += '</div>';
            });
            
            resultsDiv.innerHTML = html;
        }
        
        // ========== 詳細分析機能 ==========
        
        function runAnalysis() {
            const text = document.getElementById('analysisText').value.trim();
            
            if (!text) {
                alert("分析するテキストを入力してください");
                return;
            }
            
            const methods = [
                { name: 'HTML版（軽量）', preprocessor: preprocessTextHTML, tokenizer: getTokensHTML },
                { name: 'TF-IDF基本', preprocessor: preprocessTextHTML, tokenizer: getTokensTFIDFBasic },
                { name: 'TF-IDF高精度', preprocessor: preprocessTextAdvanced, tokenizer: getTokensAdvanced },
                { name: 'TF-IDF Ultra', preprocessor: preprocessTextAdvanced, tokenizer: getTokensUltra }
            ];
            
            let html = `<h3>🔍 前処理分析結果</h3>`;
            html += `<p><strong>元テキスト:</strong> ${text}</p>`;
            
            methods.forEach(method => {
                const preprocessed = method.preprocessor(text);
                const tokens = method.tokenizer(text);
                const uniqueTokens = [...new Set(tokens)];
                
                html += `
                    <div class="vocab-analysis">
                        <h4>${method.name}</h4>
                        <p><strong>前処理後:</strong> ${preprocessed}</p>
                        <p><strong>トークン数:</strong> ${tokens.length} (ユニーク: ${uniqueTokens.length})</p>
                        <p><strong>トークン例:</strong> ${tokens.slice(0, 15).join(', ')}...</p>
                    </div>
                `;
            });
            
            document.getElementById('analysisResults').innerHTML = html;
        }
        
        // ========== データ読み込み ==========
        
        function loadDemo() {
            documentData = [
                { text: "Python機械学習入門講座。scikit-learnライブラリを使用してデータ分析を学習します。" },
                { text: "Web開発フレームワーク比較！ReactとVue.jsの特徴について詳しく解説。" },
                { text: "データベース最適化手法の実践。SQLクエリのパフォーマンス改善テクニック。" },
                { text: "クラウドインフラ構築ガイド。AWSとAzureを活用したスケーラブルなシステム設計です。" },
                { text: "AI画像認識技術の基礎。TensorFlowとPyTorchを使った深層学習の実装方法。" },
                { text: "JavaScript開発環境の構築。Node.jsとnpmを使ったモダンな開発ワークフロー。" },
                { text: "データ可視化とグラフ作成。Pythonのmatplotlibとseabornライブラリの活用術。" },
                { text: "セキュリティ対策の実装。Webアプリケーションの脆弱性対策と暗号化技術について。" }
            ];
            alert(`デモデータを読み込みました: ${documentData.length}件`);
        }
        
        // ========== 精度比較機能 ==========
        
        // Python実験データ（あなたの実験結果を再現）
        const PYTHON_RESULTS = {
            "Python機械学習": {
                "HTML版（文字除去+バイグラム）": {
                    executionTime: 3.0,
                    vocabSize: 183,
                    hitCount: 2,
                    topResults: [
                        { similarity: 0.3590, text: "Python機械学習入門講座。scikit-learnライブラリを使用してデータ..." },
                        { similarity: 0.0882, text: "AI画像認識技術の基礎。TensorFlowとPyTorchを使った深層学習の実..." }
                    ]
                },
                "Flask版（形態素解析）": {
                    executionTime: 17.0,
                    vocabSize: 66,
                    hitCount: 3,
                    topResults: [
                        { similarity: 0.5553, text: "Python機械学習入門講座。scikit-learnライブラリを使用してデータ..." },
                        { similarity: 0.1454, text: "データ可視化とグラフ作成。Pythonのmatplotlibとseabornライ..." },
                        { similarity: 0.1233, text: "AI画像認識技術の基礎。TensorFlowとPyTorchを使った深層学習の実..." }
                    ]
                }
            },
            "React開発！": {
                "HTML版（文字除去+バイグラム）": {
                    executionTime: 3.0,
                    vocabSize: 183,
                    hitCount: 2,
                    topResults: [
                        { similarity: 0.3220, text: "JavaScript開発環境の構築。Node.jsとnpmを使ったモダンな開発ワ..." },
                        { similarity: 0.1598, text: "Web開発フレームワーク比較！ReactとVue.jsの特徴について詳しく解説。..." }
                    ]
                },
                "Flask版（形態素解析）": {
                    executionTime: 10.0,
                    vocabSize: 66,
                    hitCount: 2,
                    topResults: [
                        { similarity: 0.3652, text: "Web開発フレームワーク比較！ReactとVue.jsの特徴について詳しく解説。..." },
                        { similarity: 0.2904, text: "JavaScript開発環境の構築。Node.jsとnpmを使ったモダンな開発ワ..." }
                    ]
                }
            },
            "データベース最適化": {
                "HTML版（文字除去+バイグラム）": {
                    executionTime: 3.0,
                    vocabSize: 183,
                    hitCount: 3,
                    topResults: [
                        { similarity: 0.4326, text: "データベース最適化手法の実践。SQLクエリのパフォーマンス改善テクニック。..." },
                        { similarity: 0.0851, text: "データ可視化とグラフ作成。Pythonのmatplotlibとseabornライ..." },
                        { similarity: 0.0732, text: "Python機械学習入門講座。scikit-learnライブラリを使用してデータ..." }
                    ]
                },
                "Flask版（形態素解析）": {
                    executionTime: 9.0,
                    vocabSize: 66,
                    hitCount: 1,
                    topResults: [
                        { similarity: 0.4115, text: "データベース最適化手法の実践。SQLクエリのパフォーマンス改善テクニック。..." }
                    ]
                }
            },
            "AWS構築": {
                "HTML版（文字除去+バイグラム）": {
                    executionTime: 2.0,
                    vocabSize: 183,
                    hitCount: 2,
                    topResults: [
                        { similarity: 0.1590, text: "JavaScript開発環境の構築。Node.jsとnpmを使ったモダンな開発ワ..." },
                        { similarity: 0.1314, text: "クラウドインフラ構築ガイド。AWSとAzureを活用したスケーラブルなシステム設..." }
                    ]
                },
                "Flask版（形態素解析）": {
                    executionTime: 9.0,
                    vocabSize: 66,
                    hitCount: 2,
                    topResults: [
                        { similarity: 0.3965, text: "クラウドインフラ構築ガイド。AWSとAzureを活用したスケーラブルなシステム設..." },
                        { similarity: 0.1419, text: "JavaScript開発環境の構築。Node.jsとnpmを使ったモダンな開発ワ..." }
                    ]
                }
            }
        };
        
        function runPrecisionTest() {
            const querySelect = document.getElementById('testQuerySelect').value;
            const customQuery = document.getElementById('customQuery').value.trim();
            const query = querySelect === 'カスタム' ? customQuery : querySelect;
            
            if (!query) {
                alert("クエリを選択または入力してください");
                return;
            }
            
            if (documentData.length === 0) {
                loadDemo();
            }
            
            // HTML版の結果を取得
            const htmlResults = {
                'HTML版（軽量）': searchJaccard(documentData, query, 5),
                'TF-IDF（基本）': searchTFIDF(documentData, query, 5, getTokensTFIDFBasic, 'TF-IDF（基本）', false),
                'TF-IDF（高精度）': searchTFIDF(documentData, query, 5, getTokensAdvanced, 'TF-IDF（高精度）', true),
                'TF-IDF（Ultra）': searchTFIDF(documentData, query, 5, getTokensUltra, 'TF-IDF（Ultra）', true)
            };
            
            displayPrecisionComparison(query, htmlResults);
        }
        
        function runFullBenchmark() {
            if (documentData.length === 0) {
                loadDemo();
            }
            
            const queries = ["Python機械学習", "React開発！", "データベース最適化", "AWS構築"];
            const allResults = {};
            
            queries.forEach(query => {
                allResults[query] = {
                    'HTML版（軽量）': searchJaccard(documentData, query, 5),
                    'TF-IDF（基本）': searchTFIDF(documentData, query, 5, getTokensTFIDFBasic, 'TF-IDF（基本）', false),
                    'TF-IDF（高精度）': searchTFIDF(documentData, query, 5, getTokensAdvanced, 'TF-IDF（高精度）', true),
                    'TF-IDF（Ultra）': searchTFIDF(documentData, query, 5, getTokensUltra, 'TF-IDF（Ultra）', true)
                };
            });
            
            displayFullBenchmark(allResults);
        }
        
        function displayPrecisionComparison(query, htmlResults) {
            const resultsDiv = document.getElementById('precisionResults');
            const pythonData = PYTHON_RESULTS[query];
            
            let html = `<h3>🎯 精度比較結果 - クエリ: "${query}"</h3>`;
            
            if (pythonData) {
                html += `
                    <div class="precision-info">
                        <h4>📊 Python版 vs HTML版 比較表</h4>
                        <table class="comparison-table">
                            <tr>
                                <th>実装</th>
                                <th>実行時間(ms)</th>
                                <th>語彙サイズ</th>
                                <th>ヒット件数</th>
                                <th>上位類似度</th>
                                <th>精度評価</th>
                            </tr>
                `;
                
                // Python結果
                Object.entries(pythonData).forEach(([method, data]) => {
                    html += `
                        <tr style="background: #fff3cd;">
                            <td><strong>🐍 ${method}</strong></td>
                            <td>${data.executionTime}</td>
                            <td>${data.vocabSize}</td>
                            <td>${data.hitCount}</td>
                            <td>${data.topResults[0]?.similarity.toFixed(4) || '0.0000'}</td>
                            <td>Python基準</td>
                        </tr>
                    `;
                });
                
                // HTML結果
                Object.entries(htmlResults).forEach(([method, result]) => {
                    const topSim = result.results[0]?.similarity || 0;
                    const pythonBaseline = pythonData["HTML版（文字除去+バイグラム）"]?.topResults[0]?.similarity || 0;
                    const improvement = pythonBaseline > 0 ? ((topSim - pythonBaseline) / pythonBaseline * 100) : 0;
                    const status = improvement > 10 ? "🚀 大幅改善" : improvement > 0 ? "📈 改善" : improvement > -10 ? "≈ 同等" : "📉 要改善";
                    
                    html += `
                        <tr style="background: #d4edda;">
                            <td><strong>🌐 HTML ${method}</strong></td>
                            <td>${result.executionTime.toFixed(1)}</td>
                            <td>${result.vocabSize}</td>
                            <td>${result.results.length}</td>
                            <td>${topSim.toFixed(4)}</td>
                            <td>${status} (${improvement.toFixed(1)}%)</td>
                        </tr>
                    `;
                });
                
                html += '</table>';
                
                // 詳細比較
                html += `<h4>🔍 検索結果の質的比較</h4>`;
                
                // Python結果
                if (pythonData["Flask版（形態素解析）"]) {
                    html += `
                        <div class="algorithm-info">
                            <h5>🐍 Python版（形態素解析・最高精度）</h5>
                    `;
                    pythonData["Flask版（形態素解析）"].topResults.slice(0, 3).forEach((item, index) => {
                        html += `<p>${index + 1}. 類似度: ${item.similarity.toFixed(4)} | ${item.text.substring(0, 60)}...</p>`;
                    });
                    html += '</div>';
                }
                
                // HTML最高精度版
                const bestHTML = htmlResults['TF-IDF（Ultra）'] || htmlResults['TF-IDF（高精度）'];
                html += `
                    <div class="algorithm-info">
                        <h5>🌐 HTML版（Ultra・sklearn不要）</h5>
                `;
                bestHTML.results.slice(0, 3).forEach((item, index) => {
                    html += `<p>${index + 1}. 類似度: ${item.similarity.toFixed(4)} | ${item.text.substring(0, 60)}...</p>`;
                });
                html += '</div>';
                
            } else {
                html += `
                    <div class="algorithm-info">
                        <h4>🌐 HTML版結果（カスタムクエリ）</h4>
                        <table class="comparison-table">
                            <tr>
                                <th>手法</th>
                                <th>実行時間(ms)</th>
                                <th>語彙サイズ</th>
                                <th>ヒット件数</th>
                                <th>上位類似度</th>
                            </tr>
                `;
                
                Object.entries(htmlResults).forEach(([method, result]) => {
                    const topSim = result.results[0]?.similarity || 0;
                    html += `
                        <tr>
                            <td>${method}</td>
                            <td>${result.executionTime.toFixed(1)}</td>
                            <td>${result.vocabSize}</td>
                            <td>${result.results.length}</td>
                            <td>${topSim.toFixed(4)}</td>
                        </tr>
                    `;
                });
                
                html += '</table></div>';
            }
            
            resultsDiv.innerHTML = html;
        }
        
        function displayFullBenchmark(allResults) {
            const resultsDiv = document.getElementById('precisionResults');
            
            let html = `<h3>📊 全クエリベンチマーク結果</h3>`;
            
            // 平均性能計算
            const avgPerformance = {};
            const methods = Object.keys(allResults[Object.keys(allResults)[0]]);
            
            methods.forEach(method => {
                let totalTime = 0, totalVocab = 0, totalHits = 0, totalSim = 0, count = 0;
                
                Object.values(allResults).forEach(queryResults => {
                    const result = queryResults[method];
                    totalTime += result.executionTime;
                    totalVocab += result.vocabSize;
                    totalHits += result.results.length;
                    totalSim += result.results[0]?.similarity || 0;
                    count++;
                });
                
                avgPerformance[method] = {
                    avgTime: totalTime / count,
                    avgVocab: totalVocab / count,
                    avgHits: totalHits / count,
                    avgSim: totalSim / count
                };
            });
            
            html += `
                <div class="precision-info">
                    <h4>📈 平均性能サマリー</h4>
                    <table class="comparison-table">
                        <tr>
                            <th>HTML実装</th>
                            <th>平均実行時間</th>
                            <th>平均語彙サイズ</th>
                            <th>平均ヒット数</th>
                            <th>平均類似度</th>
                            <th>総合評価</th>
                        </tr>
            `;
            
            Object.entries(avgPerformance).forEach(([method, perf]) => {
                const score = (perf.avgSim * 40) + (Math.min(perf.avgHits / 3, 1) * 30) + (Math.max(1 - perf.avgTime / 10, 0) * 30);
                const grade = score > 80 ? "🏆 A+" : score > 70 ? "🥇 A" : score > 60 ? "🥈 B" : score > 50 ? "🥉 C" : "📝 D";
                
                html += `
                    <tr>
                        <td><strong>${method}</strong></td>
                        <td>${perf.avgTime.toFixed(1)}ms</td>
                        <td>${Math.round(perf.avgVocab)}</td>
                        <td>${perf.avgHits.toFixed(1)}</td>
                        <td>${perf.avgSim.toFixed(4)}</td>
                        <td>${grade} (${score.toFixed(0)}点)</td>
                    </tr>
                `;
            });
            
            html += '</table></div>';
            
            // クエリ別詳細
            html += `<h4>📋 クエリ別詳細結果</h4>`;
            Object.entries(allResults).forEach(([query, results]) => {
                html += `
                    <div class="vocab-analysis">
                        <h5>🔍 "${query}"</h5>
                        <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 10px;">
                `;
                
                Object.entries(results).forEach(([method, result]) => {
                    html += `
                        <div style="background: white; padding: 10px; border-radius: 5px; border: 1px solid #ddd;">
                            <strong>${method}</strong><br>
                            類似度: ${(result.results[0]?.similarity || 0).toFixed(4)}<br>
                            時間: ${result.executionTime.toFixed(1)}ms<br>
                            件数: ${result.results.length}
                        </div>
                    `;
                });
                
                html += '</div></div>';
            });
            
            resultsDiv.innerHTML = html;
        }
        
        // ファイル読み込み
        document.getElementById('fileInput').addEventListener('change', function(e) {
            const file = e.target.files[0];
            if (!file) return;
            
            const reader = new FileReader();
            reader.onload = function(e) {
                const content = e.target.result;
                const lines = content.split('\n').filter(line => line.trim());
                
                documentData = lines.map(line => ({
                    text: line
                }));
                
                alert(`ファイル読み込み完了: ${documentData.length}行`);
            };
            reader.readAsText(file, 'UTF-8');
        });
        
        // Enterキーで検索
        document.getElementById('queryInput').addEventListener('keypress', function(e) {
            if (e.key === 'Enter') search();
        });
        
        document.getElementById('comparisonQuery').addEventListener('keypress', function(e) {
            if (e.key === 'Enter') runComparison();
        });
        
        // 精度比較用のイベントリスナー
        document.getElementById('testQuerySelect').addEventListener('change', function(e) {
            const customInput = document.getElementById('customQuery');
            if (e.target.value === 'カスタム') {
                customInput.style.display = 'inline';
                customInput.focus();
            } else {
                customInput.style.display = 'none';
            }
        });
    </script>
</body>
</html>
```