Elasticsearchより軽量なサーチエンジンを二つためしてみました。(以下ChatGPTより)
| 項目 | Typesense | MeiliSearch |
|---|---|---|
| コンセプト | シンプルで高速、予測変換的な検索 | Google 風の自然な検索(意味的な曖昧検索) |
| 検索の方向性 | 厳密性・制御重視 | 曖昧検索・自然言語検索重視 |
| 型サポート | 厳密なスキーマ(型定義が必要) | スキーマレス(柔軟) |
| 結果の一致度 | strict(スコア計算が predictable) | loose(fuzzy matching が強い) |
| セットアップ | かなり簡単(docker 1 コマンド) | 同じく簡単 |
| 運用(クラスタ) | 楽(metadata 少ない) | 少し重め(indexing が cost 高い) |
| スキーマ | 必須 | 不要 |
| 用途 | Typesense | MeiliSearch |
|---|---|---|
| RAG の事前フィルタ(構造化データ) | ◎ | ○ |
| 質問文の全文検索 | ○ | ◎ |
| 資料 / 文書検索 | ○ | ◎(自然文に強い) |
Typesense
サーバ
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
import Typesense from 'typesense'; // Typesenseクライアントの初期化 const client = new Typesense.Client({ nodes: [{ host: 'localhost', port: '8108', protocol: 'http' }], apiKey: 'xyz', connectionTimeoutSeconds: 2 }); // サンプルデータ: 日本の書籍 const books = [ { id: '1', title: '吾輩は猫である', author: '夏目漱石', year: 1905, genre: '小説', description: '猫の視点から人間社会を風刺的に描いた作品' }, { id: '2', title: '雪国', author: '川端康成', year: 1935, genre: '小説', description: '温泉地を舞台にした美しい恋愛小説' }, { id: '3', title: '人間失格', author: '太宰治', year: 1948, genre: '小説', description: '人間性の喪失を描いた私小説' }, { id: '4', title: 'ノルウェイの森', author: '村上春樹', year: 1987, genre: '小説', description: '1960年代を舞台にした青春小説' }, { id: '5', title: '羅生門', author: '芥川龍之介', year: 1915, genre: '短編小説', description: '平安時代を舞台にした人間のエゴイズムを描いた作品' }, { id: '6', title: 'こころ', author: '夏目漱石', year: 1914, genre: '小説', description: '明治時代の知識人の苦悩を描いた作品' }, { id: '7', title: '火花', author: '又吉直樹', year: 2015, genre: '小説', description: '漫才師の師弟関係を描いた青春小説' }, { id: '8', title: '蹴りたい背中', author: '綿矢りさ', year: 2003, genre: '小説', description: '孤独な高校生の心情を描いた作品' } ]; // コレクションスキーマの定義 const booksSchema = { name: 'books', fields: [ { name: 'title', type: 'string' }, { name: 'author', type: 'string', facet: true }, { name: 'year', type: 'int32', facet: true }, { name: 'genre', type: 'string', facet: true }, { name: 'description', type: 'string' } ], default_sorting_field: 'year' }; async function setupTypesense() { try { console.log('🚀 Typesenseデモを開始します...\n'); // 既存のコレクションを削除(存在する場合) try { await client.collections('books').delete(); console.log('✓ 既存のコレクションを削除しました'); } catch (error) { // コレクションが存在しない場合は無視 } // コレクションを作成 console.log('📚 コレクションを作成中...'); await client.collections().create(booksSchema); console.log('✓ コレクション "books" を作成しました\n'); // ドキュメントをインデックス console.log('📝 書籍データをインデックス中...'); for (const book of books) { await client.collections('books').documents().create(book); console.log(` ✓ "${book.title}" by ${book.author}`); } console.log(`\n✓ ${books.length}件の書籍をインデックスしました\n`); // 検索例1: タイトル検索 console.log('🔍 検索例1: "猫" で検索'); const searchResult1 = await client.collections('books') .documents() .search({ q: '猫', query_by: 'title,description' }); console.log(`見つかった件数: ${searchResult1.found}`); searchResult1.hits.forEach(hit => { console.log(` - ${hit.document.title} (${hit.document.author}, ${hit.document.year})`); }); console.log(''); // 検索例2: 著者検索 console.log('🔍 検索例2: "夏目漱石" で検索'); const searchResult2 = await client.collections('books') .documents() .search({ q: '夏目漱石', query_by: 'author' }); console.log(`見つかった件数: ${searchResult2.found}`); searchResult2.hits.forEach(hit => { console.log(` - ${hit.document.title} (${hit.document.year})`); }); console.log(''); // 検索例3: ファセット検索(年代別) console.log('🔍 検索例3: "小説" でフィルタ + ファセット'); const searchResult3 = await client.collections('books') .documents() .search({ q: '*', query_by: 'title', filter_by: 'genre:小説', facet_by: 'author,year' }); console.log(`見つかった件数: ${searchResult3.found}`); console.log('著者別:'); searchResult3.facet_counts[0].counts.forEach(facet => { console.log(` - ${facet.value}: ${facet.count}件`); }); console.log(''); // 検索例4: 曖昧検索(typo tolerance) console.log('🔍 検索例4: "そうせき" で曖昧検索(typo tolerance)'); const searchResult4 = await client.collections('books') .documents() .search({ q: 'そうせき', query_by: 'author,title', num_typos: 2 }); console.log(`見つかった件数: ${searchResult4.found}`); searchResult4.hits.forEach(hit => { console.log(` - ${hit.document.title} by ${hit.document.author}`); }); console.log(''); console.log('✅ デモが完了しました!'); console.log('\n💡 search.js を実行してインタラクティブな検索を試してください'); } catch (error) { console.error('❌ エラーが発生しました:', error.message); if (error.httpStatus === undefined) { console.error('\n💡 Typesenseサーバーが起動していることを確認してください'); console.error(' docker run -p 8108:8108 -v/tmp/data:/data typesense/typesense:27.1 \\'); console.error(' --data-dir /data --api-key=xyz --enable-cors'); } } } setupTypesense(); |
クライアント
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
import Typesense from 'typesense'; import * as readline from 'readline'; // Typesenseクライアントの初期化 const client = new Typesense.Client({ nodes: [{ host: 'localhost', port: '8108', protocol: 'http' }], apiKey: 'xyz', connectionTimeoutSeconds: 2 }); // 対話型検索インターフェース const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); async function interactiveSearch() { console.log('📚 Typesense インタラクティブ検索'); console.log('終了するには "exit" または "quit" と入力してください\n'); const askQuestion = () => { rl.question('🔍 検索キーワード: ', async (query) => { if (query.toLowerCase() === 'exit' || query.toLowerCase() === 'quit') { console.log('👋 検索を終了します'); rl.close(); return; } if (!query.trim()) { askQuestion(); return; } try { const searchResult = await client.collections('books') .documents() .search({ q: query, query_by: 'title,author,description', num_typos: 2, per_page: 10 }); console.log(`\n📊 検索結果: ${searchResult.found}件見つかりました\n`); if (searchResult.found === 0) { console.log('該当する書籍が見つかりませんでした\n'); } else { searchResult.hits.forEach((hit, index) => { const doc = hit.document; console.log(`${index + 1}. ${doc.title}`); console.log(` 著者: ${doc.author}`); console.log(` 年: ${doc.year}`); console.log(` ジャンル: ${doc.genre}`); console.log(` 概要: ${doc.description}`); if (hit.text_match) { console.log(` スコア: ${hit.text_match}`); } console.log(''); }); } askQuestion(); } catch (error) { console.error('❌ 検索エラー:', error.message); if (error.httpStatus === undefined) { console.error('\n💡 Typesenseサーバーが起動していることを確認してください'); rl.close(); } else { askQuestion(); } } }); }; // 最初の質問を開始 askQuestion(); } interactiveSearch(); |
MeiliSearch
サーバ
docker-compose.yml
|
1 2 3 4 5 6 7 8 9 10 11 12 |
services: meilisearch: image: getmeili/meilisearch:v1.10 container_name: meilisearch ports: - "7700:7700" environment: - MEILI_MASTER_KEY=masterKey123 - MEILI_ENV=development volumes: - ./meili_data:/meili_data restart: unless-stopped |
クライアント
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ MeiliSearch デモプログラム 書籍検索システムのサンプル実装 """ import meilisearch import json import time import sys # UTF-8エンコーディングの確保 if sys.stdout.encoding != 'utf-8': sys.stdout.reconfigure(encoding='utf-8') # MeiliSearchクライアントの初期化 client = meilisearch.Client('http://localhost:7700', 'masterKey123') # サンプル書籍データ books = [ { 'id': 1, 'title': '吾輩は猫である', 'author': '夏目漱石', 'genre': '小説', 'year': 1905, 'description': '猫の視点から人間社会を風刺した作品' }, { 'id': 2, 'title': '人間失格', 'author': '太宰治', 'genre': '小説', 'year': 1948, 'description': '主人公の葉蔵の手記という形式で書かれた私小説' }, { 'id': 3, 'title': '雪国', 'author': '川端康成', 'genre': '小説', 'year': 1935, 'description': 'ノーベル文学賞受賞作家による代表作' }, { 'id': 4, 'title': '羅生門', 'author': '芥川龍之介', 'genre': '短編小説', 'year': 1915, 'description': '今昔物語集を題材にした短編小説' }, { 'id': 5, 'title': 'ノルウェイの森', 'author': '村上春樹', 'genre': '小説', 'year': 1987, 'description': '1960年代後半の東京を舞台にした青春小説' }, { 'id': 6, 'title': '坊っちゃん', 'author': '夏目漱石', 'genre': '小説', 'year': 1906, 'description': '四国の中学校に赴任した教師の物語' }, { 'id': 7, 'title': '銀河鉄道の夜', 'author': '宮沢賢治', 'genre': 'ファンタジー', 'year': 1934, 'description': 'ジョバンニとカムパネルラの幻想的な旅' }, { 'id': 8, 'title': 'こころ', 'author': '夏目漱石', 'genre': '小説', 'year': 1914, 'description': '友情と恋愛、人間の心理を描いた作品' }, ] def setup_index(): """インデックスの作成とデータの投入""" print("📚 MeiliSearch デモプログラム") print("=" * 50) # インデックスの作成または取得 try: index = client.get_index('books') print(f"✓ インデックス 'books' を取得しました") except: print("✓ 新しいインデックス 'books' を作成中...") client.create_index('books', {'primaryKey': 'id'}) index = client.get_index('books') # ドキュメントの追加 print(f"✓ {len(books)}件の書籍データを追加中...") index.add_documents(books) # インデックスの更新を待つ time.sleep(2) # 検索可能な属性を設定 index.update_searchable_attributes([ 'title', 'author', 'genre', 'description' ]) # フィルタ可能な属性を設定 index.update_filterable_attributes(['genre', 'author', 'year']) # ソート可能な属性を設定 index.update_sortable_attributes(['year', 'title']) time.sleep(1) print("✓ セットアップ完了!\n") return index def display_results(results): """検索結果の表示""" hits = results['hits'] if not hits: print("検索結果が見つかりませんでした。") return print(f"検索結果: {len(hits)}件 (処理時間: {results['processingTimeMs']}ms)\n") for hit in hits: print(f" 📖 {hit['title']}") print(f" 著者: {hit['author']} | ジャンル: {hit['genre']} | 年: {hit['year']}") print(f" {hit['description']}") print() def interactive_search(index): """インタラクティブな検索""" print("\n" + "=" * 50) print("🔍 インタラクティブ検索モード") print("=" * 50) print("終了するには 'quit' または 'exit' を入力してください\n") while True: try: query = input("検索キーワード > ").strip() if query.lower() in ['quit', 'exit', 'q']: print("検索を終了します。") break if not query: continue # 文字列を正規化してUTF-8として扱う query = str(query).encode('utf-8').decode('utf-8') results = index.search(query) display_results(results) except Exception as e: print(f"❌ 検索エラー: {e}") print("別のキーワードで試してください。\n") def main(): """メイン処理""" try: # セットアップ index = setup_index() # インタラクティブ検索 interactive_search(index) except Exception as e: print(f"❌ エラーが発生しました: {e}") print("MeiliSearchサーバーが起動していることを確認してください。") print("起動コマンド: docker-compose up -d") if __name__ == '__main__': main() |
コードはClaude Codeで生成しました。特に何も指定しなかったところ、Typesenseはnode.jsで、MeiliSearchはDockerとPythonでした。
環境も含めてすべて掲載するべきとも思いましたが、最近はAIがすべてやってくれるので、こういったものが、このような形でうごかされる、ということがわかれば良いかな、と考えがかわってきました。
