# MyScaleのハイブリッド検索

このガイドでは、ハイブリッド検索を使用してテキスト検索のエクスペリエンスを向上させる利点を紹介し、MyScaleでの実装手順を提供します。

# ハイブリッド検索の必要性

ベクトル検索は、単語間の意味的な関係を捉えることができ、自然言語の複雑な表現や曖昧な表現を処理し、マルチモーダルおよびクロスモーダルの検索をサポートすることができます。多くのタスクにおいて強力で効率的ですが、ベクトル検索は短いテキスト検索の意味を正しく理解するためには補助が必要です。

なぜハイブリッド検索が必要なのでしょうか?

短いテキストクエリは、正確で高い関連性のある結果を生成するためには、より多くの情報と文脈が必要であり、低精度の結果を返すことがあります。たとえば、ユーザーが商品名、製品タグ、服のサイズなどのフレーズに対して包括的で正確なマッチングを行う必要がある場合、ベクトル検索ではなく、用語のマッチングに基づく従来の検索技術(例:「BM25」や「TF-IDF」)がより適しています。

ハイブリッド検索は、ベクトル化されたドキュメントの意味的なカバレッジが不十分な場合の課題を克服するための、意味的な検索と従来の用語のマッチングの組み合わせです。たとえば、ハードウェアストアのデータベースでドライバを検索する場合、ベクトル検索を使用するとデータベースに格納されているすべてのオプションが結果セットに含まれます。しかし、モデル、長さ、材料が正確に一致する特定のドライバを探している場合、用語のマッチングの方が役立ちます。

# MyScaleでのハイブリッド検索の使用方法

このチュートリアルでは、MyScaleでのハイブリッド検索の使用方法を説明します。このチュートリアルを完了するには、MyScaleのアカウントとローカルマシン上のPython3環境のみが必要です。MyScaleにログインし、このチュートリアル全体で使用するクラスタを作成してください。

TIP

クラスタを作成する手順については、クイックスタートドキュメント (opens new window)を参照してください。

クラスタを作成したら、次の手順に従って進めます。

# MyScaleでテーブルを作成する

次のSQLステートメントを実行して、MyScaleのSQLワークスペースにテーブルrd0を作成します。

CREATE TABLE default.rd0
(
    `id` UInt64,
    `body` String,
    `title` String,
    `url` String,
    `body_vector` Array(Float32),
    CONSTRAINT check_length CHECK length(body_vector) = 384
)
ENGINE = MergeTree
ORDER BY id;

以下のSQLステートメントを使用して、テーブルが作成されたかどうかを確認できます。

SHOW tables;

テーブルが作成された場合、このSQLステートメントは次の結果セットを返します。

name
rd0

# Amazon S3からデータをインポートする

RedisSearchがホストするWikipediaの抽象データセット (opens new window)を改善し、ベクトルデータを含めました。テキストのbody列を384次元のベクトルに変換するために、sentence-transformers/all-MiniLM-L6-v2を使用しました。これらのベクトルはbody_vector列に格納され、その間の距離はコサインを使用して計算されます。

TIP

all-MiniLM-L6-v2の使用方法の詳細については、HuggingFaceのドキュメント (opens new window)を参照してください。

最終的なデータセットであるwiki_abstract_with_vector.parquet (opens new window)は8.2GBで、5,622,309のエントリを含んでいます。このデータセットの内容を以下にプレビューできます。ローカルマシンにダウンロードする必要はありません。S3を介して直接MyScaleにインポートできます。

id body title url body_vector
... ... ... ... ...
77 Jake Rodkin is an American .... and Puzzle Agent. Jake Rodkin https://en.wikipedia.org/wiki/Jake_Rodkin (opens new window) [-0.081793934,....,-0.01105572]
78 Friedlandpreis der Heimkehrer is ... of Germany. Friedlandpreis der Heimkehrer https://en.wikipedia.org/wiki/Friedlandpreis_der_Heimkehrer (opens new window) [0.018285718,...,0.03049711]
... ... ... ... ...

次のSQLコマンドをSQLワークスペースで実行して、このデータをインポートします。

INSERT INTO default.rd0 SELECT * FROM s3('https://myscale-datasets.s3.ap-southeast-1.amazonaws.com/wiki_abstract_with_vector.parquet','Parquet');

注意

データのインポートには約10分かかる見込みです。

次のSQLステートメントを実行して、インポートされたデータが5,622,309行に達したかどうかを確認します。

SELECT COUNT(*) FROM default.rd0;

TIP

データのインポートが完了するまで、このSQLステートメントを複数回実行することができます。

# ベクトルインデックスを作成する

ベクトルインデックスを作成する最初のステップは、ベクトル検索のパフォーマンスを向上させるために、テーブルのデータパーツを1つのパーツにマージすることです。その後、このテーブルにベクトルインデックスを追加します。

# ベクトル検索のパフォーマンスを向上させる

テーブルを最適化してベクトル検索のパフォーマンスを向上させるには、SQLワークスペースで次のSQLコマンドを実行します。

OPTIMIZE TABLE default.rd0 FINAL;

このコマンドの実行には時間がかかる場合があります。

次のSQLステートメントを実行して、このテーブルのデータパーツが1つに圧縮されたかどうかを確認します。

SELECT COUNT(*) FROM system.parts WHERE table='rd0' AND active=1;

データパーツが1つに圧縮された場合、このSQLステートメントは次の結果セットを返します。

count()
1

# ベクトルインデックスを作成する

次のステートメントを実行して、ベクトルインデックスを作成します。

注意

MSTGは、MyScaleが開発したベクトルインデックスです。

ALTER TABLE default.rd0 ADD VECTOR INDEX RD0_MSTG body_vector
TYPE MSTG('metric_type=Cosine');

インデックスの作成には時間がかかります。次のSQLステートメントを実行して、インデックスの作成の進行状況を確認します。ステータス列がBuiltを返す場合、インデックスの作成が成功しています。インデックスがまだ作成中の場合、ステータス列はInProgressを返すはずです。

SELECT * FROM system.vector_indices;

# ベクトル検索とハイブリッド検索を実行する

このハウツーガイドでは、ベクトル検索とハイブリッド検索の両方について説明します。

ただし、先に進む前に、いくつかの事前作業を行う必要があります。

# 事前作業

アプリケーションで以下のPythonコードを使用して、次のことを実現します。

  • ホスト、ユーザー名、パスワードを変更して、MyScaleクラスタに接続します。
  • テキストをベクトルに変換するために、transformer all-MiniLM-L6-v2モデルをインポートします。
  • SQLの実行結果を表示するためのシンプルな出力関数を作成します。
import clickhouse_connect
from prettytable import PrettyTable
from sentence_transformers import SentenceTransformer
# transformer all-MiniLM-L6-v2を使用
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
# MyScaleの情報
host = "your_endpoint"
port = 443
username = "your_username"
password = "your_password"
database = "default"
table = "rd0"
# MyScaleクライアントの初期化
client = clickhouse_connect.get_client(host=host, port=port,
                                       username=username, password=password)
# コンテンツを表示するためのテーブルを使用
def print_results(result_rows, field_names):
    x = PrettyTable()
    x.field_names = field_names
    for row in result_rows:
        x.add_row(row)
    print(x)

# ベクトル検索

次のPythonコードスニペットでは、ベクトル検索プロセス、またはニューラル検索の手順が次のように説明されています。

  • モデルall-MiniLM-L6-v2を使用してテキスト「テレビニュースサービスの歴史」を埋め込みベクトルに変換します。
  • ベクトル検索を使用して、データセットの上位5つの類似度が高いWikipediaページを返します。
# ベクトル検索を試す
sentence = "history about television news service"
sentence_embedding = model.encode([sentence])[0]
sentence_result = client.query(query=f"SELECT id, title, body, distance('alpha=1')"
                                     f"(body_vector, {list(sentence_embedding)}) AS distance "
                                     f"FROM {database}.{table} ORDER BY distance ASC LIMIT 5")
print_results(sentence_result.result_rows, ["ID", "Title", "Body", "Distance"])

以下の表は、検索結果を示しています。

ID Title Body Distance
2341540 Television news in the United States Television news in the United States has evolved over many years. It has gone from a simple 10- to 15-minute format in the evenings, to a variety of programs and channels. 0.3019871711730957
4741891 United States cable news Cable news channels are television channels devoted to television news broadcasts, with the name deriving from the proliferation of such networks during the 1980s with the advent of cable television. In the United States, early networks included CNN in 1980, Financial News Network (FNN) in 1981 and CNN2 (now HLN) in 1982. 0.3059382438659668
4555265 News and Views (TV series) News and Views was an early American evening news program. Broadcast on ABC from 1948 to 1951, it was ABC's first evening news program and one of the first such programs on any television network; Both CBS and NBC also initiated their evening news programs (respectively CBS Television News and Camel News Caravan, called Camel Newsreel Theatre at first) that same year, both debuting a few months before the first broadcast of News and Views on August 11, 1948. 0.3165452480316162
185179 MediaTelevision Media Television was a Canadian television newsmagazine series, which aired weekly on Citytv from 1991 to 2004. It was also syndicated internationally, airing in over 100 countries around the world at some point during its run. 0.32938069105148315
1426832 News service News service may refer to: 0.3431185483932495

# ベクトル検索の制限

上記の説明から、短いテキストフレーズに対して純粋なベクトル検索を使用することには制限があることがわかります。

例えば、「BGLE Island」というフレーズをベクトルに変換し、ベクトル検索を実行し、結果を見てみましょう。

terms = "BGLE Island"
terms_embedding = model.encode([terms])[0]
stage1 = f"SELECT id, title, body, distance('alpha=1')" \
         f"(body_vector,{list(terms_embedding)}) AS distance FROM {database}.{table} " \
         f"ORDER BY distance ASC LIMIT 5"
sentence_result = client.query(query=stage1)
print_results(client.query(query=stage1).result_rows, ["ID", "Title", "Body", "Distance"])

以下は、上位5つの検索結果です。

ID Title Body Distance
2625112 Bligh Island (Alaska) Bligh Island}} 0.227422833442688
2625120 Bligh Island (Canada) Bligh Island}} 0.227422833442688
4894492 Hedley (band) Island 0.3269183039665222
4708096 Blueberry Island Blueberry Island may refer to: 0.3446136713027954
5519217 Brown Island (Antarctica) Brown Island}} 0.35350120067596436

注意

これらの結果を見ると、「BGLE」という単語が上位5つの結果に含まれていないことが明らかです。

# ハイブリッド検索

短いフレーズや単語単位の検索に純粋なベクトル検索に頼るのではなく、結果の精度を向上させるためにハイブリッド検索を使用しましょう。例えば、「BGLE Island」という用語に対して、次の2段階のアプローチを取ります。

  • ベクトル検索を使用して、上位200の候補を特定します。
  • MyScaleの組み込み関数と簡略化されたTF-IDF(単語の出現頻度-逆文書頻度)メソッドを使用して、これらの結果を再構成および洗練します。

# ベクトル検索の使用

次のコードスニペットでは、上位200の結果を特定するためにベクトル検索を実行する方法が説明されています。

# ステージ1. ベクトルリコール
terms = "BGLE Island"
terms_embedding = model.encode([terms])[0]
terms_pattern = [f'(?i){x}' for x in terms.split(' ')]
stage1 = f"SELECT id, title, body, distance('alpha=1')" \
         f"(body_vector,{list(terms_embedding)}) AS distance FROM {database}.{table} " \
         f"ORDER BY distance ASC LIMIT 200"

# ヘルパー関数

ハイブリッド検索を実行する前に、MyScaleが提供する次の2つの関数を理解する必要があります。

multiMatchAllIndices(): この関数は、指定された正規表現と一致するすべての部分文字列の開始インデックスを返します。2つのパラメータ、ソース文字列と正規表現のリストを取ります。

TIP

このインデックスは0ではなく1から始まります。

注意

詳細については、ClickHouseのドキュメントのmultiMatchAllIndices (opens new window)を参照してください。

例:

SELECT multiMatchAllIndices(
        'He likes to eat tomatoes.',
        ['(?i)\\blike\\b', '(?i)likes', '(?i)Tomatoes']) AS result

このSQL文を実行すると、次の結果が返されます。

result
[2, 3]

countMatches(): この関数は、文字列内の指定された部分文字列の数をカウントします。2つのパラメータ、ソース文字列とre2構文を使用した正規表現を取ります。

注意

詳細については、ClickHouseのドキュメントのcountMatches (opens new window)を参照してください。

例:

SELECT countMatches('He likes to eat tomatoes', '(?i)Tomatoes') AS result

このSQL文を実行すると、次の結果が返されます。

result
1

# 検索結果のソート

次のPythonコードスニペットでは、この次のステージでこれらの検索結果を2回ソートします(用語の再順位付け)。

  • これらの結果を人気度に基づいてソートします。検索ヒット数が多いほど、ランキングが高くなります。
  • これらの結果を再度、検索ヒット数(用語の出現頻度)に基づいてソートします。出現頻度が高いほど、ランキングが高くなります。

TIP

2回目のソートには、簡略化されたTF-IDFを使用します。

# ステージ2. 用語の再順位付け
stage2 = f"SELECT tempt.id, tempt.title,tempt.body, distance1, distance2 FROM ({stage1}) tempt " \
         f"ORDER BY length(multiMatchAllIndices(arrayStringConcat([body, title], ' '), {terms_pattern})) " \
         f"AS distance1 DESC, " \
         f"log(1 + countMatches(arrayStringConcat([title, body], ' '), '(?i)({terms.replace(' ', '|')})')) " \
         f"AS distance2 DESC limit 10"
sentence1_result = client.query(query=stage2)
print_results(sentence1_result.result_rows, ["ID", "Title", "Body", "distance1", "distance2"])

検索結果は次のようになります。

ID Title Body distance1 distance2
4426976 Symington Islands Symington Islands () is a group of small islands lying west-northwest of Lahille Island, in the Biscoe Islands. Charted by the British Graham Land Expedition (BGLE) under Rymill, 1934-37. 2 1.945910148700207
4425283 Saffery Islands Saffery Islands () is a group of islands extending west from Black Head, off the west coast of Graham Land. Charted by the British Graham Land Expedition (BGLE) under Rymill, 1934–37. 2 1.6094379132876024
466090 The Narrows (Antarctica) The Narrows () is a narrow channel between Pourquoi Pas Island and Blaiklock Island, connecting Bigourdan Fjord and Bourgeois Fjord off the west coast of Graham Land. It was discovered and given this descriptive name by the British Graham Land Expedition (BGLE), 1934–37, under Rymill. 2 1.3862943611198906
79253 Boaz Island, Bermuda Boaz Island, formerly known as Gate's Island or Yates Island, is one of the six main islands of Bermuda. It is part of a chain of islands in the west of the country that make up Sandys Parish, lying between the larger Ireland Island and Somerset Island, and is connected to both by bridges. 1 2.1972245771389134
3886596 Moresby Island (Gulf Islands) Moresby Island is one of the Gulf Islands of British Columbia, located on the west side of Swanson Channel and east of the southern end of Saltspring Island. It is not to be confused with Moresby Island, the second largest of the Queen Charlotte Islands off the north coast of BC. 1 2.0794415416798357
5026601 Bazett Island Bazett Island is a small island close south of the west end of Krogh Island, in the Biscoe Islands. It was mapped from air photos by the Falkland Islands and Dependencies Aerial Survey Expedition (1956–57), and named by the UK Antarctic Place-Names Committee for Henry C. 1 1.945910148700207
5026603 Bazzano Island Bazzano Island () is a small island lying off the south end of Petermann Island, between Lisboa Island and Boudet Island in the Wilhelm Archipelago. It was discovered and named by the French Antarctic Expedition, 1908–10, under Jean-Baptiste Charcot. 1 1.945910148700207
5451889 Baudisson Island Baudisson Island is an island of Papua New Guinea, located south of New Hanover Island and west of the northern part of New Ireland. It is located between Selapiu Island and Manne Island. 1 1.945910148700207
4176021 Bluck's Island, Bermuda Bluck's Island (formerly Denslow['s] Island, Dyer['s] Island) is an island of Bermuda. It lies in the harbor of Hamilton in Warwick Parish. 1 1.7917594699409376
202822 Sorge Island Sorge Island () is an island lying just south of The Gullet in Barlas Channel, close east of Adelaide Island. Mapped by Falkland Islands Dependencies Survey (FIDS) from surveys and air photos, 1948-59. 1 1.7917594699409376

2つのソート操作は以下のように示されます。

rerank

# TF-IDFの説明

TF-IDFは、ドキュメントのコレクション内での単語の関連性を評価するために使用される統計的な尺度です。これは、特定のドキュメント内で単語が出現する回数と、その単語の逆(反対)文書頻度の2つのメトリックを乗算することによって達成されます。

例えば:

この例では、が単語の集合であり、​がドキュメントにおけるすべての単語/用語の出現頻度であるとします。標準のTF-IDF計算では、各単語の出現頻度を個別に計算して関連性を測定します。

次に、各単語に対して異なる逆文書頻度を計算します。すべての単語を1つのクラスと見なす場合、TF-IDFの計算は次のように簡略化できます。

ここで、

簡略化されたTF-IDFの計算では、すべての用語の関連性を計算するために同じ逆文書頻度(IDF)を分母に使用します。したがって、ソート結果に影響を与えないこれらのIDFの分母は省略できるため、最終的な簡略化されたTF-IDFは、TFの形式になります。