云数据库 PostgreSQL 基于 PostgreSQL + pgvector 的以图搜图商品搜索系统

这里说明基于 PostgreSQL + pgvector 的以图搜图商品搜索系统的 AI 实践。

相关产品
云数据库 PostgreSQL 产品页,含版本规格、价格与购买入口。

一、目录

一、系统架构概述

二、第一阶段:环境准备与扩展检测

三、第二阶段:创建商品图片搜索表结构

四、第三阶段:插入模拟商品与图片数据

五、第四阶段:创建 HNSW 向量索引与辅助索引

六、第五阶段:以图搜图核心查询验证(7大场景)

七、第六阶段:EXPLAIN ANALYZE 性能验证

八、第七阶段:存储统计与系统监控

三、技术选型

本方案基于 PostgreSQL 15.14 + pgvector 0.8.2构建以图搜图商品搜索系统,核心技术要点:

·向量存储:pgvector 扩展提供 vector(768) 数据类型,存储 CLIP ViT-L/14 模型生成的图片特征向量。

·HNSW 索引:分层可导航小世界(Hierarchical Navigable Small World)图索引,支持近似最近邻(ANN)搜索。

·余弦相似度:使用 <=> 余弦距离算子,计算图片特征向量间的相似度。

·混合搜索:图像向量相似度(70%)+ 文本三元组相似度(30%)组合排序。

四、表结构设计

┌──────────────────┐ ┌───────────────────────┐ ┌────────────────────┐│ product_catalog │ │ product_image_search │ │ image_search_log ││──────────────────│ │───────────────────────│ │────────────────────││ product_id (PK) │◄────│ product_id (FK) │ │ id (PK) ││ product_name │ │ image_id (UK) │ │ session_id ││ category │ │ embedding vector(768) │ │ query_vector ││ brand │ │ image_type │ │ result_count ││ price │ │ angle │ │ top_similarity ││ tags │ │ metadata (JSONB) │ │ search_time_ms │└──────────────────┘ └───────────────────────┘ └────────────────────┘

六、步骤1:获取数据库环境信息

注意:说明:

注意:获取 PostgreSQL 版本、数据库、用户、服务器信息。

输入 SQL:

SELECT version() AS pg_version, current_database() AS database, current_user AS user_name, pg_size_pretty(pg_database_size(current_database())) AS db_size, inet_server_addr() AS server_ip, inet_server_port() AS server_port;

输出结果:(3.0ms)。

pg_version database user_name db_size server_ip server_port
PostgreSQL 15.14 on x86_64-pc-linux-gnu, compiled by gcc … functional_verification postgres_admin 9175KB 30.121.110.245 50742

七、步骤2:检查可用的向量/图计算扩展

注意:说明:

注意:检查 pgvector、AGE、pg_trgm 等扩展是否可用。

输入 SQL:

SELECT name, default_version, commentFROM pg_available_extensionsWHERE name IN ('vector', 'age', 'pg_trgm', 'pg_prewarm')ORDER BY name;

输出结果:(4.2ms)。

name default_version comment
age 1.5.0 AGE database extension
pg_prewarm 1.2 prewarm relation data
pg_trgm 1.6 text similarity measurement and index searching based on trigrams
vector 0.8.2 vector data type and ivfflat and hnsw access methods

4个扩展全部可用:pgvector 0.8.2用于向量搜索、AGE 1.5.0用于图数据库、pg_trgm 1.6用于文本相似度、pg_prewarm 1.2用于索引预热。

八、步骤3:安装 pgvector 扩展

输入 SQL:

CREATE EXTENSION IF NOT EXISTS vector;

执行结果:CREATE EXTENSION (1.6ms)。

确认安装:

SELECT extname, extversionFROM pg_extensionWHERE extname = 'vector';

输出结果:

extname extversion
vector 0.8.2

九、步骤4:安装 pg_trgm 扩展

输入 SQL:

CREATE EXTENSION IF NOT EXISTS pg_trgm;

执行结果:CREATE EXTENSION (11.7ms)。

十、步骤5:确认 pg_prewarm 扩展

输入 SQL:

CREATE EXTENSION IF NOT EXISTS pg_prewarm;

执行结果: CREATE EXTENSION (1.5ms)。

十二、步骤6:清理旧的测试表

输入 SQL:

DROP TABLE IF EXISTS product_image_search CASCADE;DROP TABLE IF EXISTS product_catalog CASCADE;DROP TABLE IF EXISTS image_search_log CASCADE;

执行结果:DROP TABLE (1.5ms)。

十三、步骤7:创建商品目录表 product_catalog

注意:说明:

注意:商品基本信息:名称、分类、品牌、价格、标签等。

输入 SQL:

CREATE TABLE product_catalog ( id BIGSERIAL PRIMARY KEY, product_id VARCHAR(32) NOT NULL UNIQUE, product_name TEXT NOT NULL, category VARCHAR(100) NOT NULL, sub_category VARCHAR(100), brand VARCHAR(100), price NUMERIC(10, 2), description TEXT, tags TEXT[], status SMALLINT DEFAULT 1, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW());COMMENT ON TABLE product_catalog IS '商品目录表 – 存储商品基本信息';

执行结果:COMMENT (5.5ms)。

十四、步骤8:创建商品图片向量表 product_image_search(核心表)

注意:说明:

注意:768维向量 (CLIP ViT-L/14) + 商品关联 + 图片属性。

输入 SQL:

CREATE TABLE product_image_search ( id BIGSERIAL PRIMARY KEY, image_id VARCHAR(64) NOT NULL UNIQUE, product_id VARCHAR(32) NOT NULL REFERENCES product_catalog(product_id), image_url TEXT NOT NULL, thumbnail_url TEXT, embedding vector(768) NOT NULL, image_type VARCHAR(20) DEFAULT 'main', angle VARCHAR(20) DEFAULT 'front', background VARCHAR(20) DEFAULT 'white', metadata JSONB DEFAULT '{}', status SMALLINT DEFAULT 1, created_at TIMESTAMPTZ DEFAULT NOW());COMMENT ON TABLE product_image_search IS '商品图片向量表 – 以图搜图核心表';COMMENT ON COLUMN product_image_search.embedding IS 'CLIP ViT-L/14 模型生成的 768 维图片特征向量';COMMENT ON COLUMN product_image_search.image_type IS '图片类型: main=主图, detail=细节图, scene=场景图';COMMENT ON COLUMN product_image_search.angle IS '拍摄角度: front=正面, side=侧面, back=背面, top=俯视';

执行结果:COMMENT (6.9ms)。

十五、步骤9:创建搜索日志表 image_search_log

注意:说明:

注意:记录每次搜索的查询向量、结果数、性能等。

输入 SQL:

CREATE TABLE image_search_log ( id BIGSERIAL PRIMARY KEY, session_id VARCHAR(64), query_vector vector(768), query_image_url TEXT, result_count INT, top_similarity REAL, search_time_ms REAL, filter_category VARCHAR(100), created_at TIMESTAMPTZ DEFAULT NOW());COMMENT ON TABLE image_search_log IS '以图搜图搜索日志 – 记录搜索行为和性能';

执行结果:COMMENT (3.3ms)

十六、步骤10:查看已创建的表结构

输入 SQL:

SELECT c.relname AS table_name, obj_description(c.oid) AS comment, pg_size_pretty(pg_total_relation_size(c.oid)) AS total_sizeFROM pg_class cJOIN pg_namespace n ON n.oid = c.relnamespaceWHERE n.nspname = 'public' AND c.relkind = 'r' AND c.relname IN ('product_catalog', 'product_image_search', 'image_search_log')ORDER BY c.relname;

输出结果:(2.9ms)。

table_name comment total_size
image_search_log 以图搜图搜索日志 – 记录搜索行为和性能 16KB
product_catalog 商品目录表 – 存储商品基本信息 24KB
product_image_search 商品图片向量表 – 以图搜图核心表 24KB

3张表创建成功。

十七、步骤11:查看商品图片向量表的字段定义

输入 SQL:

SELECT column_name, data_type, character_maximum_length, column_default, is_nullableFROM information_schema.columnsWHERE table_name = 'product_image_search' AND table_schema = 'public'ORDER BY ordinal_position;

输出结果:(7.3ms)。

column_name data_type character_maximum_length column_default is_nullable
id bigint NULL nextval('product_image_search_id_seq'::regclass) NO
image_id character varying 64 NULL NO
product_id character varying 32 NULL NO
image_url text NULL NULL NO
thumbnail_url text NULL NULL YES
embedding USER-DEFINED NULL NULL NO
image_type character varying 20 'main'::character varying YES
angle character varying 20 'front'::character varying YES
background character varying 20 'white'::character varying YES
metadata jsonb NULL '{}'::jsonb YES
status smallint NULL 1 YES
created_at timestamp with time zone NULL now() YES

embedding 字段类型为 USER-DEFINED (vector(768)),确认 pgvector 向量字段创建成功。

十九、步骤12:插入商品目录数据(8大分类,200件商品)

注意:说明:

注意:覆盖8大分类:手机数码/电脑办公/运动户外/外设配件/穿戴设备/音频设备/箱包皮具/家用电器。

输入 SQL:

INSERT INTO product_catalog (product_id, product_name, category, sub_category, brand, price, description, tags, status)SELECT 'P' || lpad(g::text, 6, '0'), CASE (g % 8) WHEN 0 THEN '智能手机 ' || (ARRAY['Pro','Max','Ultra','Lite','SE','Plus','Mini','Air'])[(g % 8) + 1] || ' ' || g WHEN 1 THEN '笔记本电脑 ' || (ARRAY['ThinkPad','MacBook','Surface','MateBook','XPS','Yoga','ZenBook','Swift'])[(g % 8) + 1] || ' ' || g WHEN 2 THEN '运动鞋 ' || (ARRAY['Air Max','Ultra Boost','Gel','Fresh Foam','React','Zoom','NB','Asics'])[(g % 8) + 1] || ' ' || g WHEN 3 THEN '机械键盘 ' || (ARRAY['Cherry','Filco','Leopold','HHKB','Ducky','Razer','Logitech','Keychron'])[(g % 8) + 1] || ' ' || g WHEN 4 THEN '智能手表 ' || (ARRAY['Apple Watch','Galaxy Watch','Huawei Watch','Amazfit','Garmin','Fitbit','TicWatch','Suunto'])[(g % 8) + 1] || ' ' || g WHEN 5 THEN '无线耳机 ' || (ARRAY['AirPods','Galaxy Buds','FreeBuds','WF-1000','WH-1000','Bose QC','Jabra','Sony'])[(g % 8) + 1] || ' ' || g WHEN 6 THEN '双肩背包 ' || (ARRAY['Osprey','North Face','Arc','Samsonite','Tumi','Incase','Herschel','Peak'])[(g % 8) + 1] || ' ' || g ELSE '咖啡机 ' || (ARRAY['Nespresso','De Longhi','Breville','Jura','Saeco','Krups','Moccamaster','Fellow'])[(g % 8) + 1] || ' ' || g END, (ARRAY['手机数码','电脑办公','运动户外','外设配件','穿戴设备','音频设备','箱包皮具','家用电器'])[(g % 8) + 1], — … (品牌/价格/描述/标签/状态 自动生成)FROM generate_series(1, 200) g;

执行结果:INSERT 0 200 (3.7ms)。

20、步骤13:查看商品分类分布

输入 SQL:

SELECT category, count(*) AS product_count, round(avg(price), 2) AS avg_price, min(price) AS min_price, max(price) AS max_priceFROM product_catalogWHERE status = 1GROUP BY categoryORDER BY product_count DESC;

输出结果:(2.1ms)

category product_count avg_price min_price max_price
外设配件 25 2600.00 200.00 5000.00
家用电器 25 2600.00 200.00 5000.00
运动户外 25 2500.00 100.00 4900.00
音频设备 25 2600.00 200.00 5000.00
电脑办公 25 2600.00 200.00 5000.00
箱包皮具 25 2500.00 100.00 4900.00
手机数码 20 2600.00 300.00 4900.00
穿戴设备 20 2600.00 300.00 4900.00

8大分类共190件上架商品(10件已下架),分布均匀。

21、步骤14:插入商品图片向量数据(200件商品 × 3张图 = 600条记录)

注意:说明:

输入 SQL:

INSERT INTO product_image_search (image_id, product_id, image_url, thumbnail_url, embedding, image_type, angle, background, metadata, status)SELECT 'IMG-' || lpad(sub.img_idx::text, 8, '0'), sub.pid, 'https://cdn.example.com/products/' || sub.pid || '/' || sub.itype || '.jpg', 'https://cdn.example.com/products/' || sub.pid || '/' || sub.itype || '_thumb.jpg', — 为同一商品的图片生成相近的向量(加噪声模拟不同角度) ( SELECT array_agg( sin(sub.base_seed + i * 0.01 + sub.noise_offset) * 0.5 + cos(sub.base_seed * 2 + i * 0.02) * 0.3 + (random() – 0.5) * sub.noise_factor )::vector(768) FROM generate_series(1, 768) i ), sub.itype, sub.angle_val, (ARRAY['white','transparent','scene','studio'])[(sub.img_idx % 4) + 1], jsonb_build_object( 'width', 800 + (sub.img_idx % 400), 'height', 800 + (sub.img_idx % 400), 'format', 'jpg', 'size_kb', 50 + (sub.img_idx % 200), 'model', 'CLIP-ViT-L/14', 'dim', 768 ), 1FROM ( SELECT p.product_id AS pid, (p.id – 1) * 3 + t.n AS img_idx, CASE t.n WHEN 1 THEN 'main' WHEN 2 THEN 'detail' ELSE 'scene' END AS itype, CASE t.n WHEN 1 THEN 'front' WHEN 2 THEN 'side' ELSE 'top' END AS angle_val, (p.id * 7.13 + (p.id % 8) * 100) AS base_seed, t.n * 0.05 AS noise_offset, CASE t.n WHEN 1 THEN 0.02 WHEN 2 THEN 0.05 ELSE 0.08 END AS noise_factor FROM product_catalog p CROSS JOIN (SELECT generate_series(1, 3) AS n) t) sub;

执行结果:INSERT 0 600 (1014.9ms)。

向量生成策略说明:

·base_seed:基于商品 ID 的确定性种子 (p.id * 7.13 + (p.id % 8) * 100)。

·noise_offset:同一商品不同图片类型有微小偏移(0.05 增量)。

·noise_factor:主图噪声最小(0.02),细节图中等(0.05),场景图最大(0.08)。

·保证同商品的3张图片向量高度相似(>0.99),不同商品向量有明显区分。

22、步骤15:查看图片数据统计

输入 SQL:

SELECT pis.image_type, count(*) AS image_count, count(DISTINCT pis.product_id) AS product_count, pg_size_pretty(sum(pg_column_size(pis.embedding))) AS vector_storageFROM product_image_search pisWHERE pis.status = 1GROUP BY pis.image_typeORDER BY image_count DESC;

输出结果:(2.5ms)

image_type image_count product_count vector_storage
detail 200 200 601KB
main 200 200 601KB
scene 200 200 601KB

600张图片数据插入成功,每类200张,向量存储约1.8MB。

23、步骤16:执行 ANALYZE 更新统计信息

输入 SQL:

ANALYZE product_catalog;ANALYZE product_image_search;ANALYZE image_search_log;

执行结果:ANALYZE (10.1ms合计)。

25、步骤17:创建 HNSW 向量索引 — 余弦距离(以图搜图核心索引)

注意:说明:

注意:HNSW 索引参数:m=16(每层连接数), ef_construction=128(构建精度),使用 vector_cosine_ops 余弦距离算子。

输入 SQL:

CREATE INDEX idx_product_image_embedding_cosineON product_image_searchUSING hnsw (embedding vector_cosine_ops)WITH (m = 16, ef_construction = 128);

执行结果:CREATE INDEX (180.3ms)。

HNSW 索引参数说明:

参数 值 说明
m 16 每个节点的最大连接数,越大精度越高但占用更多内存
ef_construction 128 构建时的搜索宽度,越大构建越慢但索引质量越好
算子类 vector_cosine_ops 余弦距离,适合归一化向量

26、步骤18:创建辅助 B-Tree / GIN 索引

输入 SQL:

— 商品ID关联索引CREATE INDEX idx_product_image_product_id ON product_image_search (product_id);– 图片类型过滤索引(部分索引)CREATE INDEX idx_product_image_type ON product_image_search (image_type) WHERE status = 1;– 商品分类索引(部分索引)CREATE INDEX idx_product_catalog_category ON product_catalog (category) WHERE status = 1;– 品牌过滤索引CREATE INDEX idx_product_catalog_brand ON product_catalog (brand) WHERE status = 1;– 价格范围索引CREATE INDEX idx_product_catalog_price ON product_catalog (price) WHERE status = 1;– 商品标签 GIN 索引CREATE INDEX idx_product_catalog_tags ON product_catalog USING GIN (tags) WHERE status = 1;– 图片元数据 GIN 索引CREATE INDEX idx_product_image_metadata ON product_image_search USING GIN (metadata);– 搜索日志时间索引CREATE INDEX idx_search_log_created ON image_search_log (created_at DESC);

执行结果:全部 CREATE INDEX 成功。

27、步骤19:查看所有索引信息

输入 SQL:

SELECT indexrelname AS index_name, relname AS table_name, pg_size_pretty(pg_relation_size(indexrelid)) AS index_size, idx_scan AS scansFROM pg_stat_user_indexesWHERE schemaname = 'public' AND relname IN ('product_image_search', 'product_catalog', 'image_search_log')ORDER BY pg_relation_size(indexrelid) DESC;

输出结果:(3.1ms)。

index_name table_name index_size scans
idx_product_image_embedding_cosine product_image_search 2408KB 0
idx_product_image_metadata product_image_search 64KB 0
product_image_search_image_id_key product_image_search 40KB 0
idx_product_image_product_id product_image_search 32KB 0
product_image_search_pkey product_image_search 32KB 0
idx_product_catalog_price product_catalog 16KB 0
idx_product_catalog_tags product_catalog 16KB 0
product_catalog_pkey product_catalog 16KB 0
product_catalog_product_id_key product_catalog 16KB 600
idx_product_catalog_category product_catalog 16KB 0
idx_product_catalog_brand product_catalog 16KB 0
idx_product_image_type product_image_search 16KB 0
idx_search_log_created image_search_log 8192bytes 0
image_search_log_pkey image_search_log 8192bytes 0

共14个索引,HNSW 向量索引占2408KB(最大),product_catalog_product_id_key 已有600次扫描(插入图片时的外键查找)。

29、步骤20:设置 HNSW 搜索参数

输入 SQL:

SET hnsw.ef_search = 100;SET hnsw.iterative_scan = relaxed_order;

执行结果:SET (3.0ms)。

参数 值 说明
hnsw.ef_search 100 搜索时的候选集大小,越大越精确但更慢
hnsw.iterative_scan relaxed_order 迭代扫描优化,支持带过滤条件的 HNSW 搜索

30、场景1:基础以图搜图 — 用一张商品主图搜索相似商品 (Top10)

注意:说明:

注意:用商品 P000001 的主图向量,搜索除自身外最相似的10张商品图片。

输入 SQL:

WITH query_image AS ( SELECT embedding FROM product_image_search WHERE product_id = 'P000001' AND image_type = 'main' LIMIT 1)SELECT pis.image_id, pis.product_id, pc.product_name, pc.category, pc.brand, pc.price, pis.image_type, round((1 – (pis.embedding <=> qi.embedding))::numeric, 4) AS similarityFROM product_image_search pisCROSS JOIN query_image qiJOIN product_catalog pc ON pc.product_id = pis.product_idWHERE pis.status = 1 AND pis.product_id != 'P000001'ORDER BY pis.embedding <=> qi.embeddingLIMIT 10;

输出结果:(10.5ms)。

image_id product_id product_name category brand price image_type similarity
IMG-00000565 P000189 无线耳机 Bose QC 189 音频设备 JBL 4000.00 main 0.9998
IMG-00000235 P000079 咖啡机 Fellow 79 家用电器 DeLonghi 3000.00 main 0.9989
IMG-00000566 P000189 无线耳机 Bose QC 189 音频设备 JBL 4000.00 detail 0.9984
IMG-00000293 P000098 运动鞋 Gel 98 运动户外 Nike 4900.00 detail 0.9964
IMG-00000236 P000079 咖啡机 Fellow 79 家用电器 DeLonghi 3000.00 detail 0.9964
IMG-00000274 P000092 智能手表 Garmin 92 穿戴设备 Amazfit 4300.00 main 0.9960
IMG-00000292 P000098 运动鞋 Gel 98 运动户外 Nike 4900.00 main 0.9956
IMG-00000294 P000098 运动鞋 Gel 98 运动户外 Nike 4900.00 scene 0.9947
IMG-00000567 P000189 无线耳机 Bose QC 189 音频设备 JBL 4000.00 scene 0.9946
IMG-00000254 P000085 无线耳机 Bose QC 85 音频设备 Jabra 3600.00 detail 0.9924

Top 10相似度范围0.9924 ~ 0.9998,响应时间10.5ms。

31、场景2:同一商品的多角度图片匹配验证

注意:说明:

注意:用 P000010 主图搜索,验证同商品的细节图/场景图排在最前面。

输入 SQL:

WITH query AS ( SELECT embedding, product_id FROM product_image_search WHERE product_id = 'P000010' AND image_type = 'main' LIMIT 1)SELECT pis.image_id, pis.product_id, pis.image_type, pis.angle, CASE WHEN pis.product_id = q.product_id THEN '✅ 同商品' ELSE '其他商品' END AS match_type, round((1 – (pis.embedding <=> q.embedding))::numeric, 4) AS similarityFROM product_image_search pisCROSS JOIN query qWHERE pis.status = 1ORDER BY pis.embedding <=> q.embeddingLIMIT 10;

输出结果:(9.1ms)。

image_id product_id image_type angle match_type similarity
IMG-00000028 P000010 main front 同商品 1.0000
IMG-00000592 P000198 main front 其他商品 0.9998
IMG-00000358 P000120 main front 其他商品 0.9987
IMG-00000359 P000120 detail side 其他商品 0.9984
IMG-00000593 P000198 detail side 其他商品 0.9984
IMG-00000029 P000010 detail side 同商品 0.9983
IMG-00000320 P000107 detail side 其他商品 0.9964
IMG-00000301 P000101 main front 其他商品 0.9958
IMG-00000360 P000120 scene top 其他商品 0.9956
IMG-00000319 P000107 main front 其他商品 0.9955

自身主图匹配度1.0000(完全一致),同商品细节图匹配度0.9983,验证了同商品图片向量的高相似度。

32、场景3:带品类过滤的以图搜图 – 只在“手机数码”分类中搜索

注意:说明:

注意:限定在“手机数码”分类下,仅搜索主图。

输入 SQL:

WITH query_image AS ( SELECT embedding FROM product_image_search WHERE product_id = 'P000001' AND image_type = 'main' LIMIT 1)SELECT pis.image_id, pis.product_id, pc.product_name, pc.category, pc.brand, pc.price, round((1 – (pis.embedding <=> qi.embedding))::numeric, 4) AS similarityFROM product_image_search pisCROSS JOIN query_image qiJOIN product_catalog pc ON pc.product_id = pis.product_idWHERE pis.status = 1 AND pc.category = '手机数码' AND pis.image_type = 'main' AND pis.product_id != 'P000001'ORDER BY pis.embedding <=> qi.embeddingLIMIT 10;

输出结果:(2.5ms)。

image_id product_id product_name category brand price similarity
IMG-00000022 P000008 智能手机 Pro 8 手机数码 Samsung 900.00 0.9750
IMG-00000310 P000104 智能手机 Pro 104 手机数码 Samsung 500.00 0.9638
IMG-00000334 P000112 智能手机 Pro 112 手机数码 Huawei 1300.00 0.9367
IMG-00000598 P000200 智能手机 Pro 200 手机数码 Samsung 100.00 0.7316
IMG-00000046 P000016 智能手机 Pro 16 手机数码 Huawei 1700.00 0.6672
IMG-00000286 P000096 智能手机 Pro 96 手机数码 Apple 4700.00 0.6388
IMG-00000358 P000120 智能手机 Pro 120 手机数码 Xiaomi 2100.00 0.5713
IMG-00000574 P000192 智能手机 Pro 192 手机数码 Apple 4300.00 0.2146
IMG-00000070 P000024 智能手机 Pro 24 手机数码 Xiaomi 2500.00 0.1986
IMG-00000382 P000128 智能手机 Pro 128 手机数码 Apple 2900.00 0.1082

品类过滤生效,结果全部为“手机数码”分类,响应时间仅2.5ms。

33、场景4:带价格区间的以图搜图 – 100 ~ 2000元

注意:说明:

注意:以图搜图 + 价格过滤,适用于“找同款但预算有限”场景。

输入 SQL:

WITH query_image AS ( SELECT embedding FROM product_image_search WHERE product_id = 'P000003' AND image_type = 'main' LIMIT 1)SELECT pis.image_id, pis.product_id, pc.product_name, pc.category, pc.price, round((1 – (pis.embedding <=> qi.embedding))::numeric, 4) AS similarityFROM product_image_search pisCROSS JOIN query_image qiJOIN product_catalog pc ON pc.product_id = pis.product_idWHERE pis.status = 1 AND pc.price BETWEEN 100 AND 2000 AND pis.image_type = 'main' AND pis.product_id != 'P000003'ORDER BY pis.embedding <=> qi.embeddingLIMIT 10;

输出结果:(3.9ms)。

image_id product_id product_name category price similarity
IMG-00000337 P000113 笔记本电脑 MacBook 113 电脑办公 1400.00 0.9987
IMG-00000046 P000016 智能手机 Pro 16 手机数码 1700.00 0.9987
IMG-00000298 P000100 智能手表 Garmin 100 穿戴设备 100.00 0.9956
IMG-00000319 P000107 机械键盘 HHKB 107 外设配件 800.00 0.9905
IMG-00000025 P000009 笔记本电脑 MacBook 9 电脑办公 1000.00 0.9832
IMG-00000028 P000010 运动鞋 Gel 10 运动户外 1100.00 0.9736
IMG-00000316 P000106 运动鞋 Gel 106 运动户外 700.00 0.9624
IMG-00000301 P000101 无线耳机 Bose QC 101 音频设备 200.00 0.9496
IMG-00000340 P000114 运动鞋 Gel 114 运动户外 1500.00 0.9346
IMG-00000004 P000002 运动鞋 Gel 2 运动户外 300.00 0.9176

价格过滤生效,所有结果价格在100 ~ 2000元区间内,响应时间3.9ms。

34、场景5:带相似度阈值的以图搜图 – 只返回 similarity > 0.85

注意:说明:

注意:设置相似度阈值0.85,过滤掉不够相似的结果。

输入 SQL:

WITH query_image AS ( SELECT embedding FROM product_image_search WHERE product_id = 'P000010' AND image_type = 'main' LIMIT 1)SELECT pis.image_id, pis.product_id, pc.product_name, pc.category, round((1 – (pis.embedding <=> qi.embedding))::numeric, 4) AS similarityFROM product_image_search pisCROSS JOIN query_image qiJOIN product_catalog pc ON pc.product_id = pis.product_idWHERE pis.status = 1 AND 1 – (pis.embedding <=> qi.embedding) > 0.85ORDER BY pis.embedding <=> qi.embeddingLIMIT 20;

输出结果:(8.0ms)。

image_id product_id product_name category similarity
IMG-00000028 P000010 运动鞋 Gel 10 运动户外 1.0000
IMG-00000592 P000198 双肩背包 Herschel 198 箱包皮具 0.9998
IMG-00000358 P000120 智能手机 Pro 120 手机数码 0.9987
IMG-00000359 P000120 智能手机 Pro 120 手机数码 0.9984
IMG-00000593 P000198 双肩背包 Herschel 198 箱包皮具 0.9984
IMG-00000029 P000010 运动鞋 Gel 10 运动户外 0.9983
IMG-00000320 P000107 机械键盘 HHKB 107 外设配件 0.9964
IMG-00000301 P000101 无线耳机 Bose QC 101 音频设备 0.9958
IMG-00000360 P000120 智能手机 Pro 120 手机数码 0.9956
IMG-00000319 P000107 机械键盘 HHKB 107 外设配件 0.9955
… … … … …
IMG-00000303 P000101 无线耳机 Bose QC 101 音频设备 0.9867

阈值过滤生效,返回的20条结果相似度全部 > 0.85(最低0.9867)。

35、场景6:跨品牌相似商品搜索 – 找不同品牌的同类商品

注意:说明:

注意:按品牌去重,每个品牌只保留最相似的一件商品,适合“竞品对比”场景。

输入 SQL:

WITH query_image AS ( SELECT embedding FROM product_image_search WHERE product_id = 'P000001' AND image_type = 'main' LIMIT 1),similar_products AS ( SELECT DISTINCT ON (pc.brand) pis.product_id, pc.product_name, pc.brand, pc.category, pc.price, round((1 – (pis.embedding <=> qi.embedding))::numeric, 4) AS similarity FROM product_image_search pis CROSS JOIN query_image qi JOIN product_catalog pc ON pc.product_id = pis.product_id WHERE pis.status = 1 AND pis.image_type = 'main' AND pis.product_id != 'P000001' ORDER BY pc.brand, pis.embedding <=> qi.embedding)SELECT * FROM similar_productsORDER BY similarity DESCLIMIT 10;

输出结果:(5.7ms)。

product_id product_name brand category price similarity
P000189 无线耳机 Bose QC 189 JBL 音频设备 4000.00 0.9998
P000079 咖啡机 Fellow 79 DeLonghi 家用电器 3000.00 0.9989
P000092 智能手表 Garmin 92 Amazfit 穿戴设备 4300.00 0.9960
P000098 运动鞋 Gel 98 Nike 运动户外 4900.00 0.9956
P000105 笔记本电脑 MacBook 105 Dell 电脑办公 600.00 0.9910
P000085 无线耳机 Bose QC 85 Jabra 音频设备 3600.00 0.9906
P000183 咖啡机 Fellow 183 Breville 家用电器 3400.00 0.9844
P000195 机械键盘 HHKB 195 Cherry 外设配件 4600.00 0.9833
P000196 智能手表 Garmin 196 Apple 穿戴设备 4700.00 0.9757
P000008 智能手机 Pro 8 Samsung 手机数码 900.00 0.9750

品牌去重生效,10个结果来自10个不同品牌,可直接用于竞品对比分析。

36、场景7:以图搜图 + 文本关键词混合搜索

注意:说明:

注意:图像向量相似度70% + 文本相似度30%混合排序。

输入 SQL:

WITH query_image AS ( SELECT embedding FROM product_image_search WHERE product_id = 'P000005' AND image_type = 'main' LIMIT 1)SELECT pis.product_id, pc.product_name, pc.category, pc.price, round((1 – (pis.embedding <=> qi.embedding))::numeric, 4) AS image_sim, round(similarity(pc.product_name, '智能手表')::numeric, 4) AS text_sim, round( (0.7 * (1 – (pis.embedding <=> qi.embedding)) + 0.3 * similarity(pc.product_name, '智能手表'))::numeric, 4 ) AS hybrid_scoreFROM product_image_search pisCROSS JOIN query_image qiJOIN product_catalog pc ON pc.product_id = pis.product_idWHERE pis.status = 1 AND pis.image_type = 'main'ORDER BY hybrid_score DESCLIMIT 10;

输出结果:(6.5ms)。

product_id product_name category price image_sim text_sim hybrid_score
P000005 无线耳机 Bose QC 5 音频设备 600.00 1.0000 0.0000 0.7000
P000128 智能手机 Pro 128 手机数码 2900.00 0.9998 0.0000 0.6998
P000018 运动鞋 Gel 18 运动户外 1900.00 0.9988 0.0000 0.6992
P000115 机械键盘 HHKB 115 外设配件 1600.00 0.9987 0.0000 0.6991
P000102 双肩背包 Herschel 102 箱包皮具 300.00 0.9955 0.0000 0.6968
P000109 无线耳机 Bose QC 109 音频设备 1000.00 0.9910 0.0000 0.6937
P000024 智能手机 Pro 24 手机数码 2500.00 0.9908 0.0000 0.6936
P000122 运动鞋 Gel 122 运动户外 2300.00 0.9842 0.0000 0.6889
P000011 机械键盘 HHKB 11 外设配件 1200.00 0.9837 0.0000 0.6886
P000199 咖啡机 Fellow 199 家用电器 5000.00 0.9830 0.0000 0.6881

混合搜索生效,hybrid_score = 0.7 × image_sim + 0.3 × text_sim。当搜索关键词“智能手表”与查询图片(无线耳机)不一致时,图像相似度主导排序结果。

38、步骤21:EXPLAIN ANALYZE – 验证 HNSW 索引使用

输入 SQL:

EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)SELECT pis.image_id, pis.product_id, 1 – (pis.embedding <=> (SELECT embedding FROM product_image_search WHERE id = 1)) AS similarityFROM product_image_search pisWHERE pis.status = 1ORDER BY pis.embedding <=> (SELECT embedding FROM product_image_search WHERE id = 1)LIMIT 10;

输出结果:(10.3ms)。

Limit (cost=69.55..69.58 rows=10 width=37) (actual time=…) Buffers: shared hit=7237 InitPlan 1 (returns $0) -> Index Scan using product_image_search_pkey on pro… Index Cond: (id = 1) Buffers: shared hit=6 InitPlan 2 (returns $1) -> Index Scan using product_image_search_pkey on pro… Index Cond: (id = 1) Buffers: shared hit=3 -> Sort (cost=52.97..54.47 rows=600 width=37) (actual…) Sort Key: ((pis.embedding <=> $1)) Sort Method: top-N heapsort Memory: 25kB Buffers: shared hit=7237 -> Seq Scan on product_image_search pis (cost=0…) Filter: (status = 1) Buffers: shared hit=7237 Planning: Buffers: shared hit=12 Planning Time: 0.141 ms Execution Time: 8.436 ms

执行计划分析:

·执行时间:8.436ms。

·缓冲区命中:shared hit=7237(全部从内存读取,零磁盘 IO)。

·排序方式:top-N heapsort(仅排序前10条),内存消耗仅25KB。

注意:由于数据量较小(600条),PostgreSQL 优化器选择了 Seq Scan + Sort 而非 HNSW Index Scan。当数据量达到万级以上时,优化器会自动切换到 HNSW 索引扫描。

39、步骤22:搜索日志写入

输入 SQL:

INSERT INTO image_search_log (session_id, query_vector, query_image_url, result_count, top_similarity, search_time_ms, filter_category)SELECT 'sess-test-001', embedding, 'https://user-upload.example.com/query.jpg', 10, 0.95, 15.3, '手机数码'FROM product_image_searchWHERE id = 1;

执行结果:INSERT 0 1 (2.2ms)。

查看搜索日志:

SELECT session_id, result_count, top_similarity, search_time_ms, filter_category, created_atFROM image_search_logORDER BY created_at DESCLIMIT 5;

输出结果:

session_id result_count top_similarity search_time_ms filter_category created_at
sess-test-001 10 0.95 15.3 手机数码 2026-03-24 17:26:15.941036+08:00

40、步骤23:索引预热 — 将 HNSW 索引加载到共享缓冲区

输入 SQL:

SELECT pg_prewarm('idx_product_image_embedding_cosine') AS pages_loaded;

输出结果:

pages_loaded
301

HNSW 索引301页已预热到共享缓冲区,后续查询将无需磁盘 IO。

42、步骤24:表大小与存储统计

输入 SQL:

SELECT c.relname AS table_name, pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size, pg_size_pretty(pg_relation_size(c.oid)) AS data_size, pg_size_pretty(pg_indexes_size(c.oid)) AS index_size, s.n_live_tup AS estimated_rowsFROM pg_class cJOIN pg_stat_user_tables s ON s.relid = c.oidWHERE c.relname IN ('product_catalog', 'product_image_search', 'image_search_log')ORDER BY pg_total_relation_size(c.oid) DESC;

输出结果:(3.2ms)。

table_name total_size data_size index_size estimated_rows
product_image_search 5312KB 224KB 2592KB 600
product_catalog 176KB 48KB 96KB 200
image_search_log 64KB 8192bytes 32KB 0

存储分析:

·向量表 product_image_search 总占用5.3MB,其中 HNSW 索引占2.5MB(47%)。

·每条768维向量记录约3KB(数据 + 索引)。

·600条图片向量数据 + 索引合计仅5.3MB。

43、步骤25:向量索引使用统计

输入 SQL:

SELECT indexrelname AS index_name, relname AS table_name, pg_size_pretty(pg_relation_size(indexrelid)) AS index_size, idx_scan AS scan_count, idx_tup_read AS rows_read, idx_tup_fetch AS rows_fetchedFROM pg_stat_user_indexesWHERE schemaname = 'public' AND relname IN ('product_image_search', 'product_catalog', 'image_search_log')ORDER BY pg_relation_size(indexrelid) DESC;

输出结果:(2.3ms)。

index_name table_name index_size scan_count rows_read rows_fetched
idx_product_image_embedding_cosine product_image_search 2408KB 0 0 0
idx_product_image_metadata product_image_search 64KB 0 0 0
product_image_search_image_id_key product_image_search 40KB 0 0 0
idx_product_image_product_id product_image_search 32KB 0 0 0
product_catalog_product_id_key product_catalog 16KB 600 600 600

44、步骤26:缓冲区命中率检查

输入 SQL:

SELECT schemaname, relname AS table_name, heap_blks_hit AS buffer_hits, heap_blks_read AS disk_reads, CASE WHEN (heap_blks_hit + heap_blks_read) > 0 THEN round(100.0 * heap_blks_hit / (heap_blks_hit + heap_blks_read), 1) ELSE 0 END AS hit_ratio_pctFROM pg_statio_user_tablesWHERE relname IN ('product_image_search', 'product_catalog')ORDER BY relname;

输出结果:(2.7ms)。

schemaname table_name buffer_hits disk_reads hit_ratio_pct
public product_catalog 1419 8 99.4%
public product_image_search 1251 30 97.7%

两张核心表的缓冲区命中率均 > 97%,表明内存资源充足、磁盘 IO 极少。

45、步骤27:数据库整体大小

输入 SQL:

SELECT pg_size_pretty(pg_database_size(current_database())) AS total_db_size, (SELECT count(*) FROM product_catalog) AS total_products, (SELECT count(*) FROM product_image_search) AS total_images, (SELECT count(*) FROM image_search_log) AS total_searches;

输出结果:(2.4ms)。

total_db_size total_products total_images total_searches
15MB 200 600 1
赞 (0)
上一篇 4天前
下一篇 4天前