跳至主要内容

Oracle AI 向量搜索教程:使用 Python 存储与查询嵌入向量

本教程介绍 Oracle AI 向量搜索,解释其核心功能,并带您完成一个实用的本地项目。同时涵盖搭建要求、语义搜索、向量索引与常见问题排查。
· 15分钟 阅读

使用 AI 探索

在本教程中,我们将使用 Oracle AI 向量搜索、Python 和 Oracle Database Free 26ai 构建一个本地语义搜索演示。

我们先从手写的三维向量开始,便于直观看到距离计算;然后生成文本嵌入向量,并通过 SQL 从 Oracle 数据库中查询它们。

本教程面向对向量搜索陌生、但熟悉 Docker、环境变量、Python 包以及本地数据库搭建的 Python 与 SQL 开发者。

AI 驱动的应用需要能理解“含义”的搜索,而不仅是精确的词面匹配。无论是支持应用、文档门户,还是内部知识工具,都应能在最佳匹配文档写着“将向量嵌入存入原生列”的情况下,找到“AI 搜索的数据库存储”。

向量嵌入通过将文本表示为数值向量,使这一点成为可能。Oracle AI 向量搜索让我们可以将这些向量直接存入 Oracle 数据库,用 SQL 查询它们,并将嵌入与关系型应用数据并存。

在这个本地流程中,我们不需要单独的向量数据库。

我们将在本地运行 Oracle Database Free 26ai,从 Python 通过 oracledb Thin 模式连接,先存储手工向量,再存储 OpenAI 嵌入向量,运行语义搜索,用简单的短语精确匹配对比语义检索,并创建一个向量索引,通过 USER_INDEXES 进行验证。

最终,我们将获得一个可运行的、由 Python 驱动的语义搜索流程:在 Oracle 数据库中存储嵌入向量,并在本地检索最相似的 documents。

什么是 Oracle AI 向量搜索?

Oracle AI 向量搜索是一组 Oracle 数据库能力,用于存储、索引和查询向量嵌入。向量嵌入是一个固定长度的数值列表,以数据库可进行数学比较的形式,表示文本、图像或其他数据的含义。

嵌入模型负责生成数值向量。它接收输入(如句子、段落、图像描述或代码片段),并返回一个维度固定的向量。

相似的输入应产生彼此接近的向量,而不相关的输入应相距更远。因此,我们必须对存储的文档与传入查询使用同一个嵌入模型,因为只有当向量共享同一坐标系时,距离才有意义。

在本教程中,手工向量刻意设置得很小,便于检查数学计算。真实的文本嵌入要大得多,因为模型需要足够的维度来编码词语、短语与主题之间更细微的关系。

向量搜索按向量之间的距离对行进行排序。对于欧氏距离和余弦距离查询,距离值越小表示匹配越近。在本教程中,我们将先使用一个小的 VECTOR(3, FLOAT32) 列,直接观察距离行为,然后转向从文本生成的 VECTOR(1536, FLOAT32) 嵌入。

Oracle AI Vector Search flow

图 1. Oracle AI 向量搜索流程

Oracle AI 向量搜索将嵌入存储在原生的VECTOR 列中,并按向量距离对结果进行排序。

当嵌入需要与关系型应用数据(如文档 ID、标题、内容、用户、产品、工单或元数据)并存时,Oracle 数据库非常有用。

一些应用会为特殊检索负载使用单独的向量数据库或搜索引擎,但对于本教程的本地语义搜索流程,Oracle 数据库就能提供我们所需的向量存储与搜索功能。

Oracle AI 向量搜索的关键特性

在演示中我们将用到四项功能:原生向量存储、距离函数、向量索引,以及与关系查询的集成。

原生向量数据类型

Oracle 数据库支持原生 VECTOR 列,并能明确维度与元素格式。在演示中,我们将对手工向量使用 VECTOR(3, FLOAT32),对默认的 text-embedding-3-small 嵌入模型使用 VECTOR(1536, FLOAT32)。

距离函数与运算符

可运行示例使用 VECTOR_DISTANCE(),因为它能在 SQL 中显式保留距离度量方式。

我们将在手工向量示例中使用欧氏距离,在文本嵌入中使用余弦距离。

Oracle 数据库也包含向量距离的简写运算符,以及与点积相关的距离行为,但本教程将保持在显式的 VECTOR_DISTANCE() 路径上,便于直接运行 SQL。

不同的距离度量在“接近度”的含义上回答的问题略有不同。

在小型示例场景下,余弦距离是文本嵌入的良好默认选择,因为它关注方向,通常更契合语义相似性。

欧氏距离更易于用手工向量进行可视化;当向量幅度或归一化是模型设计的一部分时,点积式比较在一些检索系统中也很有用。

度量

比较的内容

适用场景

注意事项

余弦

向量方向

文本嵌入与语义相似性

结果依赖于使用同一嵌入模型与一致的预处理

欧氏

直线距离

小型示例、几何直觉、部分数值特征向量

幅度会影响距离,因而缩放可能很重要

点积

方向与幅度共同作用

围绕归一化向量或内积打分设计的工作流

在比较结果前需要理解分值方向与归一化规则

向量索引

精确向量搜索对正确性与小数据集很有用。随着数据集增长,向量索引可支持近似最近邻搜索工作流。

Oracle AI 向量搜索包含内存邻接图索引(常与 HNSW 风格搜索相关)与邻接分区索引(常与 IVF 风格分区相关)。

在本地演示中,我们使用 ORGANIZATION NEIGHBOR PARTITIONS,并通过 USER_INDEXES 验证索引。

近似索引以牺牲少量穷举搜索的确定性,换取规模化下更快的检索。

合适的索引家族取决于工作负载、数据规模、更新模式与召回目标。

本教程选择 IVF 风格的邻接分区索引,因为它使本地搭建更简单,并提供具体的索引创建步骤,而不会把教程变成索引调优指南。

索引家族

基本思想

优势

取舍

可能的使用场景

IVF / 邻接分区

对向量空间进行分区,再搜索可能的分区

心智模型简单,适用于许多批量加载的数据集

召回与速度取决于分区与搜索设置

我们希望为不断增长的数据集提供直接的近似搜索路径

HNSW / 邻接图

构建连接近邻向量的图,以便快速遍历

对许多最近邻工作负载有较强的召回与延迟表现

更偏内存,且配置影响更大

我们需要低延迟搜索,并能承担内存与调优投入

与关系查询集成

向量列可以与普通的关系列并存。我们的documents 表会同时存储标题、内容与嵌入向量,因此 SQL 可以同时返回相似度分数与原始文本文档。

如何开始使用 Oracle AI 向量搜索

我们将采用一种本地搭建路径:使用 Docker 中的 Oracle Database Free 26ai、FREEPDB1 服务,以及一个专用的 vector_demo 应用用户。

向量搜索概念本身较易上手,但搭建过程属于中级难度,因为我们要用到 Docker、数据库模式、Python 包、环境变量以及嵌入 API 密钥。

先决条件

我们需要:

  • Docker Desktop 或 Docker Engine。
  • 访问 Oracle Container Registry 的权限。
  • 本地 1521 和 5500 端口可用。
  • Python 3.10 或更高版本。
  • 基础的 Python 与 SQL 知识。
  • 对 Docker 与环境变量的基本熟悉。
  • 用于嵌入步骤的 OpenAI API 密钥。

本演示需要包含此处所用向量功能的 Oracle 数据库版本或环境,包括原生 VECTOR 列、VECTOR_DISTANCE() 与 CREATE VECTOR INDEX。

本地路径使用下方镜像标签的 Oracle Database Free 26ai,且 DSN 使用 FREEPDB1 可插拔数据库服务。

手工向量步骤不需要 OpenAI API 密钥。语义步骤会将示例文档文本与查询文本发送给配置的嵌入提供商,因此我们只使用示例或非敏感文本。

本教程使用 oracledb,即当前的 Oracle 数据库 Python 驱动。python-oracledb 默认以 Thin 模式运行,因此本地演示无需 Oracle Client 库。Thick 模式在其他部署中很有用,但此处不会调用 oracledb.init_oracle_client()。

在本地启动 Oracle Database Free 26ai

为我们将要编写的脚本创建一个项目目录。

mkdir oracle-ai-vector-search-python
 cd oracle-ai-vector-search-python

使用本地容器镜像启动 Oracle Database Free 26ai。

docker run --name oracle-free-26ai-vector \
   --detach \
   --publish 1521:1521 \
   --publish 5500:5500 \
   --shm-size=1g \
   --env ORACLE_PWD="replace-with-a-strong-password" \
   container-registry.oracle.com/database/free:23.26.1.0-lite-amd64

检查容器日志,并等待数据库报告启动完成。

docker logs oracle-free-26ai-vector

FREEPDB1 是本地 Oracle Database Free 容器路径所用的可插拔数据库服务。我们的 Python DSN 将是 localhost:1521/FREEPDB1。

在 FREEPDB1 中创建 vector_demo 模式

仅在模式搭建步骤中使用 SYS。sqlplus 命令中的密码为 docker run 命令的 ORACLE_PWD 值;CREATE USER 中的密码是应用密码,我们将导出为 DB_PASSWORD。

在数据库容器内运行设置。

# Create the application user and grant the privileges needed for the demo.
 docker exec -i oracle-free-26ai-vector \
   sqlplus -s 'sys/"replace-with-oracle-pwd"@FREEPDB1 as sysdba' <<'SQL'
 WHENEVER SQLERROR EXIT SQL.SQLCODE

 CREATE USER vector_demo IDENTIFIED BY "replace-with-vector-demo-password";

 GRANT CREATE SESSION, CREATE TABLE, CREATE SEQUENCE, UNLIMITED TABLESPACE TO vector_demo;

 EXIT
 SQL

这是演示容器的本地开发捷径。由于表使用了标识列,因此需要 CREATE SEQUENCE。

在生产环境中,我们会与数据库管理员一起审查权限,并根据具体的应用工作负载应用最小权限原则。

安装 Python 依赖

创建并激活虚拟环境。

python -m venv .venv
 source .venv/bin/activate
 python -m pip install --upgrade pip

安装演示所用的 Python 包。

python -m pip install oracledb openai

设置环境变量

将数据库凭据和嵌入配置存入环境变量,而不是在 Python 文件中硬编码。

export DB_USER="vector_demo"
 export DB_PASSWORD="replace-with-vector-demo-password"
 export DB_DSN="localhost:1521/FREEPDB1"
 export OPENAI_API_KEY="replace-with-your-api-key"
 export EMBEDDING_MODEL="text-embedding-3-small"
 export EMBEDDING_DIM="1536"

OPENAI_API_KEY 仅在嵌入与语义搜索步骤中需要。EMBEDDING_DIM 必须与所选嵌入模型返回的维度数量一致。

Oracle AI 向量搜索演示项目

我们将通过八个检查点构建演示。每个脚本引入一个概念,并产生可在继续前验证的输出。

仅数据库部分在步骤 1–3 完成。届时,我们已连接到 Oracle 数据库、存储手工向量、按距离查询它们,并创建语义表。OpenAI API 密钥从第 4 步开始需要。

步骤 1:连接到本地数据库

首先,我们要验证 Python 能以 vector_demo 应用用户身份连接到 Oracle 数据库。该脚本从环境中读取 DB_USER, DB_PASSWORD 与 DB_DSN,并打印驱动模式与数据库版本。

创建脚本

将以下代码保存为 01_check_connection.py。

import os

 import oracledb

 ADMIN_USERS = {"SYS", "SYSTEM", "PDBADMIN"}


 def required_env(name):
 	value = os.getenv(name)
 	if not value:
         raise SystemExit(f"Set {name} before running this script.")
 	return value


 def main():
 	# Load required database settings from environment variables.
 	user = required_env("DB_USER")
     password = required_env("DB_PASSWORD")
 	dsn = required_env("DB_DSN")

 	if user.upper() in ADMIN_USERS:
         raise SystemExit(
             "Use the vector_demo application user, not an admin user."
     	)

 	# Open a Thin-mode connection with the application user.
 	with oracledb.connect(user=user, password=password, dsn=dsn) as connection:
         mode = "thin" if oracledb.is_thin_mode() else "thick"
         print("Connected to Oracle Database.")
         print(f"Driver mode: {mode}")
         print(f"Database version: {connection.version}")


 if __name__ == "__main__":
 	main()

运行脚本

运行连接检查。

python 01_check_connection.py

预期输出

输出应确认连接成功,且驱动处于 Thin 模式。

Connected to Oracle Database.
 Driver mode: thin
 Database version: 23.26.1.0.0

在创建任何向量表之前,我们已确认本地数据库、服务名、凭据与 Python 驱动均正常工作。

步骤 2:用手工向量建立直觉

在引入嵌入模型之前,先存储三个便于推理的小向量。array.array("f", values) 会创建一个 32 位浮点数组,python-oracledb 可将其绑定到 VECTOR(..., FLOAT32) 列中。

Three-dimensional apple, banana, and car vectors showing Euclidean distance from the apple query vector.

图 2. 手工向量与欧氏距离。

手工示例使用欧氏距离。apple 向量与查询向量相同,距离为 0.0000。banana 向量第二名,因为 sqrt((1.0 - 0.9)^2 + (0.0 - 0.1)^2 + (0.0 - 0.0)^2) = 0.1414。

创建脚本

将以下代码保存为 02_manual_vectors.py。

import array
 import os

 import oracledb

 ADMIN_USERS = {"SYS", "SYSTEM", "PDBADMIN"}


 def required_env(name):
 	value = os.getenv(name)
 	if not value:
         raise SystemExit(f"Set {name} before running this script.")
 	return value


 def main():
 	user = required_env("DB_USER")
 	if user.upper() in ADMIN_USERS:
         raise SystemExit(
             "Use the vector_demo application user, not an admin user."
     	)

 	# Define three small vectors so the distance calculation is easy to inspect.
 	rows = [
         ("apple", array.array("f", [1.0, 0.0, 0.0])),
         ("banana", array.array("f", [0.9, 0.1, 0.0])),
         ("car", array.array("f", [0.0, 1.0, 0.0])),
 	]

 	with oracledb.connect(
         user=user,
         password=required_env("DB_PASSWORD"),
         dsn=required_env("DB_DSN"),
 	) as connection:
         with connection.cursor() as cursor:
             # Recreate the table so the script can be run repeatedly.
             cursor.execute(
                 """
                 BEGIN
                 	EXECUTE IMMEDIATE 'DROP TABLE manual_vectors PURGE';
                 EXCEPTION
                 	WHEN OTHERS THEN
                     	IF SQLCODE <> -942 THEN
                         	RAISE;
                     	END IF;
                 END;
                 """
             )
             cursor.execute(
                 """
                 CREATE TABLE manual_vectors (
                 	id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
                 	label VARCHAR2(100),
                 	embedding VECTOR(3, FLOAT32)
                 )
                 """
             )
             # Bind each vector as a 32-bit floating-point array.
             cursor.executemany(
                 """
                 INSERT INTO manual_vectors (label, embedding)
                 VALUES (:1, :2)
                 """,
                 rows,
             )
             connection.commit()

             # Rank rows by Euclidean distance from the query vector.
             cursor.execute(
                 """
                 SELECT label,
                    	VECTOR_DISTANCE(
                        	embedding,
                        	:query_vector,
                        	EUCLIDEAN
                    	) AS distance
                 FROM manual_vectors
                 ORDER BY distance
                 FETCH EXACT FIRST 2 ROWS ONLY
                 """,
                 query_vector=array.array("f", [1.0, 0.0, 0.0]),
             )

             print("Created manual_vectors.")
             print("Inserted 3 manual vectors.")
             print("\nNearest manual vectors:")
             for rank, (label, distance) in enumerate(cursor, start=1):
                 print(f"{rank}. {label:<6} distance={float(distance):.4f}")


 if __name__ == "__main__":
 	main()

运行脚本

运行手工向量脚本。

python 02_manual_vectors.py

预期输出

与查询向量相同的向量会以距离 0.0000 排在首位,邻近的 banana 向量第二位。

Created manual_vectors.
 Inserted 3 manual vectors.

 Nearest manual vectors:
 1. apple  distance=0.0000
 2. banana distance=0.1414

这就是向量搜索的核心思想:行按与查询向量的数学接近程度排序。嵌入模型会生成更大的向量,但数据库行为一致。

步骤 3:创建语义文档表

既然我们知道向量可用,就创建一个真实的应用风格表。关键在于向量列的维度必须与嵌入模型的输出维度一致。

维度是表契约的一部分。如果一个嵌入模型返回 1,536 个数值,列就必须声明为 VECTOR(1536, FLOAT32)。

如果之后切换到输出长度不同的模型,我们需要重建或迁移该向量列,才能让插入与查询继续使用兼容的向量。

由于 DDL 不能将向量维度作为常规 SQL 绑定变量,脚本会将 EMBEDDING_DIM 转换为整数后,再用于 CREATE TABLE 语句。

创建脚本

将以下代码保存为 03_create_documents_table.py。

import os

 import oracledb

 ADMIN_USERS = {"SYS", "SYSTEM", "PDBADMIN"}


 def required_env(name):
 	value = os.getenv(name)
 	if not value:
         raise SystemExit(f"Set {name} before running this script.")
 	return value


 def configured_dimension():
 	try:
         dimension = int(required_env("EMBEDDING_DIM"))
 	except ValueError as exc:
         raise SystemExit("EMBEDDING_DIM must be an integer.") from exc

 	if dimension <= 0:
         raise SystemExit("EMBEDDING_DIM must be a positive integer.")

 	return dimension


 def main():
 	user = required_env("DB_USER")
 	if user.upper() in ADMIN_USERS:
         raise SystemExit(
             "Use the vector_demo application user, not an admin user."
     	)

 	# Validate the configured model name and vector dimension.
     model_name = required_env("EMBEDDING_MODEL")
     dimension = configured_dimension()

 	with oracledb.connect(
         user=user,
         password=required_env("DB_PASSWORD"),
         dsn=required_env("DB_DSN"),
 	) as connection:
         with connection.cursor() as cursor:
             # Recreate the documents table so the script is repeatable.
             cursor.execute(
                 """
                 BEGIN
                 	EXECUTE IMMEDIATE 'DROP TABLE documents PURGE';
                 EXCEPTION
                 	WHEN OTHERS THEN
                     	IF SQLCODE <> -942 THEN
                         	RAISE;
                     	END IF;
                 END;
                 """
             )
             cursor.execute(
                 f"""
                 CREATE TABLE documents (
                 	id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
                 	title VARCHAR2(200) NOT NULL,
                 	content VARCHAR2(1000) NOT NULL,
                 	embedding VECTOR({dimension}, FLOAT32)
                 )
                 """
             )

     print(f"Embedding model: {model_name}")
     print(f"Embedding dimension: {dimension}")
     print("Recreated documents table.")


 if __name__ == "__main__":
 	main()

运行脚本

运行建表脚本。

python 03_create_documents_table.py

预期输出

输出应显示配置的嵌入模型与向量维度。

Embedding model: text-embedding-3-small
 Embedding dimension: 1536
 Recreated documents table.

我们已完成仅数据库部分。Oracle 数据库可以在本地存储并比较向量;接下来,我们将用从文本生成的嵌入取代手写向量。

步骤 4:验证嵌入模型维度

在插入嵌入前,我们先向嵌入模型请求一个向量,并验证其长度。这能避免最常见的向量表错误:插入的向量长度与 VECTOR 列维度不匹配。

嵌入模型不仅是“文本到数字”的转换器;它定义了应用的语义空间。本演示中的所有文档与查询嵌入都必须来自同一模型。

使用单一模型可确保小距离代表相近含义,而不是两个无关向量恰巧形状相似。模型也决定向量长度,这就是我们在加载数据前要检查返回维度的原因。

创建脚本

将以下代码保存为 04_check_embedding_dimension.py。

import os

 from openai import OpenAI


 def required_env(name):
 	value = os.getenv(name)
 	if not value:
         raise SystemExit(f"Set {name} before running this script.")
 	return value


 def configured_dimension():
 	try:
         return int(required_env("EMBEDDING_DIM"))
 	except ValueError as exc:
         raise SystemExit("EMBEDDING_DIM must be an integer.") from exc


 def main():
     required_env("OPENAI_API_KEY")

     model_name = required_env("EMBEDDING_MODEL")
     expected_dimension = configured_dimension()

 	# Request one embedding to confirm the model output dimension.
 	client = OpenAI()
     response = client.embeddings.create(
         model=model_name,
         input="Oracle AI Vector Search dimension check",
 	)

     returned_dimension = len(response.data[0].embedding)

     print(f"Embedding model: {model_name}")
     print(f"Returned embedding length: {returned_dimension}")
     print(f"Configured EMBEDDING_DIM: {expected_dimension}")

 	# Stop before loading data if the table dimension is incompatible.
 	if returned_dimension != expected_dimension:
         raise SystemExit(
             "Dimension check failed. Update EMBEDDING_DIM, rerun "
             "03_create_documents_table.py, and try again."
     	)

     print("Dimension check passed.")


 if __name__ == "__main__":
 	main()

运行脚本

运行维度检查。

python 04_check_embedding_dimension.py

预期输出

对于默认模型与维度,输出应类似如下。

Embedding model: text-embedding-3-small
 Returned embedding length: 1536
 Configured EMBEDDING_DIM: 1536
 Dimension check passed.

若之后选择不同的嵌入模型,应更新 EMBEDDING_MODEL,将 EMBEDDING_DIM 设为返回向量长度,并重建 documents 表。

步骤 5:生成并插入文档嵌入

现在我们为一个小型自包含数据集生成嵌入,并将其插入 Oracle 数据库。

每个嵌入在绑定前会转换为 array.array("f", embedding),以匹配 FLOAT32 向量列。

创建脚本

将以下代码保存为 05_insert_embeddings.py。

import array
 import os

 import oracledb
 from openai import OpenAI

 DOCUMENTS = [
 	(
         "Oracle AI Vector Search",
         "Oracle Database can store vector embeddings in native VECTOR "
         "columns and query them with SQL similarity search.",
 	),
 	(
         "Semantic Search for Applications",
         "Semantic search ranks results by meaning, helping applications "
         "find related content even when exact words differ.",
 	),
 	(
         "Vector Index Workflow",
         "Vector indexes help approximate nearest-neighbor search scale "
         "when datasets grow beyond small examples.",
 	),
 	(
         "Local Docker Development",
         "Docker containers make it practical to run a local database for "
         "development and repeatable tutorials.",
 	),
 	(
         "Python Database Code",
         "Python applications can connect to Oracle Database with the "
         "oracledb driver and use environment variables for credentials.",
 	),
 	(
         "Acoustic Guitar Practice",
         "Daily guitar practice improves timing, chord transitions, and "
         "confidence when learning new songs.",
 	),
 	(
         "Weeknight Vegetable Soup",
         "A simple soup can combine onions, carrots, beans, herbs, and "
         "broth for an easy weeknight meal.",
 	),
 ]

 ADMIN_USERS = {"SYS", "SYSTEM", "PDBADMIN"}


 def required_env(name):
 	value = os.getenv(name)
 	if not value:
         raise SystemExit(f"Set {name} before running this script.")
 	return value


 def configured_dimension():
 	try:
         return int(required_env("EMBEDDING_DIM"))
 	except ValueError as exc:
         raise SystemExit("EMBEDDING_DIM must be an integer.") from exc


 def main():
 	user = required_env("DB_USER")
 	if user.upper() in ADMIN_USERS:
         raise SystemExit(
             "Use the vector_demo application user, not an admin user."
     	)

     required_env("OPENAI_API_KEY")
     model_name = required_env("EMBEDDING_MODEL")
     expected_dimension = configured_dimension()

 	# Generate embeddings for all sample documents in one API request.
 	client = OpenAI()
     response = client.embeddings.create(
         model=model_name,
         input=[content for _, content in DOCUMENTS],
 	)

 	# Restore the response order using each item's index.
     embeddings = [
         item.embedding
     	for item in sorted(
             response.data,
             key=lambda item: item.index,
     	)
 	]

 	if any(len(embedding) != expected_dimension for embedding in embeddings):
         raise SystemExit(
             "An embedding length did not match EMBEDDING_DIM. "
             "Rerun 04_check_embedding_dimension.py."
     	)

 	# Bind each embedding as a 32-bit floating-point array.
 	rows = [
         (title, content, array.array("f", embedding))
     	for (title, content), embedding in zip(DOCUMENTS, embeddings)
 	]

 	# Replace the sample rows and commit the transaction.
 	with oracledb.connect(
         user=user,
         password=required_env("DB_PASSWORD"),
         dsn=required_env("DB_DSN"),
 	) as connection:
         with connection.cursor() as cursor:
             cursor.execute("DELETE FROM documents")
             cursor.executemany(
                 """
                 INSERT INTO documents (title, content, embedding)
                 VALUES (:1, :2, :3)
                 """,
                 rows,
             )
             cursor.execute("SELECT COUNT(*) FROM documents")
             row_count = cursor.fetchone()[0]
             connection.commit()

     print(f"Embedding model: {model_name}")
     print(f"Generated embeddings for {len(DOCUMENTS)} documents.")
     print(f"Inserted {len(DOCUMENTS)} documents.")
     print(f"documents row count: {row_count}")


 if __name__ == "__main__":
 	main()

运行脚本

运行插入脚本。

python 05_insert_embeddings.py

预期输出

行数应与内联的七个 documents 相匹配。

Embedding model: text-embedding-3-small
 Generated embeddings for 7 documents.
 Inserted 7 documents.
 documents row count: 7

现在我们已在 Oracle 数据库中将文本与嵌入向量存储在一起。包含与主题无关的吉他与汤的 documents 也被加入,以便搜索结果的近与远匹配更为明显。

步骤 6:运行语义搜索查询

接下来,我们将对自然语言查询进行嵌入,并与存储的文档嵌入进行比较。该脚本使用余弦距离,它常用于文本嵌入,因为它关注向量方向而非原始幅度。

对于语义搜索,查询需要经过与文档 相同的嵌入模型。我们并不是让 SQL 直接理解英文句子。

我们是让嵌入模型将句子转换为向量,再让 Oracle 数据库按与该查询向量的距离对存储向量进行排序。

创建脚本

将以下代码保存为 06_semantic_search.py。

import array
 import os

 import oracledb
 from openai import OpenAI

 QUERY_TEXT = "Where should an AI app keep meaning-based search data?"
 ADMIN_USERS = {"SYS", "SYSTEM", "PDBADMIN"}


 def required_env(name):
 	value = os.getenv(name)
 	if not value:
         raise SystemExit(f"Set {name} before running this script.")
 	return value


 def main():
 	user = required_env("DB_USER")
 	if user.upper() in ADMIN_USERS:
         raise SystemExit(
             "Use the vector_demo application user, not an admin user."
     	)

     required_env("OPENAI_API_KEY")

 	# Generate an embedding for the natural-language query.
 	client = OpenAI()
     response = client.embeddings.create(
         model=required_env("EMBEDDING_MODEL"),
         input=QUERY_TEXT,
 	)
     query_vector = array.array("f", response.data[0].embedding)

 	with oracledb.connect(
         user=user,
         password=required_env("DB_PASSWORD"),
         dsn=required_env("DB_DSN"),
 	) as connection:
         with connection.cursor() as cursor:
             # Rank stored documents by cosine distance from the query vector.
             cursor.execute(
                 """
                 SELECT title,
                    	content,
                    	VECTOR_DISTANCE(
                        	embedding,
                        	:query_vector,
                        	COSINE
                    	) AS distance
                 FROM documents
                 ORDER BY distance
                 FETCH EXACT FIRST 5 ROWS ONLY
                 """,
                 query_vector=query_vector,
             )
             rows = cursor.fetchall()

 	if not rows:
         raise SystemExit("No documents found. Run 05_insert_embeddings.py first.")

     print(f"Query: {QUERY_TEXT}")
     print("\nTop 5 results:")

 	for rank, (title, content, distance) in enumerate(rows, start=1):
         print(f"{rank}. {title}")
         print(f"   distance: {float(distance):.6f}")
         print(f"   {content[:90]}...")


 if __name__ == "__main__":
 	main()

运行脚本

运行语义搜索脚本。

python 06_semantic_search.py

预期输出

当嵌入模型发生变化时,精确排名与距离可能不同。然而,与 Oracle AI 向量搜索、语义搜索、Python 数据库代码、向量索引相关的 documents 应该排在前列。

Query: Where should an AI app keep meaning-based search data?

 Top 5 results:
 1. Oracle AI Vector Search
    distance: <cosine distance from your run>
    Oracle Database can store vector embeddings in native VECTOR columns and query them with SQL...

 2. Semantic Search for Applications
    distance: <cosine distance from your run>
    Semantic search ranks results by meaning, helping applications find related content even when...

 3. Python Database Code
    distance: <cosine distance from your run>
    Python applications can connect to Oracle Database with the oracledb driver and use environment...

 4. Vector Index Workflow
    distance: <cosine distance from your run>
    Vector indexes help approximate nearest-neighbor search scale when datasets grow beyond small...

 5. Local Docker Development
    distance: <cosine distance from your run>
    Docker containers make it practical to run a local database for development and repeatable...

在本教程中的欧氏与余弦距离查询里,数值越小表示匹配越近。

点积是许多向量搜索工作流中另一种有用的向量比较方法,尤其在幅度或归一化向量重要时,但分值方向与归一化需要谨慎处理。

我们将继续在可运行代码中使用欧氏与余弦距离,以保持结果解释的简明。

若在未来的应用中自行对嵌入做归一化,需要对存储的文档嵌入与查询嵌入一并、且一致地归一化;否则距离与排名会发生变化。

步骤 7:将语义搜索与短语匹配对比

语义搜索的价值在于它不要求文档中出现完全相同的短语。

让我们将语义查询与一个简单的精确短语谓词进行对比。

这不是一个全文搜索引擎演示;它是一个小型 SQL 对比,旨在直观展示字面匹配与基于含义的排序之间的差异。

创建脚本

将以下代码保存为 07_semantic_vs_keyword.py。

import array
 import os

 import oracledb
 from openai import OpenAI

 SEMANTIC_QUERY = "Where should an AI app keep meaning-based search data?"
 KEYWORD_PHRASE = "meaning-based search data"
 ADMIN_USERS = {"SYS", "SYSTEM", "PDBADMIN"}


 def required_env(name):
 	value = os.getenv(name)
 	if not value:
         raise SystemExit(f"Set {name} before running this script.")
 	return value


 def main():
 	user = required_env("DB_USER")
 	if user.upper() in ADMIN_USERS:
         raise SystemExit(
             "Use the vector_demo application user, not an admin user."
     	)

     required_env("OPENAI_API_KEY")

 	# Generate an embedding for the semantic query.
 	client = OpenAI()
     response = client.embeddings.create(
         model=required_env("EMBEDDING_MODEL"),
         input=SEMANTIC_QUERY,
 	)
     query_vector = array.array("f", response.data[0].embedding)

 	with oracledb.connect(
         user=user,
         password=required_env("DB_PASSWORD"),
         dsn=required_env("DB_DSN"),
 	) as connection:
         with connection.cursor() as cursor:
             # Rank documents by semantic similarity.
             cursor.execute(
                 """
                 SELECT title,
                        VECTOR_DISTANCE(embedding, :query_vector, COSINE)
                 FROM documents
                 ORDER BY VECTOR_DISTANCE(embedding, :query_vector, COSINE)
                 FETCH EXACT FIRST 3 ROWS ONLY
                 """,
                 query_vector=query_vector,
             )
             semantic_rows = cursor.fetchall()

             # Compare semantic results with an exact phrase predicate.
             cursor.execute(
                 """
                 SELECT title
                 FROM documents
                 WHERE LOWER(title || ' ' || content)
                   	LIKE '%' || :phrase || '%'
                 ORDER BY title
                 FETCH FIRST 3 ROWS ONLY
                 """,
                 phrase=KEYWORD_PHRASE.lower(),
             )
             keyword_rows = cursor.fetchall()

     print(f"Semantic query: {SEMANTIC_QUERY}")
     print(f"Keyword phrase: {KEYWORD_PHRASE}")

     print("\nSemantic results:")
 	for rank, (title, distance) in enumerate(semantic_rows, start=1):
         print(f"{rank}. {title}")
         print(f"   distance: {float(distance):.6f}")

     print("\nKeyword results:")
 	if keyword_rows:
     	for rank, (title,) in enumerate(keyword_rows, start=1):
             print(f"{rank}. {title}")
 	else:
         print("0 exact phrase matches")


 if __name__ == "__main__":
 	main()

运行脚本

运行对比脚本。

python 07_semantic_vs_keyword.py

预期输出

即便没有出现精确关键短语,语义搜索也应返回相关的 documents。

Semantic query: Where should an AI app keep meaning-based search data?
 Keyword phrase: meaning-based search data

 Semantic results:
 1. Oracle AI Vector Search
    distance: <cosine distance from your run>
 2. Semantic Search for Applications
    distance: <cosine distance from your run>
 3. Python Database Code
    distance: <cosine distance from your run>

 Keyword results:
 0 exact phrase matches

传统 SQL 谓词在过滤、连接、权限与精确匹配方面仍然重要。当应用需要语义检索时,向量搜索提供可与关系数据结合的基于含义的排序信号。

步骤 8:添加并验证向量索引

最后,我们将创建一个向量索引,并验证 Oracle 数据库报告其为由演示模式拥有的有效索引。

此步骤展示索引创建流程;我们的小数据集不足以得出有意义的性能结论。

没有索引时,Oracle 数据库可以将查询向量与每个存储向量逐一精确比较。这种方式适用于很小的数据集,并有助于学习。

在更大的数据集上,近似向量索引会缩小搜索空间,让查询能够快速返回,同时仍能找到近邻。

在选择索引家族前,应综合考虑数据规模、延迟目标、召回要求、内存预算,以及向量变更频率。

最后的查询会确认在创建索引后,语义搜索仍能返回结果。

本教程不使用 EXPLAIN PLAN 或 DBMS_XPLAN,因为我们通过模式元数据验证索引创建,而非通过执行计划。

创建脚本

将以下代码保存为 08_create_vector_index.py。

import array
 import os

 import oracledb
 from openai import OpenAI

 INDEX_NAME = "DOCUMENT_EMBEDDING_IDX"
 QUERY_TEXT = "Where should an AI app keep meaning-based search data?"
 ADMIN_USERS = {"SYS", "SYSTEM", "PDBADMIN"}


 def required_env(name):
 	value = os.getenv(name)
 	if not value:
         raise SystemExit(f"Set {name} before running this script.")
 	return value


 def main():
 	user = required_env("DB_USER")
 	if user.upper() in ADMIN_USERS:
         raise SystemExit(
             "Use the vector_demo application user, not an admin user."
     	)

     required_env("OPENAI_API_KEY")

 	# Generate the query vector used for the validation search.
 	client = OpenAI()
     response = client.embeddings.create(
         model=required_env("EMBEDDING_MODEL"),
         input=QUERY_TEXT,
 	)
     query_vector = array.array("f", response.data[0].embedding)

 	with oracledb.connect(
         user=user,
         password=required_env("DB_PASSWORD"),
         dsn=required_env("DB_DSN"),
 	) as connection:
         with connection.cursor() as cursor:
             # Remove a prior index so the script can be rerun safely.
             try:
                 cursor.execute(f"DROP INDEX {INDEX_NAME}")
             except oracledb.DatabaseError as exc:
                 error = exc.args[0]
                 if error.code != 1418:
                 	raise

             # Create an approximate neighbor-partitions vector index.
             cursor.execute(
                 """
                 CREATE VECTOR INDEX document_embedding_idx
                 ON documents (embedding)
                 ORGANIZATION NEIGHBOR PARTITIONS
                 DISTANCE COSINE
                 WITH TARGET ACCURACY 95
                 """
             )

             # Confirm that Oracle reports the vector index as valid.
             cursor.execute(
                 """
                 SELECT index_name, index_type, status
                 FROM user_indexes
                 WHERE index_name = :index_name
                 """,
                 index_name=INDEX_NAME,
             )
             index_row = cursor.fetchone()

             # Confirm that semantic search still returns rows after index creation.
             cursor.execute(
                 """
                 SELECT title
                 FROM documents
                 ORDER BY VECTOR_DISTANCE(embedding, :query_vector, COSINE)
                 FETCH EXACT FIRST 3 ROWS ONLY
                 """,
                 query_vector=query_vector,
             )
             rows = cursor.fetchall()

 	if not index_row:
         raise SystemExit("Vector index was not found in USER_INDEXES.")

     print("Created vector index DOCUMENT_EMBEDDING_IDX.")
     print("\nIndex validation:")
     print(f"Index name: {index_row[0]}")
     print(f"Index type: {index_row[1]}")
     print(f"Status: {index_row[2]}")
     result_count = len(rows)

 	print(
         "\nSemantic search still returns "
         f"{result_count} results after index creation."
 	)


 if __name__ == "__main__":
 	main()

运行脚本

运行索引脚本。

python 08_create_vector_index.py

预期输出

输出应包含 DOCUMENT_EMBEDDING_IDX 的一行,INDEX_TYPE 为 VECTOR,STATUS 为 VALID。

Created vector index DOCUMENT_EMBEDDING_IDX.

 Index validation:
 Index name: DOCUMENT_EMBEDDING_IDX
 Index type: VECTOR
 Status: VALID

 Semantic search still returns 3 results after index creation.

USER_INDEXES 确认索引存在且有效。这并不能证明某个具体查询使用了该索引,而本教程也不使用执行计划输出来作为验证方法。

在生产工作负载中,我们会用真实的数据规模、查询模式与性能目标进行测试。

Oracle AI 向量搜索技巧与故障排查

数据库容器仍在启动中

等待启动完成后,再次运行 docker logs oracle-free-26ai-vector。数据库必须就绪,Python 才能连接到 localhost:1521/FREEPDB1。

端口 1521 已被占用

停止冲突的本地服务,或更改 Docker 端口映射。若主机端口变更,请更新 DB_DSN。

数据库连接失败

核实 DB_USER, DB_PASSWORD 与 DB_DSN。在 DSN 中使用 FREEPDB1,并以 vector_demo 连接应用脚本,而不是 SYS, SYSTEM 或 PDBADMIN。

缺少嵌入 API 密钥

步骤 1–3 仅需数据库。步骤 4–8 需要 OPENAI_API_KEY,且可能产生 API 成本。

向量维度不匹配

运行 04_check_embedding_dimension.py,更新 EMBEDDING_DIM,重新运行 03_create_documents_table.py,并用 05_insert_embeddings.py 重新加载嵌入。

向量绑定失败

确保每个 FLOAT32 向量绑定都使用 array.array("f", values)。教程中对手工向量、存储的文档嵌入与查询嵌入均采用此格式。

语义排序与示例不同

这属正常。嵌入提供商可能更新模型,且浮点距离在不同运行间会有差异。

向量索引创建失败

确认 documents 表存在、vector_demo 模式拥有它,且没有残留的 DOCUMENT_EMBEDDING_IDX 来自未完全执行的运行。

走向生产

使用 oracledb.create_pool() 进行连接池化,基准测试真实数据规模,审查安全与机密管理,并基于真实查询模式调优索引。

停止并移除容器

完成本教程后,停止并移除本地容器。

docker stop oracle-free-26ai-vector
docker rm oracle-free-26ai-vector

结语

我们用 Oracle AI 向量搜索、Oracle Database Free 26ai 与 Python 构建了一个本地语义搜索流程。

我们从手写的 VECTOR(3, FLOAT32) 值开始,使用 array.array("f", values) 插入模型生成的嵌入,使用 VECTOR_DISTANCE() 查询相似的 documents,以简单的短语精确谓词对比语义检索,并通过 USER_INDEXES 验证了一个向量索引。

当嵌入需要与关系数据并存、且我们希望无需为本地流程新增独立向量数据库即可实现 SQL 可见的存储与搜索时,这种方式非常契合。

该演示故意保持小而精悍:它讲解的是基础原语,而非生产级规模。更大的应用应在真实数据下做基准测试,审查安全与权限,使用连接池,并在真实查询模式下验证索引选择。

接下来,可通过以下资源深化实现:

FAQs

什么是 Oracle AI 向量搜索?

Oracle AI 向量搜索是一组 Oracle 数据库能力,用于在关系数据旁边存储、索引和查询向量嵌入。

完成本教程是否需要 OpenAI API 密钥?

手工向量与数据库搭建步骤不需要 API 密钥。第 4–8 步需要一个嵌入提供商以及配置的 OPENAI_API_KEY。

为何 EMBEDDING_DIM 必须与嵌入模型一致?

VECTOR 列的维度必须等于嵌入模型返回的数值个数。若不匹配,将导致向量插入失败。

本教程使用了哪些距离度量?

手工向量示例使用欧氏距离。语义搜索示例使用余弦距离。

有效的向量索引能否证明查询使用了该索引?

不会。 USER_INDEXES 仅确认索引存在且状态有效,但并不确认特定查询使用了该索引。

Mark Nelson

作者

Mark Nelson

Mark Nelson 是 Oracle 的架构师兼开发者布道师。他专注于人工智能、微服务与数据库技术的融合。他是活跃的博主、出版作者、Manning Publications 的技术审稿人、Stanford Code in Place 的章节负责人,以及 DeepLearning.ai 的导师。他经常在 Java 与 Oracle 用户组、AI 线下活动以及各类大型会议上演讲。他热爱学习与分享,拥有在 IBM 和 Oracle 超过三十年的行业经验。

主题