現代の検索における課題
検索バーの実装は、実際に自分で使ってみるまでは簡単に思えるものです。多くの開発者は、まずSQLで単純なLIKE %query%を使うことから始めます。私自身、MySQL、PostgreSQL、MongoDBでこれを行ってきましたが、テーブルが10万行を超えたあたりで限界が来ます。パフォーマンスが急激に低下するのです。これらのデータベースは全文検索のために設計されておらず、タイポ(打ち間違い)耐性がなく、複雑なランキング処理も苦手で、大量のテキストデータを検索すると動作が極端に遅くなります。
長年、Elasticsearchが業界の標準でした。非常に強力ですが、メモリを大量に消費することでも有名です。2GBのRAMしかない小さなVPSでElasticsearchを動かそうとすれば、起動シーケンスが終わる前にクラッシュするでしょう。安定させるだけでも、Java Virtual Machine(JVM)の深い知識と膨大な設定が必要になります。
Typesenseはそれを変えます。C++で書かれたこのオープンソースエンジンは、圧倒的なスピードと開発者の使いやすさを追求して構築されています。パフォーマンスを妥協しない、Elasticsearchの「ライト」版だと考えてください。AmazonやAlgoliaで見られるような、入力中から結果が表示される「サーチ・アズ・ユー・タイプ」の体験を、エンタープライズ級のオーバーヘッドなしで提供するために特別に設計されています。
なぜTypesenseを選ぶのか?
コードに入る前に、なぜこのエンジンが注目されているのかを見ていきましょう。ディスクからデータを取得する従来のデータベースとは異なり、Typesenseは検索インデックス全体をRAM上に保持します。このアーキテクチャにより、50ミリ秒未満のレスポンスタイムが可能になります。データ消失の心配はありません。すべてディスクに永続化されるため、サーバーが再起動してもデータは安全です。
モダンなアプリにおいて、Typesenseが優れた選択肢となる理由は以下の通りです:
- 即座なタイポ耐性: 入力ミスを柔軟に処理します。ユーザーが「ipone」と入力しても、Typesenseは追加設定なしで即座に「iPhone」を見つけ出します。
- クリーンなRESTful API: 複雑なQuery DSLを覚える必要はありません。JSON APIが使えれば、Typesenseを使いこなせます。
- 低リソース消費: Elasticsearchが動作に4GBのRAMを必要とするような場面でも、Typesenseならわずか512MBのメモリでかなりのトラフィックを余裕で処理できます。
- 組み込みの高可用性: クラスタリングをネイティブにサポートしているため、スケーリングも簡単です。
実践:Dockerでのセットアップ
Dockerは、Typesenseを使い始めるのに最適な方法です。ローカル環境を汚さず、本番環境でも手元のマシンと全く同じように動作させることができます。今回は書店の小規模な検索エンジンを構築して、その仕組みを実演します。
1. Docker Composeファイルの作成
まずプロジェクトディレクトリを作成し、docker-compose.ymlファイルを追加します。これで検索エンジンのコンテナを定義します。
version: '3.8'
services:
typesense:
image: typesense/typesense:0.25.1
container_name: typesense
restart: on-failure
ports:
- "8108:8108"
volumes:
- ./typesense-data:/data
command: '--data-dir /data --api-key=secret_key_123 --enable-cors'
この設定では、ポート8108をマッピングし、永続ボリュームを作成します。コンテナを削除してもデータは保護されます。--api-keyはゲートキーパーの役割を果たします。実際の本番環境では安全なキーを使用してください。
2. エンジンの起動
ターミナルで以下のコマンドを実行します:
docker-compose up -d
http://localhost:8108/healthにアクセスしてステータスを確認してください。{"ok":true}というレスポンスが返ってくれば、準備完了です。
3. スキーマの定義
Typesenseはスキーマを意識する(schema-aware)エンジンです。データ型を推測することもできますが、明示的に定義することでバグを防げます。ここではPythonクライアントを使用しますが、ロジックはJavaScript、Go、PHPでも同じです。
import typesense
client = typesense.Client({
'nodes': [{
'host': 'localhost',
'port': '8108',
'protocol': 'http'
}],
'api_key': 'secret_key_123',
'connection_timeout_seconds': 2
})
books_schema = {
'name': 'books',
'fields': [
{'name': 'title', 'type': 'string' },
{'name': 'author', 'type': 'string' },
{'name': 'publication_year', 'type': 'int32', 'facet': True },
{'name': 'rating', 'type': 'float' }
],
'default_sorting_field': 'rating'
}
client.collections.create(books_schema)
4. データのインデックス作成
次に、検索対象となるコンテンツが必要です。ドキュメントを個別にインデックスすることもできますが、大規模なデータセットの場合はバッチ処理(一括処理)の方がはるかに高速です。
book_data = [
{'title': '華麗なるギャツビー', 'author': 'F・スコット・フィッツジェラルド', 'publication_year': 1925, 'rating': 4.4},
{'title': 'ホビットの冒険', 'author': 'J.R.R. トールキン', 'publication_year': 1937, 'rating': 4.8},
{'title': '1984年', 'author': 'ジョージ・オーウェル', 'publication_year': 1949, 'rating': 4.7}
]
for book in book_data:
client.collections['books'].documents.create(book)
5. 検索のテスト
このスピードこそがTypesenseの醍醐味です。UIを遅延させることなく、一文字入力するたびに検索を実行できます。
search_parameters = {
'q' : 'hobit', # あえてタイポ(hobit)で検索
'query_by' : 'title,author',
'sort_by' : 'rating:desc'
}
result = client.collections['books'].documents.search(search_parameters)
print(result)
「hobit」というタイポがあっても、Typesenseは即座に「ホビットの冒険」を返します。レスポンスにはハイライトされたスニペットが含まれているため、なぜその結果がクエリに一致したのかをユーザーに正確に示すことができます。
さらに活用するために
基本ができるようになったら、「ファセット(Faceting)」を試してみてください。ECサイトで見られるような「出版年で絞り込む」や「著者で絞り込む」といったサイドバーのフィルターを簡単に作成できます。スキーマで'facet': Trueを設定したため、Typesenseは検索結果内の各年度に何冊の本が存在するかを自動的にカウントします。
次はセキュリティです。フロントエンドのコードでマスターAPIキーを直接使用してはいけません。代わりに、Typesenseを使用して「スコープ付き検索キー(Scoped Search Keys)」を生成します。これらは検索のみを実行できる制限付きのキーで、ユーザーIDに基づいて特定のドキュメントだけにアクセスを制限することも可能です。
結論
基本的なSQLクエリからTypesenseのような専用エンジンに切り替えることは、大きなアップグレードになります。インフラコストを低く抑えつつ、プレミアムなユーザー体験を提供できます。次のプロジェクトにElasticsearchがオーバースペックだと感じるなら、Typesenseは完璧な中間解です。高速でDockerとの相性も良く、10分足らずで稼働させることができます。ぜひ試して、検索のレイテンシが劇的に下がるのを実感してください。

