Django ORM で TiDB Serverless に接続する : (2)Vector Search編

に公開

https://zenn.dev/kameoncloud/articles/8634bd75fbd1d8
前回は公式が提供してくれているチュートリアルを使わず最もシンプルな形での接続を行ってみました。

公式提供コンテンツは簡単なゲームが提供されているので是非合わせて参考してみてください。
https://docs.pingcap.com/ja/tidb/stable/dev-guide-sample-application-python-django/

今日はNext StepとしてVector Searchとの連携をやっていきます。ベクトル検索は、類似文書の検索や意味的な検索に強みがあり、生成AIとの連携やセマンティック検索などの分野で用いられる技術です。今回はDjangoアプリケーションから、TiDBのベクトル検索機能を試してみます。
https://docs.pingcap.com/ja/tidb/stable/vector-search-integrate-with-django-orm/
こちらにサンプルが提供されていますのでそれを試します。

さっそくやってみる

環境はWSL Ubuntuを使います。

1. 環境構築

まずはサンプルをgitからCloneします。

git clone https://github.com/pingcap/tidb-vector-python.git

次にPythonの仮想環境を作成します。

cd tidb-vector-python/examples/orm-django-quickstart
python3 -m venv .venv
source .venv/bin/activate

必要な依存関係をインストールします。

pip install -r requirements.txt
インストールログ
Collecting Django==4.2.4 (from -r requirements.txt (line 1))
  Using cached Django-4.2.4-py3-none-any.whl.metadata (4.1 kB)
Collecting django-tidb>=5.0.1 (from -r requirements.txt (line 2))
  Using cached django_tidb-5.2.0-py3-none-any.whl.metadata (9.7 kB)
Collecting mysqlclient==2.2.0 (from -r requirements.txt (line 3))
  Using cached mysqlclient-2.2.0-cp312-cp312-linux_x86_64.whl
Collecting python-dotenv==1.0.0 (from -r requirements.txt (line 4))
  Using cached python_dotenv-1.0.0-py3-none-any.whl.metadata (21 kB)
Collecting tidb-vector>=0.0.9 (from -r requirements.txt (line 5))
  Using cached tidb_vector-0.0.14-py3-none-any.whl.metadata (10 kB)
Collecting asgiref<4,>=3.6.0 (from Django==4.2.4->-r requirements.txt (line 1))
  Using cached asgiref-3.9.0-py3-none-any.whl.metadata (9.3 kB)
Collecting sqlparse>=0.3.1 (from Django==4.2.4->-r requirements.txt (line 1))
  Using cached sqlparse-0.5.3-py3-none-any.whl.metadata (3.9 kB)
Collecting numpy<2,>=1 (from tidb-vector>=0.0.9->-r requirements.txt (line 5))
  Using cached numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (61 kB)
Using cached Django-4.2.4-py3-none-any.whl (8.0 MB)
Using cached python_dotenv-1.0.0-py3-none-any.whl (19 kB)
Using cached django_tidb-5.2.0-py3-none-any.whl (25 kB)
Using cached tidb_vector-0.0.14-py3-none-any.whl (21 kB)
Using cached asgiref-3.9.0-py3-none-any.whl (23 kB)
Using cached numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (18.0 MB)
Using cached sqlparse-0.5.3-py3-none-any.whl (44 kB)
Installing collected packages: sqlparse, python-dotenv, numpy, mysqlclient, django-tidb, asgiref, tidb-vector, Django
Successfully installed Django-4.2.4 asgiref-3.9.0 django-tidb-5.2.0 mysqlclient-2.2.0 numpy-1.26.4 python-dotenv-1.0.0 sqlparse-0.5.3 tidb-vector-0.0.14

2.TiDB Serverless への接続設定

.envを作成します。

.env
TIDB_HOST=gateway01.us-west-2.prod.aws.tidbcloud.com
TIDB_PORT=4000
TIDB_USERNAME=<userid>
TIDB_PASSWORD=<password>
TIDB_DATABASE=test
TIDB_CA_PATH=ca.pem

<userid>,<passowrd>は皆さんの環境ごとに置き換えておきます。TIDB_HOSTもTiDB Serverlessを起動しているリージョンごとに異なりますので適宜書き換えます。
次に以下の内容でca.pemを作成します。

-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----

3.起動

ではスキーマを投入します。
https://zenn.dev/kameoncloud/articles/8634bd75fbd1d8
前回の手順では、models.pysettings.pyを書き換えましたが、今回はすでに準備されているサンプルを使用しますのでgitからcloneしたものは設定済となっているのでmigrateからで実行します。

python manage.py migrate
実行ログ
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sample_project, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sample_project.0001_initial... OK
  Applying sessions.0001_initial... OK

複数のシステムテーブルと1個のユーザー定義テーブルが作成されます。

4.アプリケーションの起動

Djangoは本来Webフレームワークですので、同じ環境でそのままWebアプリケーションが起動可能です。

python manage.py runserver
実行ログ
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
July 06, 2025 - 04:27:32
Django version 4.2.4, using settings 'sample_project.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

5. テストと動作解説

3つのAPIが用意されています。

これは./sample_project/urls.pyで定義されています。

urls.py
from django.urls import path

from . import views

urlpatterns = [
    path("", views.list_routes, name="index"),
    path("insert_documents", views.insert_documents, name="insert_documents"),
    path("get_nearest_neighbors_documents", views.get_nearest_neighbors_documents, name="get_nearest_neighbors_documents"),
    path("get_documents_within_distance", views.get_documents_within_distance, name="get_documents_within_distance"),
]

それぞれの挙動が定義されているのは./sample_project/views.pyです。例えばinsert_documentsであれば以下の通りです。

views.py
def insert_documents(request):
    Document.objects.create(content="dog", embedding=[1, 2, 1])
    Document.objects.create(content="fish", embedding=[1, 2, 4])
    Document.objects.create(content="tree", embedding=[1, 0, 0])

    return HttpResponse("Insert documents successfully.")

3つのベクトルデータを挿入しています。ここで使用しているDocumentクラスは./sample_project/models.pyで定義されています。

models.py
class Document(models.Model):
    content = models.TextField()
    embedding = VectorField(dimensions=3)
    class Meta:
        indexes = [
            VectorIndex(L2Distance("embedding"), name='idx_l2'),
            VectorIndex(CosineDistance("embedding"), name='idx_cos'),
        ]

まずhttp://127.0.0.1:8000/insert_documentsにアクセスするとベクトルデータがインポートされます。

次にhttp://127.0.0.1:8000/get_nearest_neighbors_documentsへアクセスします。
これはviews.pyで以下の様に定義されています。

views.py
def get_nearest_neighbors_documents(request):
    results = Document.objects.annotate(
        distance=CosineDistance('embedding', [1, 2, 3])
    ).order_by('distance')[:3]
    response = []
    for doc in results:
        response.append({
            'distance': doc.distance,
            'document': doc.content
        })

    return JsonResponse(response, safe=False)

このサンプルではコサイン検索を行っていますが、前述のとおりDocumentクラスにはコサイン検索のほかにユークリッド距離(L2Distance)が定義されていますので、そちらを使うこともできます。

TiDB Vector Serchではほかにマンハッタン距離や負のスカラー積を提供しています。
https://zenn.dev/kameoncloud/articles/56e84dce9cd89f
以下の様にmodels.pyを改良すれば利用可能になります。

models.py
class Document(models.Model):
    content = models.TextField()
    embedding = VectorField(dimensions=3)

    class Meta:
        indexes = [
            VectorIndex(L2Distance("embedding"), name='idx_l2'),
            VectorIndex(CosineDistance("embedding"), name='idx_cos'),
            VectorIndex(L1Distance("embedding"), name='idx_l1'),
            VectorIndex(NegativeInnerProduct("embedding"), name='idx_neg_ip'),
        ]

Discussion