目次
本記事は、情報検索・検索技術Advent Calendar 2025の19日目の記事です(今年はなんか参加者が少なくてさみしいけど、きっと検索の人たちは検索流行ってて忙しいんだなということにしておこう)。
こちらの秋の検索勉強会で話をした内容になります。 録画に失敗したので記事を書いておくかなと。。。
発表の資料と作ってるGitHubのリポジトリです。
なんで作ってるの?
仕事柄(検索システムに関するコンサルなどをやっていたりします)、検索に関する記事をよく読んでいます。 Xだったり、どこかのSlackだったり、Googleニュースだったりといろんなところから出てくる記事になんとなく目を通したり、仕事で調べた検索に関する各社の公式ブログや論文などにも目を通しています。
それなりの量を日々読んでいて、お客さんの話や質問で上がってくる内容に関連しそうな記事があったなぁと思うことが多々あります。 ただ、最近読んだことは覚えていても、どの記事だったかが見つけられないことがあるんです(年齢的な問題?)。
なので、なんとなく読んで面白かった記事について、ブックマーク+メモを残す仕組みをWeaviateを使って作ってみているという話です。
ちなみに、コードやドキュメントはAmazon QのVS Code拡張を使いながら書いてもらっています。 個人ツールはそれっぽい名前を付けてもらうために、作りたいものの概要を説明してから「中二病っぽい名前の候補を10個作って」とLLMにお願いして名前を付けています。
なんでWeaviate?
ベクトル検索を主体にしたいなぁというのがあります。
- うろ覚えなのでなんとなくで検索できるようにしたい
- 絞り込み検索もしたい(どのサイトとかは覚えてたりする)
- Weaviateを宣伝したい(Community作ろうとしてます)
スライドではUIや前段のスクレイピングなどの話も記載していますが、今回のブログではWeaviateの部分を簡単に紹介する形にします。 Weaviateに登録する前段で記事の要約をして、関連するキーワードをLLMをつかって抽出する処理を入れてあります。
スキーマの定義
Weaviateでは、コレクションに対してデータを登録すれば自動的にデータに合わせてスキーマを定義してくれる機能があります。 この場合、コレクションに作られるベクトル空間は1つとなります。
ただ、今回はベクトル空間をいくつか用意したい(タイトル、要約、自分のメモなど)と思っているので、あらかじめコレクション作成時にこちらでスキーマを指定します。
# コレクション作成
client.collections.create(
name="GrimoireChunk",
description="Grimoire Keeperで管理するWebページのチャンク",
properties=[
Property(name="pageId", data_type=DataType.INT),
Property(name="chunkId", data_type=DataType.INT),
Property(name="url", data_type=DataType.TEXT),
Property(name="title", data_type=DataType.TEXT),
Property(name="memo", data_type=DataType.TEXT),
Property(name="content", data_type=DataType.TEXT),
Property(name="summary", data_type=DataType.TEXT),
Property(name="keywords", data_type=DataType.TEXT_ARRAY),
Property(name="createdAt", data_type=DataType.DATE),
Property(name="isSummary", data_type=DataType.BOOL),
],
vector_config=[
Configure.Vectors.text2vec_openai(
name="content_vector", source_properties=["content"]
),
Configure.Vectors.text2vec_openai(
name="title_vector",
source_properties=["title", "summary"],
),
Configure.Vectors.text2vec_openai(
name="memo_vector", source_properties=["memo"]
),
],
)
コレクションの定義をしている部分のコードです。
コレクションの名前(name)、説明文(description)、プロパティ(properties:フィールド、カラムのようなもの)をまず定義しています。
プロパティにも説明を書いた方がいいのですが、今回は記述していないです
( Query Agent | Weaviate Documentationという機能も提供されており、エージェントがコレクションを検索するときのヒントになるので、コレクションやプロパティに説明を付けてあげたほうが今後の役に立つことになります)。
ここまでは、よくあるスキーマの定義になりますが、最後のvector_configがこのコレクションのベクトルに関する設定になります。
今回は名前を付けたベクトル(named vectors)を3つ設定しています。
title_vector:タイトルと要約から生成するベクトル空間content_vector:記事本文をチャンキングしたものから生成するベクトル空間memo_vector:メモから生成するベクトル空間
すべて、入力としてはpropertiesで定義したデータの型(今回はすべてテキスト)のデータになります。
Weaviateの内部で、ベクトル化モジュールがOpenAIのembeddingモデルをAPI経由で呼び出して、テキストからベクトルに変換して保存してくれます。
アプリとしては、タイトルや要約などはテキストとしてWeaviateにデータを登録するだけになります。
modelパラメータを設定することで、自分でモデルの選択もできますが、今回はデフォルトのモデルを使用するようにしています。
データの登録
collection = client.collections.get("GrimoireChunk")
for i, chunk in enumerate(chunks):
weaviate_object = {
"pageId": page_data.id,
"chunkId": i,
"url": page_data.url,
"title": page_data.title,
"memo": page_data.memo or "",
"content": chunk,
"summary": page_data.summary or "",
"keywords": json.loads(page_data.keywords or "[]"),
"createdAt": (
page_data.created_at.replace(tzinfo=None).isoformat() + "Z"
if page_data.created_at.tzinfo is None
else page_data.created_at.isoformat()
),
"isSummary": i == 0,
}
result = collection.data.insert(properties=weaviate_object)
登録の処理の抜粋です。
前処理として本文の部分(content)をチャンクに分割しているので、それらを登録しています。
それとは別に、title_vectorやmemo_vectorのベクトル空間を検索した時に、不要なデータが表示されないようにisSummaryということで、最初のチャンクにフラグを立てて、それで絞り込めるようにしてあります。
検索処理
collection = client.collections.get("GrimoireChunk")
# フィルター条件構築
where_filter = self._build_weaviate_filter(filters) if filters else None
exclude_filter = self._build_exclude_filter(exclude_keywords) if exclude_keywords else None
# ベクトル別フィルター追加
summary_filter = None
if vector_name != "content_vector":
from weaviate.classes.query import Filter
summary_filter = Filter.by_property("isSummary").equal(True)
# フィルター結合
filters_list = []
if where_filter:
filters_list.append(where_filter)
if summary_filter:
filters_list.append(summary_filter)
if exclude_filter:
filters_list.append(exclude_filter)
final_filter = None
if len(filters_list) == 1:
final_filter = filters_list[0]
elif len(filters_list) > 1:
final_filter = Filter.all_of(filters_list)
# クエリ実行
response = collection.query.near_text(
query=query,
target_vector=vector_name,
limit=limit,
filters=final_filter,
return_metadata=MetadataQuery(certainty=True),
)
# 結果変換
return self._convert_search_results_v4(response)
検索処理です。入力されたキーワードや文章をベクトル検索するのにはnear_textメソッドを利用します。
target_vectorが検索対象となるベクトル空間の名前(例:title_vector)の指定です。
クエリ実行前にいくつかのフィルタ条件を組み立てて、条件として追加してあります(期間だったり除外するキーワードなどの条件)。
検索するときもこちらでベクトルに変換するなどの処理は行っておらず、テキストをqueryに渡せば、よしなにWeaviate側でベクトル空間の設定をもとにエンベディングして検索をしてくれます。
まとめ
とりあえず、トラディショナルな感じのRAGのベースを自分でも試してみるかな?と思いながら作ってます。 が、いろいろと使ってみて気づくことが多いなとなってるところです。
データはWeaviateに入れる前の時点でファイルとして保存するような作りにしているので、いろいろと改善できそうなことに手を出しながら触っていこうかなと思います(late interactionっぽいことやってみたり、チャットのUIを用意したり、チャンキングの仕組みを変えてみたり)。
なんとなく、Weaviateの使い方がこんな感じかな?というイメージを持ってもらえたら幸いです。
comments powered by Disqus
See Also by Hugo
- Weaviateで日本語を利用した検索という話をしました
- Weaviate入門(Quickstart - Go言語編)
- Weaviate入門(オブジェクト、コレクション)
- 今年もオンラインでBerlin Buzzwordsに参加した
- Bonfire Data & Science #1に参加しました