管理向量索引
如需对此功能提供反馈或请求支持,请发送电子邮件至 bq-vector-search@google.com。
本文档介绍如何创建和管理向量索引。
向量索引是一种数据结构,旨在让 VECTOR_SEARCH
函数对嵌入执行更高效的向量搜索。当 VECTOR_SEARCH
能够使用向量索引时,该函数会使用近似最近邻搜索技术来帮助提高搜索性能,虽然降低召回率,但可返回更接近的结果。
角色与权限
要创建向量索引,您需要对要在其中创建索引的表拥有 bigquery.tables.createIndex
IAM 权限。要删除向量索引,您需要拥有 bigquery.tables.deleteIndex
权限。以下每个预定义的 IAM 角色都包含使用向量索引所需的权限:
- BigQuery Data Owner (
roles/bigquery.dataOwner
) - BigQuery Data Editor (
roles/bigquery.dataEditor
)
创建向量索引
如需创建向量索引,请使用 CREATE VECTOR INDEX
数据定义语言 (DDL) 语句:
转到 BigQuery 页面。
在查询编辑器中,选择索引类型并在以下 SQL 语句上运行:
如需创建 IVF 向量索引,请执行以下操作:
CREATE [ OR REPLACE ] VECTOR INDEX [ IF NOT EXISTS ] INDEX_NAME ON DATASET_NAME.TABLE_NAME(COLUMN_NAME) STORING(STORED_COLUMN_NAME [, ...]) OPTIONS(index_type = "IVF", distance_type = "DISTANCE_TYPE", ivf_options = '{"num_lists":NUM_LISTS}')
如需创建 TreeAH 向量索引,请执行以下操作:
CREATE [ OR REPLACE ] VECTOR INDEX [ IF NOT EXISTS ] INDEX_NAME ON DATASET_NAME.TABLE_NAME(COLUMN_NAME) OPTIONS(index_type = "TREE_AH", distance_type = "DISTANCE_TYPE", tree_ah_options = '{"leaf_node_embedding_count":LEAF_NODE_EMBEDDING_COUNT, "normalization_type":"NORMALIZATION_TYPE"}')
如需了解详情,请参阅选择向量索引类型。
替换以下内容:
INDEX_NAME
:您要创建的向量索引的名称。由于索引始终在基表所在的项目和数据集中创建,因此无需在名称中指定这些内容。DATASET_NAME
:包含该表的数据集的名称。TABLE_NAME
:包含嵌入数据列的表的名称。COLUMN_NAME
:包含嵌入数据的列的名称。该列的类型必须是ARRAY<FLOAT64>
。该列不能有任何子字段。数组中的所有元素都必须为非NULL
,且列中的所有值都必须具有相同的数组维度。STORED_COLUMN_NAME
:表中要存储在向量索引中的顶级列的名称。列类型不能为RANGE
。如果表具有行级访问权限政策或是列具有政策标记,则不会使用存储列。如需了解如何启用存储列,请参阅存储列和预先过滤。DISTANCE_TYPE
:指定使用此索引执行向量搜索时要使用的默认距离类型。支持的值为EUCLIDEAN
、COSINE
和DOT_PRODUCT
。默认值为EUCLIDEAN
。索引创建本身始终使用
EUCLIDEAN
距离进行训练,但VECTOR_SEARCH
函数中使用的距离可能不同。如果您为
VECTOR_SEARCH
函数的distance_type
参数指定值,则系统会使用该值(而非DISTANCE_TYPE
值)。NUM_LISTS
:小于或等于 5,000 的INT64
值,用于确定 IVF 算法创建的列表数量。IVF 算法将整个数据空间划分为若干个列表(数量等于NUM_LISTS
),其中彼此相距较近的数据点更有可能被放在同一个列表中。如果NUM_LISTS
较小,则列表数量会较少,且包含的数据点数量较多;如果数值较大,则创建的列表数量会较多,且包含的数据点数量较少。您可以将
NUM_LISTS
与VECTOR_SEARCH
函数中的fraction_lists_to_search
参数结合使用,创建高效的向量搜索。如果您的数据分布在嵌入空间中的多个小组里,指定较大的NUM_LISTS
可创建包含较多列表的索引;指定较小的fraction_lists_to_search
值可在向量搜索中扫描较少的列表。当您的数据分布在数量较少但规模较大的组中时,请使用较小的NUM_LISTS
和较大的fraction_lists_to_search
值。使用大的num_lists
值可能会导致向量索引需要更长的时间进行构建。如果您未指定
NUM_LISTS
,BigQuery 会计算出合适的值。LEAF_NODE_EMBEDDING_COUNT
:一个大于或等于 500 的INT64
值,指定 TreeAH 算法创建的树的每个叶节点中向量的近似数量。TreeAH 算法将整个数据空间划分为若干个列表,每个列表包含大约LEAF_NODE_EMBEDDING_COUNT
个数据点。若值越小,则创建的列表就越多,数据点数量越少;若值越大,则创建的列表就越少,数据点数量越多。默认值为 1,000。NORMALIZATION_TYPE
:一个STRING
值。支持的值有NONE or
L2
。默认值为NONE
。 标准化会在对基表数据和查询数据执行任何处理之前进行,但不会修改TABLE_NAME
中的嵌入列COLUMN_NAME
。根据数据集、嵌入模型以及VECTOR_SEARCH
期间使用的距离类型,对嵌入进行标准化可能会提高召回率。
以下示例在 my_table
的 embedding
列上创建向量索引:
CREATE TABLE my_dataset.my_table(embedding ARRAY<FLOAT64>); CREATE VECTOR INDEX my_index ON my_dataset.my_table(embedding) OPTIONS(index_type = 'IVF');
以下示例在 my_table
的 embedding
列上创建向量索引,并指定要使用的距离类型和 IVF 选项:
CREATE TABLE my_dataset.my_table(embedding ARRAY<FLOAT64>); CREATE VECTOR INDEX my_index ON my_dataset.my_table(embedding) OPTIONS(index_type = 'IVF', distance_type = 'COSINE', ivf_options = '{"num_lists": 2500}')
以下示例在 my_table
的 embedding
列上创建向量索引,并指定要使用的距离类型和 TreeAH 选项:
CREATE TABLE my_dataset.my_table(id INT64, embedding ARRAY<FLOAT64>); CREATE VECTOR INDEX my_index ON my_dataset.my_table(embedding) OPTIONS (index_type = 'TREE_AH', distance_type = 'EUCLIDEAN', tree_ah_options = '{"normalization_type": "L2"}');
选择向量索引类型
BigQuery 提供两种向量索引类型。
IVF 指数
IVF 是倒排文件索引,使用 k-means 算法对向量数据进行聚类,然后根据这些聚类对向量数据进行分区。当您使用 VECTOR_SEARCH
函数搜索向量数据时,它可以使用这些分区来减少为确定结果而需要读取的数据量。
TreeAH 索引
TreeAH 是一种使用 Google ScaNN 算法的向量索引。具体运作方式如下:
基表被分成更小、更易于管理的分片。
使用从
tree_ah_options
中的leaf_node_embedding_count
选项派生的聚类数量来训练聚类模型。这些向量会经过产品量化处理,并存储在索引表中。
在
VECTOR_SEARCH
期间,使用非对称哈希高效地计算每个查询向量的候选列表。该非对称哈希针对近似距离计算进行了硬件优化。然后,使用精确嵌入对这些候选项重新评分和重新排名。
TreeAH 算法针对处理数百个或更多查询向量的批量查询进行了优化。与 IVF 相比,使用产品量化可以显著缩短延迟时间并减少费用,而且有望减少几个数量级。但是,由于开销增加,当您的查询向量较少时,IVF 算法可能更出色。
如果您的用例满足以下条件,我们建议您尝试使用 TreeAH 索引类型:
您的表包含的行数不超过 2 亿。
您经常执行涉及数百个或更多查询向量的大型批量查询。
对于小批量查询,使用 TreeAH 索引类型的
VECTOR_SEARCH
可能会回退为使用暴力搜索。在这种情况下,系统会填充“未使用向量索引”原因来说明原因。您的工作流不需要使用存储列或预过滤。BigQuery 会将与 TreeAH 索引搭配使用的预过滤条件视为后过滤条件。
如有任何问题和疑虑,特别是可伸缩性、限制和性能,请通过 bq-vector-search@google.com 与我们联系。
存储列和预先过滤
如需进一步提高向量索引的效率,您可以指定基表中的列以存储在向量索引中。使用存储列可通过以下方式优化调用 VECTOR_SEARCH
函数的查询:
VECTOR_SEARCH
函数会输出名为base
的结构体,其中包含基表中的所有列。如果没有存储列,则需要进行可能开销很大的联接来检索存储在base
中的列。如果您的查询仅从base
中选择存储列,则 BigQuery 会优化查询以消除该联接。您不必搜索整个表,而是可以对使用
WHERE
子句预先过滤基表的查询语句调用VECTOR_SEARCH
函数。如果您的表具有索引,并且您仅过滤存储列,则 BigQuery 会在搜索之前过滤数据,然后使用索引搜索较小的结果集,从而优化查询。如果您过滤未存储的列,则 BigQuery 会在搜索表后应用过滤(即进行事后过滤)。事后过滤效率较低,并且可能会导致结果集中的匹配项少于
top_k
。在某些情况下,预先过滤也可能会减小结果集的大小。如果发生这种情况,请尝试在VECTOR_SEARCH
调用中增加fraction_lists_to_search
的值。
如需存储列,请在 CREATE VECTOR INDEX
DDL 语句的 STORING
子句中列出这些列。存储列会增加向量索引的大小,因此最好仅存储最常使用或过滤后的列。
以下示例创建了一个具有存储列的向量索引,然后说明了不同类型的向量搜索的行为:
-- Create a table that contains an embedding. CREATE TABLE my_dataset.my_table(embedding ARRAY<FLOAT64>, type STRING, creation_time DATETIME, id INT64); -- Create a query table that contains an embedding. CREATE TABLE my_dataset.my_testdata(embedding ARRAY<FLOAT64>, test_id INT64); -- Create a vector index with stored columns. CREATE VECTOR INDEX my_index ON my_dataset.my_table(embedding) STORING (type, creation_time) OPTIONS (index_type = 'IVF'); -- Select only stored columns from a vector search to avoid an expensive join. SELECT query, base.type, distance FROM VECTOR_SEARCH( TABLE my_dataset.my_table, 'embedding' TABLE my_dataset.my_testdata); -- Pre-filter on a stored column. The index speeds up the query. SELECT * FROM VECTOR_SEARCH( (SELECT * FROM my_dataset.my_table WHERE type = 'animal'), 'embedding', TABLE my_dataset.my_testdata); -- Filter on a column that isn't stored. The index is used to search the -- entire table, and then the results are post-filtered. You might see fewer -- than 5 matches returned for some embeddings. SELECT query.test_id, base.type, distance FROM VECTOR_SEARCH( (SELECT * FROM my_dataset.my_table WHERE id = 123), 'embedding', TABLE my_dataset.my_testdata, top_k => 5); -- Use post-filters. The index is used, but the entire table is searched and -- the post-filtering might reduce the number of results. SELECT query.test_id, base.type, distance FROM VECTOR_SEARCH( TABLE my_dataset.my_table, 'embedding', TABLE my_dataset.my_testdata, top_k => 5) WHERE base.type = 'animal'; -- Use pre-filters with brute force. The data is filtered and then searched -- with brute force for exact results. SELECT query.test_id, base.type, distance FROM VECTOR_SEARCH( (SELECT * FROM my_dataset.my_table WHERE id = 123), 'embedding', TABLE my_dataset.my_testdata, options => '{"use_brute_force":true}');
限制
- 您不能在预先过滤中使用逻辑视图。
- 如果您的预先过滤包含子查询,则可能会干扰索引使用。
- 如果基表中列的模式、类型或架构发生更改,并且是向量索引中存储的列,则可能会延迟一段时间才会在向量索引中反映此更改。在更新应用于索引之前,向量搜索查询会使用基表中修改后的存储列。
- 当表的索引具有存储列时,如果从对该表进行的
VECTOR_SEARCH
查询的query
输出中选择STRUCT
类型的列,则整个查询可能会失败。 - 预览版中的 TreeAH 索引不支持存储列。
了解索引刷新
向量索引由 BigQuery 完全管理,并且会在编入索引的表发生变化时自动进行刷新。如果您删除表中编入索引的列或重命名表本身,则向量索引会自动予以删除。
如果您在小于 10 MB 的表上创建向量索引,则系统不会填充该向量索引。同样,如果您从编入索引的表中删除数据,并且该表大小低于 10 MB,则系统会暂时停用向量索引。在这种情况下,向量搜索查询不使用该索引,并且 Job
资源的 vectorSearchStatistics
部分中的 indexUnusedReasons
代码为 BASE_TABLE_TOO_SMALL
。在没有索引的情况下,VECTOR_SEARCH
会自动转为使用暴力破解来查找嵌入的最近邻。
使用 VECTOR_SEARCH
函数的查询始终会返回正确的结果,即使部分数据尚未编入索引也是如此。
获取有关向量索引的信息
您可以通过查询 INFORMATION_SCHEMA
来验证向量索引是否存在以及是否就绪。以下视图包含有关向量索引的元数据:
INFORMATION_SCHEMA.VECTOR_INDEXES
视图具有数据集中的向量索引的相关信息。CREATE VECTOR INDEX
语句完成后,您在使用索引之前,必须先填充该索引。您可以使用last_refresh_time
和coverage_percentage
列来验证向量索引的就绪情况。如果向量索引尚未就绪,您仍然可以对表使用VECTOR_SEARCH
函数,只是在没有索引的情况下,其运行速度可能会更慢。INFORMATION_SCHEMA.VECTOR_INDEX_COLUMNS
视图具有数据集中所有表的向量索引列的相关信息。INFORMATION_SCHEMA.VECTOR_INDEX_OPTIONS
视图具有数据集中向量索引使用的选项的相关信息。
向量索引示例
以下示例展示了位于项目 my_project
的数据集 my_dataset
中表的所有活跃向量索引。它包括索引名称、用于创建索引的 DDL 语句以及索引覆盖率百分比。如果编入索引的基表小于 10 MB,则系统不会填充其索引,在这种情况下,coverage_percentage
值为 0。
SELECT table_name, index_name, ddl, coverage_percentage FROM my_project.my_dataset.INFORMATION_SCHEMA.VECTOR_INDEXES WHERE index_status = 'ACTIVE';
结果类似于以下内容:
+------------+------------+-------------------------------------------------------------------------------------------------+---------------------+ | table_name | index_name | ddl | coverage_percentage | +------------+------------+-------------------------------------------------------------------------------------------------+---------------------+ | table1 | indexa | CREATE VECTOR INDEX `indexa` ON `my_project.my_dataset.table1`(embeddings) | 100 | | | | OPTIONS (distance_type = 'EUCLIDEAN', index_type = 'IVF', ivf_options = '{"num_lists": 100}') | | +------------+------------+-------------------------------------------------------------------------------------------------+---------------------+ | table2 | indexb | CREATE VECTOR INDEX `indexb` ON `my_project.my_dataset.table2`(vectors) | 42 | | | | OPTIONS (distance_type = 'COSINE', index_type = 'IVF', ivf_options = '{"num_lists": 500}') | | +------------+------------+-------------------------------------------------------------------------------------------------+---------------------+ | table3 | indexc | CREATE VECTOR INDEX `indexc` ON `my_project.my_dataset.table3`(vectors) | 98 | | | | OPTIONS (distance_type = 'DOT_PRODUCT', index_type = 'TREE_AH', | | | | | tree_ah_options = '{"leaf_node_embedding_count": 1000, "normalization_type": "NONE"}') | | +------------+------------+-------------------------------------------------------------------------------------------------+---------------------+
向量索引列示例
以下查询会提取具有向量索引的列的相关信息:
SELECT table_name, index_name, index_column_name, index_field_path FROM my_project.dataset.INFORMATION_SCHEMA.VECTOR_INDEX_COLUMNS;
结果类似于以下内容:
+------------+------------+-------------------+------------------+ | table_name | index_name | index_column_name | index_field_path | +------------+------------+-------------------+------------------+ | table1 | indexa | embeddings | embeddings | | table2 | indexb | vectors | vectors | | table3 | indexc | vectors | vectors | +------------+------------+-------------------+------------------+
向量索引选项示例
以下查询会提取向量索引选项的相关信息:
SELECT table_name, index_name, option_name, option_type, option_value FROM my_project.dataset.INFORMATION_SCHEMA.VECTOR_INDEX_OPTIONS;
结果类似于以下内容:
+------------+------------+------------------+------------------+-------------------------------------------------------------------+ | table_name | index_name | option_name | option_type | option_value | +------------+------------+------------------+------------------+-------------------------------------------------------------------+ | table1 | indexa | index_type | STRING | IVF | | table1 | indexa | distance_type | STRING | EUCLIDEAN | | table1 | indexa | ivf_options | STRING | {"num_lists": 100} | | table2 | indexb | index_type | STRING | IVF | | table2 | indexb | distance_type | STRING | COSINE | | table2 | indexb | ivf_options | STRING | {"num_lists": 500} | | table3 | indexc | index_type | STRING | TREE_AH | | table3 | indexc | distance_type | STRING | DOT_PRODUCT | | table3 | indexc | tree_ah_options | STRING | {"leaf_node_embedding_count": 1000, "normalization_type": "NONE"} | +------------+------------+------------------+------------------+-------------------------------------------------------------------+
向量索引使用情况
运行向量搜索查询的作业的作业元数据中提供了有关向量索引使用情况的信息。您可以使用 Google Cloud 控制台、bq 命令行工具、BigQuery API 或客户端库查看作业元数据。
使用 Google Cloud 控制台时,您可以在向量索引使用模式和未使用向量索引的原因字段中找到向量索引使用情况信息。
使用 bq 工具或 BigQuery API 时,您可以在 Job
资源的 VectorSearchStatistics
部分中找到向量索引使用情况信息。
索引使用模式通过提供以下值之一来指示是否使用了向量索引:
UNUSED
:未使用向量索引。PARTIALLY_USED
:查询中的某些VECTOR_SEARCH
函数使用向量索引,某些函数则不使用。FULLY_USED
:查询中的每个VECTOR_SEARCH
函数都使用一个向量索引。
当索引使用模式值为 UNUSED
或 PARTIALLY_USED
时,未使用索引的原因会指明没有在查询中使用向量索引的原因。
例如,bq show --format=prettyjson -j my_job_id
返回的以下结果表明,由于在 VECTOR_SEARCH
函数中指定了 use_brute_force
选项,因此未使用索引:
"vectorSearchStatistics": { "indexUnusedReasons": [ { "baseTable": { "datasetId": "my_dataset", "projectId": "my_project", "tableId": "my_table" }, "code": "INDEX_SUPPRESSED_BY_FUNCTION_OPTION", "message": "No vector index was used for the base table `my_project:my_dataset.my_table` because use_brute_force option has been specified." } ], "indexUsageMode": "UNUSED" }
索引管理选项
要创建索引并让 BigQuery 维护它们,您有两种选择:
- 使用默认共享槽池:当计划编入索引的数据低于每个组织的限制时,您可以使用免费共享槽池来管理索引。
- 使用您自己的预留:要在较大的生产工作负载上实现更可预测且一致的索引编制进度,您可以使用自己的预留来管理索引。
使用共享槽
如果您尚未将项目配置为使用专用预留来执行索引操作,就会在免费的共享槽池中处理索引管理,但存在以下限制。
如果向表添加数据,从而导致编入索引的表的总大小超过组织的限制,则 BigQuery 会暂停所有编入索引的表的索引管理。发生这种情况时,INFORMATION_SCHEMA.VECTOR_INDEXES
视图中的 index_status
字段会显示 PENDING DISABLEMENT
,且该索引会排队等待删除。虽然索引正在等待停用,但它仍然在查询中使用,并且您需要为该索引支付存储费用。索引删除后,index_status
字段会将该索引显示为 TEMPORARILY DISABLED
。在此状态下,查询不使用该索引,并且您不需要为该索引支付存储费用。在这种情况下,IndexUnusedReason
代码为 BASE_TABLE_TOO_LARGE
。
如果您从表中删除数据,并且编入索引的表的总大小低于每个组织的限制,则所有已编入索引的表的索引管理功能都将恢复。INFORMATION_SCHEMA.VECTOR_INDEXES
视图中的 index_status
字段为 ACTIVE
,查询可以使用该索引,并且您需要为该索引支付存储费用。
BigQuery 不保证共享池的可用容量或您看到的索引吞吐量。对于生产应用,您可能需要使用专用槽来执行索引处理功能。
使用您自己的预留
您可以选择指定使用自己的预留来将表编入索引,而不是使用默认的共享槽池。使用您自己的预留可确保索引管理作业(例如创建、刷新和后台优化)具有可预测且一致的性能。
- 在预留中运行索引作业时,没有表大小限制。
- 使用您自己的预留可以在管理索引时更加灵活。 如果需要创建非常大的索引或对编入索引的表进行大量更新,您可以暂时为分配空间添加更多的槽。
如需将具有指定预留的项目中的表编入索引,请在表所在的区域中创建预留。然后,将项目分配到该预留,并将 job_type
设置为 BACKGROUND
:
SQL
在 Google Cloud 控制台中,转到 BigQuery 页面。
在查询编辑器中,输入以下语句:
CREATE ASSIGNMENT `ADMIN_PROJECT_ID.region-LOCATION.RESERVATION_NAME.ASSIGNMENT_ID` OPTIONS ( assignee = 'projects/PROJECT_ID', job_type = 'BACKGROUND');
请替换以下内容:
点击
运行。
如需详细了解如何运行查询,请参阅运行交互式查询。
bq
使用 bq mk
命令:
bq mk \ --project_id=ADMIN_PROJECT_ID \ --location=LOCATION \ --reservation_assignment \ --reservation_id=RESERVATION_NAME \ --assignee_id=PROJECT_ID \ --job_type=BACKGROUND \ --assignee_type=PROJECT
请替换以下内容:
查看索引作业
每次在单个表上创建或更新索引时,都会创建一个新的索引作业。如需查看作业的相关信息,请查询 INFORMATION_SCHEMA.JOBS*
视图。通过在查询的 WHERE
子句中设置 job_type IS NULL AND SEARCH(job_id, '`search_index`')
,即可对索引作业进行过滤。以下示例列出了项目 my_project
中最新的五个索引作业:
SELECT * FROM region-us.INFORMATION_SCHEMA.JOBS WHERE project_id = 'my_project' AND job_type IS NULL AND SEARCH(job_id, '`search_index`') ORDER BY creation_time DESC LIMIT 5;
选择预留大小
要为预留选择适当的槽数,您应该考虑何时运行索引管理作业、它们使用的槽数以及您的使用量随时间变化的情况。在以下情况下,BigQuery 会触发索引管理作业:
- 您在表上创建索引。
- 编入索引的表中的数据被修改。
- 表的架构发生了变化,并且此变化影响将哪些列编入索引。
- 索引数据和元数据会定期优化或更新。
表的索引管理作业所需的槽数取决于以下因素:
- 表的大小
- 将数据注入到表中的速率
- 应用于表的 DML 语句的速率
- 构建和维护索引的可接受延迟时间
- 索引的复杂程度,通常由数据属性(例如重复字词数量)决定
监控用量和进度
评估高效地运行索引管理作业所需的槽数的最佳方法是监控槽利用率并相应地调整预留大小。以下查询生成索引管理作业的每日槽使用量。us-west1
地区仅包含过去 30 天的数据:
SELECT TIMESTAMP_TRUNC(job.creation_time, DAY) AS usage_date, -- Aggregate total_slots_ms used for index-management jobs in a day and divide -- by the number of milliseconds in a day. This value is most accurate for -- days with consistent slot usage. SAFE_DIVIDE(SUM(job.total_slot_ms), (1000 * 60 * 60 * 24)) AS average_daily_slot_usage FROM `region-us-west1`.INFORMATION_SCHEMA.JOBS job WHERE project_id = 'my_project' AND job_type IS NULL AND SEARCH(job_id, '`search_index`') GROUP BY usage_date ORDER BY usage_date DESC limit 30;
如果没有足够的槽来运行索引管理作业,则索引可能会与其表不同步,并且索引作业可能会失败。在此情况下,BigQuery 从头开始重新构建索引。为避免产生不同步索引,请确保您有足够的槽来支持数据注入和优化中的索引更新。如需详细了解如何监控槽使用量,请参阅管理员资源图表。
删除向量索引
当您不再需要向量索引或想要更改表中编入索引的列时,可以使用 DROP VECTOR INDEX
DDL 语句删除该表上的索引。
例如:
DROP VECTOR INDEX my_index ON my_dataset.indexed_table;
如果删除编入索引的表,其索引也会自动删除。
后续步骤
- 如需大致了解向量索引用例、价格和限制,请参阅向量搜索简介。
- 试用使用向量搜索来搜索嵌入教程。