跳到正文
gihyo.jp·· 3 小時前AI 評分59

LanceDB 圖片與文字管理教學:結合 pandas、關鍵字與向量搜尋

画像と本文をまとめて扱うLanceDB 〜pandasとの連携からキーワード・ベクトル検索まで〜

AI 導讀

文章以 12 張照片示範如何用 LanceDB 將圖片、説明文字、屬性及搜尋向量存入同一資料表,並配合 pandas 擷取和分析資料。文章再以 intfloat/multilingual-e5-base 和 ICU 示範向量搜尋與日文關鍵字搜尋,並結合屬性篩選。

正文

寺田 学(@terapyon)です。2026年10月の「Python Monthly Topics」は、画像やテキストをまとめて保存し、検索にも利用できるデータベースのLanceDBを紹介します。

筆者は、独自に開発している研究用のベクトルDBで、データの保存にLanceを使っています。Lanceは、画像やベクトルを含むデータを保存するためのオープンソースのデータ形式(Lanceフォーマット)と、その形式を読み書きするライブラリです。当初は、一部のメタデータを表す数値の対応表(マッピングテーブル)をNumPyのオブジェクトとして持っていました。その時は管理が煩雑になりやすく、使うときにはデータをすべてメモリに読み込む必要がありました。Lanceへ移行してからは、データをひとまとまりとして扱え、必要な部分だけを読み出せるようになりました。

このLanceを土台にしたデータベースがLanceDBで、筆者はPythonのプロジェクトからも利用しています。利用する中で見つけた不具合は、修正を送ってLance 13.0.0に取り込まれました。

写真に説明文や分類、検索用のベクトルを付けて扱うことは、pandasやParquet、DuckDB、NumPyでもできます。ただ、写真・説明文・分類・ベクトルをひとつのテーブルにまとめておき、必要な列だけ読む、条件で絞り込む、キーワードや意味で検索する、といった取り出し方をそのテーブルに対してそのまま使えるのがLanceDBの便利な点です。

本記事では、写真と説明文、属性情報をLanceDBに登録し、pandasと連携しながら必要なデータを取り出すところから始めます。その後、キーワード検索とベクトル検索へと使い方を広げます。画像や文書に説明や分類を付けて管理している方や、手元のデータを検索にも使ってみたい方は、ぜひ試してみてください。

1. データの管理と検索を支えるLanceDB

まず、LanceDBで扱うデータの形と、それを支えるLanceフォーマット、Pythonからの使い方を見ていきます。

1.1. 画像⁠・本文⁠・属性情報の一元管理

LanceDBは、画像やテキスト、属性情報、検索用のベクトルを同じテーブルで扱えるデータベースです。各列の名前やデータ型はスキーマで定め、検索用のインデックスの作成や、行の追加・更新・削除、データのバージョン管理にも対応しています。詳しくは公式ドキュメントのテーブルの説明を参照してください。

今回は1枚の写真を1レコードとし、以下のような情報を持たせます。

情報 内容
ID レコードを識別する値
画像 画像ファイルの本体を表すバイト列
本文 写真の内容を説明するテキスト
属性 カテゴリや年など、分類・絞り込みに使う情報
ベクトル 本文から生成する、意味による検索のための数値の配列

ベクトルは、本文の意味の近さで検索するために使います(2026年4月:Pythonによるベクトル検索の基礎と実践 ~Embedding、Vector DB~で紹介しました⁠)⁠。画像は、ファイルの保存先ではなく画像本体をテーブルに保存します。こうすると、属性で絞り込んだり検索したりしたレコードから、そのまま画像を取り出せます。なお、LanceDBには大きなデータを必要なときに読み込むための専用のBlob APIもありますが、本記事の写真は1枚100KB程度と小さいため、通常のバイナリ列を使います。違いは公式ドキュメントのマルチモーダルデータの説明にまとめられています。

1.2. LanceDBを支えるLanceフォーマット

LanceDBのデータは、導入で触れたLanceフォーマットで保存されます。LanceはLanceDBとは別のオープンソースプロジェクトです。ソースコードはGitHubにあり、LanceDBのオープンソース版と同じく、Apache License 2.0で公開されています。

Lanceは列指向の形式なので、写真のカテゴリを集計するときに画像本体まで読み出す必要はありません。一方で、検索結果の数件から画像や本文を取り出すような、行単位のランダムアクセスも重視して設計されています。筆者がLanceに興味を持ったのも、この両方を扱う点です。ただし、研究用のベクトルDBでの自分の用途では、アクセス性能に調整が必要な部分もありました。本記事では性能の比較ではなく、データの保存と利用の流れに着目します。

必要な列を使って集計し、選んだレコードの画像や本文を取り出す。こうした二つの使い方を支える保存形式がLanceであり、本記事ではLanceDBを通して利用していきます。

1.3. PythonからのLanceDBの利用

本記事では、LanceDBのオープンソース版をPythonから利用します。ローカルでの利用では、データベースサーバーを起動せず、保存先のディレクトリを指定して使い始められます。ElasticsearchのPythonクライアントのように起動済みのサーバーへ接続するのではなく、ライブラリとしてPythonの処理に組み込み、保存先のデータを直接扱います。保存先にはS3などのオブジェクトストレージも指定でき、これは後半で紹介します。

オープンソース版はApache License 2.0で公開されており、公式SDKにはPythonのほか、TypeScriptやRust向けのものがあります。分散処理や大規模な利用に向けては、複数のマシンで構成したクラスタで動くLanceDB Enterpriseも提供されています。

Pythonからは、辞書のリストやpandasのDataFrameでデータを登録し、条件や列を指定して取得した結果をDataFrameに変換できます。保存しているデータすべてではなく、必要な範囲だけをDataFrameにして、pandasで集計・可視化するという使い方です。次章では、写真と説明文を含むデータを用意し、pandas・Parquetからの登録と取得を確認していきます。

2. 画像⁠・本文⁠・属性情報の登録と取得

1章で示した1枚1レコードの構成を、実際にLanceDBへ登録します。この章では検索はまだ使わず、属性や列を指定して必要なデータを取り出すところまでを確認します。

2.1. 実行環境と題材の準備

本記事のコードは以下の環境で動作確認しています。コードは、JupyterLabのノートブックで上から順にセルを実行する前提です。

項目 バージョン 備考
Python 3.14.8
LanceDB 0.40.0 データの保存・取得
pandas 3.0.5 データの準備・集計
PyArrow 25.0.1 Parquetの読み込み
Pillow 12.3.0 画像の表示
sentence-transformers 6.0.1 Embeddingの生成(3章)
JupyterLab 4.6.4 コードの実行

uvを使う場合

uvを使ったインストール
uv add lancedb pandas pyarrow pillow sentence-transformers jupyterlab

pipを使う場合

pipを使ったインストール
pip install lancedb pandas pyarrow pillow sentence-transformers jupyterlab

本記事の題材には、2026年5月:Pythonマルチモーダル検索とANN 〜ベクトル検索の応用編〜で使った写真12枚を再利用します。画像は、5月の記事で公開している画像のzipファイルからダウンロードできます。以下のディレクトリ構成を前提としています。

ディレクトリ構成
project/
├── images/          # 写真の画像ファイル(12枚)
│   ├── europython-keynote-01.jpg
│   ├── europython-talk-01.jpg
│   └── ...
├── photos.csv       # 写真の情報をまとめたCSVファイル
└── lancedb.ipynb    # 記事のコードを実行するノートブック

photos.lancedb/とphotos.parquetが、2.2のコードを実行すると作成されます。

題材となる12枚の画像(2026年5月:Pythonマルチモーダル検索とANN 〜ベクトル検索の応用編〜 より)
題材となる12枚の画像

写真ごとのID・ファイル名・説明文・年・カテゴリは、以下のCSVファイルにまとめました。説明文と年は今回の説明用に付けた値で、年は実際の撮影年ではありません。

写真の情報をまとめたCSVファイル (photos.csv)
id,filename,text,year,category
0,europython-keynote-01.jpg,大きな会場のステージで登壇者が基調講演をしている。,2025,conference
1,europython-talk-01.jpg,Pythonの技術について発表者が講演している。,2026,conference
2,europython-panel.jpg,複数の登壇者がパネルディスカッションで議論している。,2026,conference
3,europython-social.jpg,イベントの交流会で参加者同士が会話を楽しんでいる。,2025,conference
4,waterside-park-ducks.jpg,池の水面で鴨が泳いでいる。,2026,park
5,waterside-park-flock.jpg,公園の水辺に鳥の群れが集まっている。,2025,park
6,skytree-04-27.jpg,街の建物の向こうに高い塔が見える。,2026,city
7,sluice-gate-01.jpg,川の水門と周囲の風景。,2025,river
8,europython-talk-02.jpg,技術イベントの会場で発表を聞いている。,2026,conference
9,europython-keynote-02.jpg,基調講演のステージと会場の様子。,2025,conference
10,waterside-park-pond.jpg,公園の池と周囲の緑が見える。,2026,park
11,sluice-gate-02.jpg,川の水門を別の場所から眺めた風景。,2025,river

このCSVファイルをpandasで読み込み、画像ファイルの本体をバイト列としてimage列に加えます。

CSVファイルと画像からDataFrameを作る
from pathlib import Path
import pandas as pd

df = pd.read_csv("photos.csv")
# ファイル名ごとに画像を読み込み、バイト列をimage列に入れる
df["image"] = [Path("images", name).read_bytes() for name in df.loc[:, "filename"]]
df.drop(columns="image").head(3)  # 画像の列を除いて表示

実行結果は以下のようになります。

id filename text year category
0 0 europython-keynote-01.jpg 大きな会場のステージで登壇者が基調講演をしている。 2025 conference
1 1 europython-talk-01.jpg Pythonの技術について発表者が講演している。 2026 conference
2 2 europython-panel.jpg 複数の登壇者がパネルディスカッションで議論している。 2026 conference

pandasのindexはLanceDBには保存されません。そのため、レコードを識別する値は id 列として明示的に持たせています。

2.2. pandas⁠・Parquetからのデータ登録

作成したDataFrameを、そのままLanceDBのテーブルとして登録します。

DataFrameからLanceDBのテーブルを作成する
import lancedb

db = lancedb.connect("photos.lancedb")  # ① 保存先のディレクトリに接続
table = db.create_table("photos", data=df, mode="overwrite")  # ② LanceDBのテーブルを作成
print(table.count_rows())  # ③ 登録件数とスキーマを確認
table.schema

実行すると、登録件数とスキーマが以下のように表示されます。

登録件数とスキーマ
12
id: int64
filename: large_string
text: large_string
year: int64
category: large_string
image: binary
  1. connect()に指定したディレクトリphotos.lancedbに、テーブルのデータが保存されます。ディレクトリがなければ作成されます
  2. mode="overwrite"は、同じ名前のテーブルがあれば作り直す指定で、セルを繰り返し実行できるようにしています。初回の実行時には、テーブルを新しく作る旨の WARN ログが表示されますが、問題はありません
  3. スキーマを見ると、DataFrameの列がそのままテーブルの列になり、画像は binary 型の列として保存されていることがわかります

筆者は普段、データの処理にはpandasを使っています。Parquetは、データをアーカイブしたいときやファイルサイズを減らしたいときに使うことがあります。こうしたParquetファイルからも、PyArrowで読み込んだ表形式のデータ(pyarrow.Table)をそのまま渡して、LanceDBのテーブルに登録できます。

ParquetファイルからLanceDBのテーブルを作成する
import pyarrow.parquet as pq

df.to_parquet("photos.parquet", index=False)  # 同じデータをParquetに書き出す
from_parquet = db.create_table(  # 読み込んだデータを別のLanceDBのテーブルに登録
    "photos_from_parquet", data=pq.read_table("photos.parquet"), mode="overwrite"
)
from_parquet.count_rows()

登録件数は12件と表示され、pandasから登録した場合と同じです。以降は、pandasから登録した photos テーブルを使います。

2.3. 必要なデータの取得とpandasとの連携

2.2で作ったテーブルは、photos.lancedbディレクトリに保存されています。ここからは、保存したテーブルを開き直して使います。

保存したテーブルを開く
import lancedb

db = lancedb.connect("photos.lancedb")  # 保存先のディレクトリに接続
table = db.open_table("photos")  # 2.2で作ったテーブルを開く

開いたテーブルから、条件と列を指定してデータを取り出します。ここでは、カテゴリがparkの写真について、IDやファイル名、説明文、年を取得します。

条件と列を指定して取得する
df_park = (
    table.search()
    .where("category = 'park'")  # ① 条件で行を絞り込む
    .select(["id", "filename", "text", "year"])  # ② 取り出す列を指定する
    .to_pandas()  # ③ DataFrameとして受け取る
)
df_park

実行結果は以下のようになります。

id filename text year
0 4 waterside-park-ducks.jpg 池の水面で鴨が泳いでいる。 2026
1 5 waterside-park-flock.jpg 公園の水辺に鳥の群れが集まっている。 2025
2 10 waterside-park-pond.jpg 公園の池と周囲の緑が見える。 2026
  1. where()にはSQLの条件式を書きます
  2. select()で取り出す列を指定します。画像の列を指定していないので、結果に画像は含まれません。必要な列だけを読み出せるという、1.2で説明したLanceの特徴をここで使っています
  3. to_pandas()で、結果をDataFrameとして受け取ります

取得した結果は通常のDataFrameなので、そのままpandasで集計できます。

取得した属性をpandasで集計する
df_meta = table.search().select(["category", "year"]).to_pandas()  # 属性の列だけ取得
# カテゴリと年ごとの件数を数える
df_meta.groupby(["category", "year"]).size().unstack(fill_value=0)

実行結果は以下のようになります(行がカテゴリ、列が年です⁠)⁠。

year 2025 2026
category
city 0 1
conference 3 3
park 1 2
river 2 0

集計の結果を見ると、cityの写真は2026年の1枚だけです。この写真の本文と画像を取り出してみます。

cityの写真の本文と画像を取り出す
from io import BytesIO
from PIL import Image

# ① cityの写真の本文と画像を取得する
row = table.search().where("category = 'city'").select(["id", "text", "image"]).to_list()[0]
print(row["id"], row["text"])
img = Image.open(BytesIO(row["image"]))  # ② バイト列から画像を開く
img  # ③ 画像を表示する

実行すると、6 街の建物の向こうに高い塔が見える。 と表示された後に、写真1の画像が表示されます。

  1. 条件に合うのは1件だけなので、to_list() が返す辞書のリストから先頭の要素を使います
  2. image 列には保存前の画像ファイルと同じバイト列が入っているため、BytesIO で包むとPillowで開けます
  3. JupyterLabでは、セルの最後の行に置いた画像が表示されます
写真1 cityの写真として取り出した画像
写真1 cityの写真として取り出した画像

ここまでで、写真・説明文・属性を1つのテーブルに保存し、属性で絞り込んだり、必要な列だけを取り出してpandasで集計したりして、集計で見つけたレコードから画像を取り出せました。ただ、「⁠池の写真」を探すには、カテゴリや年といった属性だけでは足りません。次章では、説明文の内容を手がかりに、同じデータから写真を探します。

3. キーワードと意味によるデータ検索

2章では、カテゴリや年といった属性を条件にしてデータを取り出しました。この章では、説明文の内容を手がかりに、2章と同じデータを探します。語句の一致で探すキーワード検索と、意味の近さで探すベクトル検索の2つを、2章と同じデータから作る検索用のテーブルで比べます。キーワード検索だけならEmbeddingモデルは要りませんが、2つの検索を同じテーブルで比べるため、3.1で先にベクトル検索の準備をします。

3.1. Embeddingモデルの登録と列への関連付け

ベクトル検索には、説明文をベクトルに変換するEmbeddingモデルが必要です。今回は、2026年4月:Pythonによるベクトル検索の基礎と実践 ~Embedding、Vector DB~と同じ多言語モデルの intfloat/multilingual-e5-base を使います。2章の photos テーブルはそのまま残し、同じDataFrameから、ベクトルの列を持つ searchable_photos テーブルを新しく作ります。

LanceDBでは、テキストをベクトルに変換する処理を「Embedding関数」と呼びます。Embedding関数は、名前を付けてレジストリ(LanceDBが管理するEmbedding関数の一覧)に登録して使います。テーブルの列とEmbedding関数を関連付けておくと、データの登録時と検索時のベクトル化をLanceDBに任せられます。

このモデルは、検索対象の文書の先頭に「passage: ⁠」⁠、検索語の先頭に「query: 」を付けた形で学習されており、付けないと性能が下がるとされています(モデルカードのFAQ⁠)⁠。LanceDBに組み込まれているsentence-transformers用のEmbedding関数は、文書と検索語を同じように処理するため、接頭辞を付ける処理を加えたクラスを作り、レジストリに登録します。

接頭辞を付けるEmbedding関数を登録して作成する
from lancedb.embeddings import get_registry, register
from lancedb.embeddings.sentence_transformers import SentenceTransformerEmbeddings


@register("multilingual-e5")  # ① この名前でレジストリに登録
class MultilingualE5(SentenceTransformerEmbeddings):
    name: str = "intfloat/multilingual-e5-base"

    def compute_source_embeddings(self, texts, *args, **kwargs):  # ② 保存する文書用
        texts = self.sanitize_input(texts)
        return self.generate_embeddings(["passage: " + text for text in texts])

    def compute_query_embeddings(self, query, *args, **kwargs):  # ③ 検索語用
        texts = self.sanitize_input(query)
        return self.generate_embeddings(["query: " + text for text in texts])


e5 = get_registry().get("multilingual-e5").create(normalize=True)  # ④ Embedding関数を作成
  1. @register()で、このクラスを"multilingual-e5"という名前でレジストリに登録します
  2. データを登録するときに呼ばれ、説明文の先頭に「passage: 」を付けてベクトル化します。テーブルに保存される説明文は元のままです
  3. 検索するときに呼ばれ、検索語の先頭に「query: 」を付けてベクトル化します
  4. get_registry().get()でレジストリから名前を指定してクラスを取り出し、create()で実際に使うEmbedding関数を作ります。normalize=Trueでベクトルの長さを1にそろえます

次に、直前のセルで作ったEmbedding関数e5を列と関連付けて、LanceDBのテーブルを作ります。ここでは、公式ドキュメントで紹介されている、Pydanticのモデル(LanceModel)でテーブルのスキーマを定義する方法を使います。

Pydanticのモデルでスキーマを定義してテーブルを作る
from lancedb.pydantic import LanceModel, Vector


class Photo(LanceModel):
    id: int
    filename: str
    text: str = e5.SourceField()  # ① ベクトル化する元の列
    year: int
    category: str
    image: bytes
    vector: Vector(e5.ndims()) = e5.VectorField()  # ② ベクトルを入れる列


searchable = db.create_table("searchable_photos", schema=Photo, mode="overwrite")
searchable.add(df)  # ③ vector列を渡さずに登録
print(searchable.count_rows())
searchable.schema.field("vector")

実行結果は以下のようになります。

登録件数とベクトルの列
12
pyarrow.Field<vector: fixed_size_list<item: float>[768]>
  1. SourceField()を指定したtext列が、ベクトル化する元の列になります
  2. VectorField()を指定したvector列に、生成したベクトルが入ります。e5.ndims()はモデルが出力するベクトルの次元数で、今回は768です。このコードでは、e5.ndims() の呼び出し時にモデルを読み込み、キャッシュがなければモデル(約1.1GB)をダウンロードします
  3. 2章と同じDataFrameをadd()で登録します。DataFrameにvector列はありませんが、登録時に説明文から作られます

以下は、直前のテーブル作成の別の書き方です。Pydanticのモデルは必須ではなく、2.2と同じようにDataFrameをそのまま create_table() に渡し、EmbeddingFunctionConfigで列とEmbedding関数を関連付けることもできます。同じ名前のテーブルをmode="overwrite"で作り直すので、どちらか一方を実行すれば十分です。このコードも e5 を使います。

DataFrameを渡してEmbedding関数と列を関連付ける
from lancedb.embeddings import EmbeddingFunctionConfig

searchable = db.create_table(
    "searchable_photos", data=df, mode="overwrite",
    embedding_functions=[  # text列から作ったベクトルをvector列に入れる
        EmbeddingFunctionConfig(source_column="text", vector_column="vector", function=e5)
    ],
)
searchable.schema.field("vector")

実行すると、Pydanticのモデルを使った場合と同じvector列(768次元)が作られます。実は、Pydanticのモデルで指定したSourceField()とVectorField()も、内部ではこのEmbeddingFunctionConfigに変換されています。スキーマを型と一緒に書いて見通しを良くしたい場合はPydanticのモデル、手元のDataFrameからそのまま作りたい場合はEmbeddingFunctionConfig、と使い分けられます。どちらで作っても、以降のコードは同じように動きます。

3.2. 日本語キーワード検索の準備

キーワード検索(全文検索)では、本文を語に区切ってインデックスを作り、検索語と同じ語を含む行を探します。日本語は英語のように空白で語が区切られていないため、語に区切る処理が必要です。LanceDBでは、ICU(International Components for Unicode)のトークナイザーを指定すると、辞書を追加でインストールしなくても日本語を語に区切れます。詳しくは公式ドキュメントの全文検索のトークナイザーの説明を参照してください。

ICUを使った全文検索のインデックスを作る
from lancedb.index import FTS  # FTS: Full-Text Search(全文検索)

# ICUで本文を語に区切り、全文検索のインデックスを作る
searchable.create_index("text", config=FTS(base_tokenizer="icu"), replace=True)

replace=Trueは、インデックスがすでにあれば作り直す指定です。

キーワード検索は語の単位で一致を調べます。たとえば、ICUは「水面」「⁠水辺」「⁠水門」をそれぞれ1つの語として区切るため、「⁠水」で検索してもこれらの説明文は見つかりません。

3.3. キーワード検索とベクトル検索の実践

同じ検索語で、キーワード検索とベクトル検索の結果を比べます。search()に検索語を渡し、query_typeに"fts"(全文検索)か"vector"を指定します。ベクトル検索では、3.1で作ったEmbedding関数が検索語を自動でベクトル化します。なお、今回は12件と少ないため、ベクトル用のインデックスは作らず、全件のベクトルと比べます。

キーワード検索とベクトル検索を比べる
score_columns = {"fts": "_score", "vector": "_distance"}  # 検索方式ごとのスコアの列
for query in ["講演", "水の上を泳ぐ鳥"]:
    for query_type, score in score_columns.items():  # キーワード検索とベクトル検索
        df_found = (
            searchable.search(query, query_type=query_type)
            .select(["id", "text", score])  # スコアの列も明示する
            .limit(3)
            .to_arrow()  # いったんArrowの表に変換してから
            .to_pandas()  # pandasのDataFrameにする
        )
        print(f"{query_type}: {query}")
        display(df_found)

select()には、スコアの列(キーワード検索は_score、ベクトル検索は_distance)も明示的に指定しています。指定しない場合、LanceDB 0.40.0ではスコアの列が自動で加わりますが、将来は加わらなくなる予定で、非推奨の警告が表示されます。また、全文検索でスコアの列を指定するとto_pandas()ではエラーになるため、to_arrow()でいったんArrowの表に変換してから、pandasのDataFrameにしています。

実行結果は以下のようになります。

fts: 講演
id text _score
0 9 基調講演のステージと会場の様子。 1.415293
1 1 Pythonの技術について発表者が講演している。 1.319581
2 0 大きな会場のステージで登壇者が基調講演をしている。 1.162367
vector: 講演
id text _distance
0 9 基調講演のステージと会場の様子。 0.267774
1 0 大きな会場のステージで登壇者が基調講演をしている。 0.281187
2 8 技術イベントの会場で発表を聞いている。 0.310224
fts: 水の上を泳ぐ鳥
id text _score
0 5 公園の水辺に鳥の群れが集まっている。 1.912924
vector: 水の上を泳ぐ鳥
id text _distance
0 4 池の水面で鴨が泳いでいる。 0.286998
1 5 公園の水辺に鳥の群れが集まっている。 0.308927
2 7 川の水門と周囲の風景。 0.376811

_scoreはキーワード検索のスコアで、値が大きいほど一致の度合いが高い結果です。_distanceはベクトルの距離で、値が小さいほど意味が近い結果です。2つは尺度が異なるため、数値どうしを比べることはできません。

「講演」では、キーワード検索は「講演」という語を含む3件を返しました。ベクトル検索の上位2件も、キーワード検索で見つかった写真ですが、3件目には「講演」を含まない「技術イベントの会場で発表を聞いている。」が入っています。語が違っても、意味の近い説明文が見つかっています。

「水の上を泳ぐ鳥」では、違いがよりはっきりします。キーワード検索で見つかったのは、「⁠鳥」という語を含むid=5の1件だけです。一方、ベクトル検索では「池の水面で鴨が泳いでいる。」(⁠id=4)がもっとも近い結果になりました。説明文に「鳥」という語はありませんが、意味の近さで見つけられています。

今回のキーワード検索は、分割後のいずれかの語が一致すると結果を返します。ただし、「⁠鴨」と「鳥」に加え、「⁠泳ぐ」と「泳いでいる」も同じ語には正規化されないため、id=4には一致しません。

検索結果は、2章で扱ったレコードと同じものです。select()にimageを加えれば、2.3と同じ方法で、見つけた写真をそのまま表示できます。

3.4. 属性による絞り込みと検索の組み合わせ

検索には、2章で使った属性の条件を組み合わせられます。ここでは「人前で技術の話をする」でベクトル検索をし、2026年の写真に絞り込みます。

ベクトル検索と年の条件を組み合わせる
df_found = (
    searchable.search("人前で技術の話をする", query_type="vector")
    .where("year = 2026")  # 2026年の写真に絞り込む
    .select(["id", "text", "year", "_distance"])  # スコアの列も明示する
    .limit(3)
    .to_arrow()  # いったんArrowの表に変換してから
    .to_pandas()  # pandasのDataFrameにする
)
df_found

実行結果は以下のようになります。

id text year _distance
0 8 技術イベントの会場で発表を聞いている。 2026 0.278215
1 1 Pythonの技術について発表者が講演している。 2026 0.330055
2 2 複数の登壇者がパネルディスカッションで議論している。 2026 0.360671

結果はすべて2026年の写真で、where() で指定した条件の中から、意味の近い3件が返っています。

ベクトル検索のwhere()は、初期値では検索の前に条件で絞り込みます(prefilter=True⁠)⁠。prefilter=Falseを指定すると、検索した後の結果を絞り込むため、指定した件数より少ない結果しか返らない場合があります。

ここまでで、2章で属性を指定して取り出したのと同じデータに対して、キーワード検索とベクトル検索を使い、属性による絞り込みも組み合わせられました。検索の結果も本文や画像と同じレコードなので、2章と同じようにpandasで扱ったり、画像を表示したりできます。

4. データの保存先と活用の広がり

ここまでは、ローカルのディレクトリにデータを保存してきました。lancedb.connect()には、s3://バケット名/パスのようなオブジェクトストレージのURIも指定できます。S3のほか、Google Cloud Storage(gs://)やAzure Blob Storage(az://)にも対応しており、認証情報は環境変数か storage_options で渡します。接続先を変えても、テーブルの作成や検索は同じAPIで扱えます。

LanceDBでは、2章で試したように必要な列だけを取り出せます。さらに、1.2で紹介したように、Lanceは行単位のランダムアクセスを重視した形式です。ファイルのどの範囲に必要なデータがあるかを特定し、その部分だけを読み出せるように設計されています(Lanceのファイル形式の仕様⁠)⁠。そのため、オブジェクトストレージに置いたデータでも、ファイル全体をダウンロードせずに、必要な列や行の分だけを読み込めます。この特徴が、クラウドストレージと相性の良い点です。

ただし、オブジェクトストレージはローカルのディスクに比べて、読み書きの遅延が大きくなります。保存先ごとの特徴や設定方法は、公式ドキュメントのストレージの説明と設定の説明にまとめられています。

活用の面では、データ分析とWebアプリの両方で、本記事の使い方をそのまま生かせます。データ分析では、2章のように条件や列を指定して必要な範囲だけをDataFrameにし、pandasで集計・可視化できます。Webアプリでは、3章の検索結果に本文や画像を含められるため、見つけた写真を別のデータを探し直さずに画面へ表示できます。

筆者は、ログなどの大きなデータを、S3に置いたParquetとDuckDBで分析してきました。こうしたデータをLanceDBで扱うことも、今後試してみたいと考えています。

5. まとめ

本記事では、写真・説明文・属性情報をLanceDBの1つのテーブルにまとめ、以下の流れで取り出し方を広げました。

登録と取得
pandasのDataFrameやParquetファイルから、画像本体を含むデータを登録しました。条件と列を指定して必要な部分だけを取り出し、pandasでの集計や画像の表示に使いました
検索
E5をテーブルの列と関連付けてベクトルを自動で作り、ICUによる日本語のキーワード検索とベクトル検索を同じデータで比べました。属性による絞り込みも組み合わせられることを確認しました
保存先
必要な部分だけを読み出せるLanceの特徴から、S3などのオブジェクトストレージにも保存先を広げられることを紹介しました

導入で書いたように、どの処理もpandasやParquet、DuckDB、NumPyを組み合わせれば実現できます。それでも、データをひとつのテーブルにまとめておき、列の選択・条件による絞り込み・検索と取り出し方を変えながら同じレコードを使える点に、筆者はLanceDBの便利さを感じています。

まずは、手元のDataFrameをcreate_table()で登録し、必要な列だけを取り出すところから試してみてください。検索は、必要になったときに同じデータから始められます。

参考リンク

來源:gihyo.jp · gihyo.jp