Redis®*: 検索とJSON

RedisクエリエンジンでJSONドキュメントを保存し、効率的にクエリを実行する

👋 Stackhero ドキュメントへようこそ!

Stackhero では、すぐに使える Redis cloud ソリューションをご提供しています。主な特長は以下の通りです:

  • Redis Commander Web UI を標準搭載。
  • メッセージサイズ・転送量が無制限
  • ワンクリックで簡単にアップデート可能。
  • プライベートかつ専用インフラによる最適なパフォーマンスと強固なセキュリティ

時間を節約し、運用をシンプルに:Stackhero の Redis cloud hosting ソリューションは、わずか5分でお試しいただけます!

StackheroのRedisインスタンスには、Redis Open Sourceで利用可能なすべてのモジュールが含まれています。これらのモジュールにより、Redisは単なるキャッシュ以上の存在となります。実際のJSONドキュメントを保存し、インデックス化し、全文検索、数値検索、タグ検索を直接実行できます。追加のデータベースをスタックに加える必要はありません。

以下の4つのモジュールは、すべてご利用のプランに含まれています:

  • JSONJSON.* コマンド):ネイティブなJSON型を追加し、JSONPathをサポートします。ドキュメント全体を書き換えることなく、個々のフィールドを読み書きできます。
  • SearchFT.* コマンド、Redisクエリエンジンとも呼ばれます):全文検索、セカンダリインデックス、集計、ベクトル検索機能を提供します。
  • BloomBF.*CF.*CMS.*TOPK.*TDIGEST.* コマンド):大規模なデータセットに対して「すでに見たことがあるか?」といった問いに、最小限のメモリで効率的に答える確率的データ構造です。
  • Time seriesTS.* コマンド):保持期間、ダウンサンプリング、集計機能を備えたタイムシリーズ型を提供します。

Vector setsVADDVSIM)はRedis本体に直接組み込まれています。これにより、埋め込み(embedding)向けの効率的なベクトル類似検索が可能で、常にサービス上で利用できます。追加の設定は不要です。

モジュールはデフォルトで無効になっています。これにより、既存サービスは新機能を有効化するまで元の挙動を維持できます。

  1. Stackheroダッシュボードでサービスを開きます。
  2. サービスの設定画面に移動します。
  3. Modules セクションで有効化したいモジュールを選択します。
  4. 変更を保存します。

Redisサービスは、選択したモジュールをロードして再起動します。この処理は通常数秒で完了します。

モジュール(JSONドキュメント、Bloomフィルター、タイムシリーズなど)を使ってデータを保存した場合、そのモジュールは有効なままにしてください。無効化すると、そのモジュールで作成したデータにRedisがアクセスできなくなり、サービスの起動もできなくなります。Vector setsはRedis本体の一部なので常に利用可能です。

現在ロードされているモジュールを確認するには、以下のコマンドを実行します:

redis-cli -u "rediss://default:<yourPassword>@<XXXXXX>.stackhero-network.com:<PORT_TLS>" MODULE LIST

ドキュメントを文字列としてシリアライズする代わりに、ネイティブなJSON形式で保存できます:

JSON.SET product:1 $ '{"name":"Espresso machine","brand":"Bianca","price":459,"tags":["coffee","kitchen"],"stock":12}'

この方法により、個々のフィールドをサーバーサイドかつアトミックに読み書きできます:

JSON.GET product:1 $.price
# "[459]"

JSON.NUMINCRBY product:1 $.stock -1
# "[11]"

JSON.ARRAPPEND product:1 $.tags '"gift"'

このワークフローは開発工数を削減し、エラーも減らせます。例えば、在庫カウンターを減らす場合、アプリケーション側でドキュメント全体を読み込み・パース・再書き込みする必要はありません。サーバー側で安全かつ即時に処理されます。

全コマンド一覧はRedis JSONドキュメントをご参照ください。

インデックスは一度宣言するだけで十分です。Redisが、指定したプレフィックスに一致するすべてのキー(インデックス作成前から存在するキーも含む)に対して自動的にインデックスを維持します。

FT.CREATE productsIndex
  ON JSON
  PREFIX 1 product:
  SCHEMA
    $.name AS name TEXT
    $.brand AS brand TAG
    $.price AS price NUMERIC SORTABLE
    $.tags[*] AS tags TAG

これで、以下のようなクエリを実行できます:

# 名前の全文検索と価格範囲指定
FT.SEARCH productsIndex "@name:(espresso) @price:[0 500]"

# ブランドの完全一致検索、価格順ソート
FT.SEARCH productsIndex "@brand:{Bianca}" SORTBY price ASC

# オートコンプリート用途のプレフィックス検索
FT.SEARCH productsIndex "@name:(espr*)"

Redisはインデックス上で直接集計処理も可能なため、データをエクスポートせずに分析クエリに対応できます:

FT.AGGREGATE productsIndex "*"
  GROUPBY 1 @brand
  REDUCE COUNT 0 AS products
  REDUCE AVG 1 @price AS averagePrice
  SORTBY 2 @products DESC

データをJSONではなく通常のハッシュで保存している場合は、ON HASHと標準のフィールド名をスキーマで指定してください。

import { createClient, SCHEMA_FIELD_TYPE } from 'redis';

const client = createClient({ url: process.env.STACKHERO_REDIS_URL_TLS });
await client.connect();

// 起動時にインデックスを作成
try {
  await client.ft.create(
    'productsIndex',
    {
      '$.name': { type: SCHEMA_FIELD_TYPE.TEXT, AS: 'name' },
      '$.brand': { type: SCHEMA_FIELD_TYPE.TAG, AS: 'brand' },
      '$.price': { type: SCHEMA_FIELD_TYPE.NUMERIC, AS: 'price', SORTABLE: true }
    },
    { ON: 'JSON', PREFIX: 'product:' }
  );
}
catch (error) {
  if (!error.message.includes('Index already exists')) {
    throw error;
  }
}

await client.json.set('product:1', '$', {
  name: 'Espresso machine',
  brand: 'Bianca',
  price: 459
});

const results = await client.ft.search('productsIndex', '@name:(espresso) @price:[0 500]');
console.log(results.total, results.documents);

await client.quit();
import os
import redis
from redis.commands.search.field import TextField, TagField, NumericField
from redis.commands.search.index_definition import IndexDefinition, IndexType
from redis.commands.search.query import Query

r = redis.from_url(os.environ['STACKHERO_REDIS_URL_TLS'], decode_responses=True)

# 起動時にインデックスを作成
try:
    r.ft('productsIndex').create_index(
        (
            TextField('$.name', as_name='name'),
            TagField('$.brand', as_name='brand'),
            NumericField('$.price', as_name='price', sortable=True),
        ),
        definition=IndexDefinition(prefix=['product:'], index_type=IndexType.JSON),
    )
except redis.ResponseError as error:
    if 'Index already exists' not in str(error):
        raise

r.json().set('product:1', '$', {
    'name': 'Espresso machine',
    'brand': 'Bianca',
    'price': 459,
})

results = r.ft('productsIndex').search(Query('@name:(espresso) @price:[0 500]'))
print(results.total, results.docs)
  • Searchインデックスはデータベース0でのみ動作します。 FT.CREATEは他のデータベースではエラーとなります。アプリケーションで複数の論理データベースを利用している場合は、インデックス化したいデータをデータベース0に保存するか、キーのプレフィックスでデータセットを分離してください。
  • インデックスもデータ同様、プランのメモリを消費します。 大規模なテキストインデックスやベクトルインデックスはメモリ使用量が大きくなる場合があるため、構築後はPrometheusメトリクスused_memoryを監視してください。
  • 既存キーへのインデックス作成はバックグラウンドで実行されます。 FT.CREATE直後は、検索結果が一時的に不完全な場合があります。インデックス作成の進捗はFT.INFO productsIndexで確認できます。
  • すべてのモジュールコマンドが完全に利用可能です。 有効化した各モジュールの全コマンドがサービス上で利用できます。

ベクトル類似性検索、セマンティックサーチ、RAG(retrieval-augmented generation)について詳しくは、ベクトル検索ガイドをご覧ください。