Django ORM で TiDB Serverless に接続する : (2)Vector Search編
前回は公式が提供してくれているチュートリアルを使わず最もシンプルな形での接続を行ってみました。
公式提供コンテンツは簡単なゲームが提供されているので是非合わせて参考してみてください。
今日はNext StepとしてVector Searchとの連携をやっていきます。ベクトル検索は、類似文書の検索や意味的な検索に強みがあり、生成AIとの連携やセマンティック検索などの分野で用いられる技術です。今回はDjangoアプリケーションから、TiDBのベクトル検索機能を試してみます。 こちらにサンプルが提供されていますのでそれを試します。
さっそくやってみる
環境は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を作成します。
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.起動
ではスキーマを投入します。
前回の手順では、models.pyやsettings.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で定義されています。
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であれば以下の通りです。
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で定義されています。
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で以下の様に定義されています。
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ではほかにマンハッタン距離や負のスカラー積を提供しています。
以下の様に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