tencent cloud

TencentDB for PostgreSQL

DocumentationTencentDB for PostgreSQLAI PracticeIntelligent Customer Service Vector Search System Based on PostgreSQL + pgvector

Intelligent Customer Service Vector Search System Based on PostgreSQL + pgvector

PDF
Focus Mode
Font Size
Last updated: 2026-04-08 15:05:11
This article introduces the AI implementation of an intelligent customer service vector search system based on PostgreSQL + pgvector.

Background

With the intelligent transformation of enterprise customer service scenarios, traditional keyword-based customer service systems have become inadequate in meeting users' demands for semantic understanding and multimodal search.
This solution builds an intelligent customer service system based on PostgreSQL 15 + pgvector 0.8.2 extensions, supporting vector-based search for both text and images. By leveraging (Cosine Similarity) for high-precision semantic matching, it enhances the intelligence level and user experience of the customer service system.

Contents

1. Environment Detection and Extension Installation

Step 1: Obtain database environment information

1. Get the PostgreSQL version.
Input SQL:
SELECT version();
output results (1 row, took 2.1ms):
version
PostgreSQL 15.14 on x86_64-pc-linux-gnu, compiled by gcc (GCC)
2. Obtain key database parameters.
Input SQL:
SELECT name, setting, unit
FROM pg_settings
WHERE name IN (
'shared_buffers', 'effective_cache_size', 'work_mem',
'maintenance_work_mem', 'max_connections',
'max_parallel_workers', 'max_parallel_workers_per_gather',
'wal_level'
)
ORDER BY name;
output results (8 rows, took 3.6ms):
name
setting
unit
effective_cache_size
524288
8KB
maintenance_work_mem
524288
KB
max_connections
2048
-
max_parallel_workers
8
-
max_parallel_workers_per_gather
2
-
shared_buffers
1572864
8KB
wal_level
logical
-
work_mem
4096
KB
Note:
shared_buffers = 12GB, effective_cache_size = 4GB, maintenance_work_mem = 512MB, meeting the requirements for vector search scenarios.

Step 2: Install the pgvector extension

Input SQL:
CREATE EXTENSION IF NOT EXISTS vector;
Output: executed successfully, took 1.7ms.
Verify SQL:
SELECT extname, extversion FROM pg_extension WHERE extname = 'vector';
output results (1 row, took 1.9ms):
extname
extversion
vector
0.8.2
Note:
pgvector 0.8.2 is installed, supporting HNSW index and vector_cosine_ops operator.

2. Clean up old tables

Step 3: Clean the old table (if it exists)

Delete in dependency order to ensure idempotent execution:
DROP TABLE IF EXISTS chat_messages CASCADE; -- took 5.7ms
DROP TABLE IF EXISTS chat_sessions CASCADE; -- took 2.7ms
DROP TABLE IF EXISTS text_vectors CASCADE; -- took 3.7ms
DROP TABLE IF EXISTS image_vectors CASCADE; -- took 3.4ms
Note:
The script supports idempotent execution and can be run repeatedly without errors.

3. Create the core table schema

Step 4: Create the image_vectors table (768-dimensional)

Input SQL:
CREATE TABLE image_vectors (
id BIGSERIAL PRIMARY KEY,
image_id VARCHAR(64) NOT NULL UNIQUE,
image_url TEXT NOT NULL,
thumbnail_url TEXT,
embedding vector(768) NOT NULL, -- generated by large model, 768-dimensional output
category VARCHAR(100),
tags TEXT[],
source VARCHAR(50) DEFAULT 'upload',
metadata JSONB DEFAULT '{}',
status SMALLINT DEFAULT 1,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);

COMMENT ON TABLE image_vectors IS 'image vector table - intelligent customer service image search';
COMMENT ON COLUMN image_vectors.embedding IS '768-dimensional image vector generated by large model';
Output: executed successfully, took 5.3ms + 1.8ms.
Verify table schema:
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = 'image_vectors'
ORDER BY ordinal_position;
output results (12 rows, took 7.6ms):
column_name
data_type
is_nullable
column_default
id
bigint
NO
nextval('image_vectors_id_seq'::regclass)
image_id
character varying
NO
-
image_url
text
NO
-
thumbnail_url
text
YES
-
embedding
USER-DEFINED
NO
-
category
character varying
YES
-
tags
ARRAY
YES
-
source
character varying
YES
'upload'::character varying
metadata
jsonb
YES
'{}'::jsonb
status
smallint
YES
1
created_at
timestamp with time zone
YES
now()
updated_at
timestamp with time zone
YES
now()

Step 5: Create the text vector table text_vectors (1024-dimensional)

Input SQL:
CREATE TABLE text_vectors (
id BIGSERIAL PRIMARY KEY,
doc_id VARCHAR(64) NOT NULL UNIQUE,
title TEXT,
content TEXT NOT NULL,
content_type VARCHAR(20) DEFAULT 'faq',
embedding vector(1024) NOT NULL, -- 1024-dimensional output generated by large model
category VARCHAR(100),
tags TEXT[],
source VARCHAR(100),
metadata JSONB DEFAULT '{}',
priority SMALLINT DEFAULT 0,
status SMALLINT DEFAULT 1,
expire_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);

COMMENT ON TABLE text_vectors IS 'text vector table - intelligent customer service knowledge base search';
COMMENT ON COLUMN text_vectors.embedding IS '1024-dimensional text vector generated by large model';
Output: executed successfully, took 4.3ms + 1.7ms.
Verify table schema (15 columns):
column_name
data_type
is_nullable
column_default
id
bigint
NO
nextval('text_vectors_id_seq'::regclass)
doc_id
character varying
NO
-
title
text
YES
-
content
text
NO
-
content_type
character varying
YES
'faq'::character varying
embedding
USER-DEFINED
NO
-
category
character varying
YES
-
tags
ARRAY
YES
-
source
character varying
YES
-
metadata
jsonb
YES
'{}'::jsonb
priority
smallint
YES
0
status
smallint
YES
1
expire_at
timestamp with time zone
YES
-
created_at
timestamp with time zone
YES
now()
updated_at
timestamp with time zone
YES
now()

Step 6: Create conversation record table chat_sessions + chat_messages

Input SQL (session table):
CREATE TABLE chat_sessions (
id BIGSERIAL PRIMARY KEY,
session_id VARCHAR(64) NOT NULL UNIQUE,
user_id VARCHAR(64) NOT NULL,
channel VARCHAR(20) DEFAULT 'web',
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMPTZ DEFAULT NOW(),
closed_at TIMESTAMPTZ
);
Output: executed successfully, took 3.5ms.
Input SQL (message table):
CREATE TABLE chat_messages (
id BIGSERIAL PRIMARY KEY,
session_id VARCHAR(64) NOT NULL,
role VARCHAR(10) NOT NULL,
content_type VARCHAR(20) DEFAULT 'text',
content TEXT NOT NULL,
image_url TEXT,
embedding vector(1024), -- 1024-dimensional vector used for similar question recommendation
matched_doc_ids TEXT[],
confidence REAL,
feedback SMALLINT,
created_at TIMESTAMPTZ DEFAULT NOW()
);

COMMENT ON TABLE chat_messages IS 'chat messages table - containing vector field used for similar question recommendation';
Output: executed successfully, took 3.5ms + 1.7ms.
Confirm all core tables:
SELECT table_name, pg_size_pretty(pg_total_relation_size(quote_ident(table_name)))
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name IN ('image_vectors', 'text_vectors', 'chat_sessions', 'chat_messages')
ORDER BY table_name;
output results (4 rows, took 3.3ms):
table_name
pg_size_pretty
chat_messages
16KB
chat_sessions
16KB
image_vectors
24KB
text_vectors
24KB

4. Create Auxiliary Index

Step 7: Create B-Tree + GIN auxiliary index

A total of 11 auxiliary indexes were created:
#
Index Name
Type
Description
Duration
1
idx_image_vectors_category
B-Tree (partial)
Image classification, WHERE status=1
2.2ms
2
idx_image_vectors_status
B-Tree
Image status
2.1ms
3
idx_text_vectors_category
B-Tree (partial)
Text classification, WHERE status=1
2.1ms
4
idx_text_vectors_content_type
B-Tree (partial)
Text type, WHERE status=1
2.1ms
5
idx_text_vectors_priority
B-Tree (partial)
Text priority descending, WHERE status=1
2.1ms
6
idx_chat_messages_session
B-Tree
message-session composite index
2.2ms
7
idx_chat_sessions_user
B-Tree
session-user index (time descending)
2.1ms
8
idx_image_vectors_tags
GIN (partial)
image tag array, WHERE status=1
2.2ms
9
idx_text_vectors_tags
GIN (partial)
text tag array, WHERE status=1
2.0ms
10
idx_image_vectors_metadata
GIN
image JSONB metadata
2.0ms
11
idx_text_vectors_metadata
GIN
text JSONB metadata
2.0ms
SQL examples for creating various indexes (partial index):
-- Image classification index (partial index, only records with status=1)
CREATE INDEX idx_image_vectors_category ON image_vectors (category) WHERE status = 1;

-- Image Tag GIN index
CREATE INDEX idx_image_vectors_tags ON image_vectors USING GIN (tags) WHERE status = 1;

-- Image metadata JSONB GIN index
CREATE INDEX idx_image_vectors_metadata ON image_vectors USING GIN (metadata);
List all created indexes:
SELECT indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'public'
AND tablename IN ('image_vectors', 'text_vectors', 'chat_sessions', 'chat_messages')
ORDER BY tablename, indexname;
output results (18 rows, took 3.4ms):
indexname
Table
chat_messages_pkey
chat_messages
idx_chat_messages_session
chat_messages
chat_sessions_pkey
chat_sessions
chat_sessions_session_id_key
chat_sessions
idx_chat_sessions_user
chat_sessions
idx_image_vectors_category
image_vectors
idx_image_vectors_metadata
image_vectors
idx_image_vectors_status
image_vectors
idx_image_vectors_tags
image_vectors
image_vectors_image_id_key
image_vectors
image_vectors_pkey
image_vectors
idx_text_vectors_category
text_vectors
idx_text_vectors_content_type
text_vectors
idx_text_vectors_metadata
text_vectors
idx_text_vectors_priority
text_vectors
idx_text_vectors_tags
text_vectors
text_vectors_doc_id_key
text_vectors
text_vectors_pkey
text_vectors

5. Insert intelligent customer service sample data

Step 8: Insert FAQ knowledge base data (12 entries)

Note:
The embedding field must be generated using a large model.
Input SQL example (faq-001):
INSERT INTO text_vectors (doc_id, title, content, content_type, embedding, category, tags, priority)
VALUES (
'faq-001',
'How to reset password',
'You can reset your password by following these steps: 1. Click the "forgot password" link on the login page; 2. Input your registered email;
3. Check your inbox for the reset email; 4. Click the link in the email to set a new password.',
'faq',
(SELECT array_agg(random())::vector(1024) FROM generate_series(1, 1024)),
'account management',
ARRAY['account management', 'faq'],
2
);
A total of 12 FAQ data items were inserted, all successfully:
doc_id
Title
Category
Priority
Duration
faq-001
How to reset password
Account management
2
2.6ms
faq-002
How to modify personal information
Account management
1
2.0ms
faq-003
Order delivery time
Order Logistics
2
1.9ms
faq-004
Return and Refund Process
After-Sales Service
2
1.9ms
faq-005
How to use coupons
Promotions
1
1.9ms
faq-006
Delivery Area and Shipping Fee
Order Logistics
1
2.1ms
faq-007
How to Contact Customer Service
Customer Service Help
2
2.0ms
faq-008
Account Security Settings
Account management
1
2.0ms
faq-009
Payment Methods Explanation
Payment-Related
1
2.0ms
faq-010
Membership Tier Benefits
Membership Services
1
2.0ms
faq-011
Product Warranty Policy
After-Sales Service
1
2.1ms
faq-012
Invoice Issuance Instructions
Payment-Related
0
2.0ms
Verify SQL:
SELECT doc_id, title, category, priority, status,
LEFT(content, 40) || '...' AS content_preview
FROM text_vectors
ORDER BY priority DESC, doc_id;
Output result (12 rows, time taken: 1.9ms):
doc_id
title
category
priority
status
content_preview
faq-001
How to reset password
Account management
2
1
You can reset your password by following these steps: 1. Click the "Forgot Password" link on the login page; 2. ...
faq-003
Order delivery time
Order Logistics
2
1
Regular orders will be shipped within 48 hours after payment, while expedited orders will be shipped within 24 hours. Shipping times during holidays may be extended...
faq-004
Return and Refund Process
After-Sales Service
2
1
Return request process: 1. Go to "My Orders" to find the corresponding order; 2. Click "Request Return"; 3. ...
faq-007
How to Contact Customer Service
Customer Service Help
2
1
You can contact customer service through the following methods: 1. Click "Online Support" in the APP to transfer to a live agent; 2. Call...
faq-002
How to modify personal information
Account management
1
1
After logging in, go to the "Personal Center" and click the "Edit Profile" button to modify personal information such as nickname, profile picture, phone number, and so on...
faq-005
How to use coupons
Promotions
1
1
On the Shopping Cart page or Checkout page, click "use coupon" and select available coupons to apply the discount. Note: discount...
faq-006
Delivery Area and Shipping Fee
Order Logistics
1
1
We support nationwide delivery (excluding Hong Kong (China), Macao (China), and Taiwan (China) and overseas regions). Orders over CNY 99 qualify for free shipping, while orders under CNY 99 incur a shipping fee of CNY 8...
faq-008
Account Security Settings
Account management
1
1
We recommend that you enable the following security settings: 1. Set a complex password (containing letters, numbers, and special characters); 2. ...
faq-009
Payment Methods Explanation
Payment-Related
1
1
We support the following payment methods: WeChat Pay, Alipay, bank card quick payment, credit card payment, Huabei installment.
faq-010
Membership Tier Benefits
Membership Services
1
1
Membership is divided into four tiers: Regular, Silver, Gold, and Diamond. Upgrade eligibility is based on annual spending amount, with different discounts enjoyed at each level...
faq-011
Product Warranty Policy
After-Sales Service
1
1
Electronic products come with a 1-year warranty; apparel items are eligible for no-questions-asked returns within 7 days; food items can be returned or exchanged within their shelf life...
faq-012
Invoice Issuance Instructions
Payment-Related
0
1
After order completion, you can apply for an e-invoice in "My Orders", supporting both VAT ordinary invoices and VAT special invoices.

Step 9: Insert image vector example data (8 records)

Note:
The embedding field must be generated using a large model.
Input SQL example (img-001):
INSERT INTO image_vectors (image_id, image_url, embedding, category, tags, metadata)
VALUES (
'img-001',
'https://cdn.example.com/products/phone_x1.jpg',
(SELECT array_agg(random())::vector(768) FROM generate_series(1, 768)),
Product Images
ARRAY['Product Images', 'smart_cs'],
'{"width":800,"height":600,"format":"jpg"}'::jsonb
);
A total of 8 image data items were inserted, all successfully:
image_id
Category
URL
Duration
img-001
Product Images
.../products/phone_x1.jpg
2.3ms
img-002
Product Images
.../products/laptop_pro.jpg
2.0ms
img-003
Product Images
.../products/headphone_bt.jpg
2.0ms
img-004
Fault Images
.../faults/screen_crack.jpg
1.9ms
img-005
Fault Images
.../faults/battery_swell.jpg
1.9ms
img-006
Manual
.../manuals/phone_guide.jpg
1.9ms
img-007
Product Images
.../products/tablet_air.jpg
1.9ms
img-008
Product Images
.../products/watch_smart.jpg
1.9ms

Step 10: Insert dialogue example data

Session insertion SQL:
INSERT INTO chat_sessions (session_id, user_id, channel, status)
VALUES
('sess-001', 'user-1001', 'web', 'active'),
('sess-002', 'user-1002', 'wechat', 'active'),
('sess-003', 'user-1003', 'app', 'closed');
Output: 3 rows affected, took 1.9ms
A total of 8 conversation entries were inserted (3 sessions):
session_id
role
content_type
Content Preview
Duration
sess-001
user
text
What should I do if I forgot my password?
2.1ms
sess-001
assistant
text
You can reset your password via the "Forgot Password" link...
2.0ms
sess-001
user
text
How long does it take for the password reset to take effect?
1.9ms
sess-002
user
text
When will my order be delivered?
2.2ms
sess-002
assistant
text
Regular orders will be shipped within 48 hours after payment...
2.1ms
sess-002
user
image
This product has a quality issue.
2.0ms
sess-003
user
text
How to apply for a refund?
1.9ms
sess-003
assistant
text
Return and refund process: Go to My Orders...
2.0ms

6. Update Statistics

Step 11: Execute ANALYZE to update statistics

ANALYZE image_vectors; -- took 2.8ms
ANALYZE text_vectors; -- took 2.1ms
ANALYZE chat_sessions; -- took 1.8ms
ANALYZE chat_messages; -- took 1.9ms
Statistics on data volume of various tables:
SELECT
'image_vectors' AS table_name, count(*) AS row_count FROM image_vectors
UNION ALL SELECT
'text_vectors', count(*) FROM text_vectors
UNION ALL SELECT
'chat_sessions', count(*) FROM chat_sessions
UNION ALL SELECT
'chat_messages', count(*) FROM chat_messages
ORDER BY table_name;
output results (4 rows, took 2.5ms):
table_name
row_count
chat_messages
8
chat_sessions
3
image_vectors
8
text_vectors
12
Note:
4 core tables, 31 sample data records.

7. Create HNSW Vector Index

Step 12: Image vector HNSW index (768-dimensional)

Input SQL:
CREATE INDEX idx_image_vectors_embedding_cosine
ON image_vectors
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 128);
Output: executed successfully, took 2.7ms.

Step 13: text vector HNSW index (1024-dimensional)

Input SQL:
CREATE INDEX idx_text_vectors_embedding_cosine
ON text_vectors
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 128);
Output: executed successfully, took 2.6ms.

Step 14: Dialogue message vector HNSW index (1024-dimensional)

Input SQL:
CREATE INDEX idx_chat_messages_embedding_cosine
ON chat_messages
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
Output: executed successfully, took 2.5ms.
View HNSW index size:
SELECT
indexname AS index_name,
pg_size_pretty(pg_relation_size(indexname::regclass)) AS index_size,
indexdef
FROM pg_indexes
WHERE schemaname = 'public'
AND indexname LIKE '%embedding_cosine'
ORDER BY indexname;
output results (3 rows, took 2.9ms):
index_name
index_size
Description
idx_chat_messages_embedding_cosine
72KB
1024-dimensional message table
idx_image_vectors_embedding_cosine
40KB
768-dimensional picture table
idx_text_vectors_embedding_cosine
104KB
1024-dimensional text table
Note:
HNSW index parameters:
m = 16: The maximum number of connections per node. The higher the value, the more precise the search but the slower the build.
ef_construction = 128: The size of the candidate list during index construction. The higher the value, the higher the index quality.
vector_cosine_ops: using the cosine distance operator, suitable for semantic similarity search.

8. Search Parameter Configuration

Step 15: Set HNSW search parameters

Input SQL:
-- Set the search candidate list size (the larger the value, the more precise the search, recommended 100 ~ 200)
SET hnsw.ef_search = 100;

-- Enable iterative scan mode (to address the issue of insufficient results during WHERE filtering)
SET hnsw.iterative_scan = relaxed_order;
Output: Both executed successfully.
Confirm settings:
SHOW hnsw.ef_search;
Output results:
hnsw.ef_search
100
Recommendation: In production environments, parameters can be written to postgresql.conf:
# pgvector HNSW search parameters
hnsw.ef_search = 100
hnsw.iterative_scan = relaxed_order

9. Vector Search Feature Verification

Step 16: Image Search by Image (Top 5 Cosine Similarity Results)

Input SQL:
SELECT
iv.image_id,
iv.image_url,
iv.category,
iv.tags,
1 - (iv.embedding <=> (SELECT embedding FROM image_vectors WHERE image_id = 'img-001'))
AS similarity
FROM image_vectors iv
WHERE iv.status = 1
ORDER BY iv.embedding <=> (SELECT embedding FROM image_vectors WHERE image_id = 'img-001')
LIMIT 5;
Output results (5 rows, took 2.6ms):
image_id
category
similarity
Description
img-001
Product Images
1.0000
Query vector itself
img-008
Product Images
0.7696
Smart watch
img-006
Manual
0.7631
Mobile Phone Manual
img-007
Product Images
0.7535
Tablet computer
img-003
Product Images
0.7471
Bluetooth earphones
Note:
Verification passed; its own similarity is 1.0, while others are sorted in descending order of cosine similarity.

Step 17: Categorically filter image search (category='Product Images')

Input SQL:
SELECT
iv.image_id,
iv.image_url,
iv.category,
1 - (iv.embedding <=> (SELECT embedding FROM image_vectors WHERE image_id = 'img-004'))
AS similarity
FROM image_vectors iv
WHERE iv.status = 1
AND iv.category = 'Product Images'
ORDER BY iv.embedding <=> (SELECT embedding FROM image_vectors WHERE image_id = 'img-004')
LIMIT 5;
output results (5 rows, took 1.9ms):
image_id
category
similarity
img-001
Product Images
0.7459
img-008
Product Images
0.7355
img-002
Product Images
0.7300
img-007
Product Images
0.7265
img-003
Product Images
0.7218
Note:
Verification passed, using the fault image img-004 as the query vector, only searching under the 'Product Images' category, and the results met expectations.

Step 18: FAQ semantic search (cosine similarity Top 5)

Input SQL:
SELECT
tv.doc_id,
tv.title,
tv.category,
tv.priority,
1 - (tv.embedding <=> (SELECT embedding FROM text_vectors WHERE doc_id = 'faq-001'))
AS similarity
FROM text_vectors tv
WHERE tv.status = 1
AND (tv.expire_at IS NULL OR tv.expire_at > NOW())
ORDER BY tv.embedding <=> (SELECT embedding FROM text_vectors WHERE doc_id = 'faq-001')
LIMIT 5;
output results (5 rows, took 2.1ms):
doc_id
title
category
priority
similarity
faq-001
How to reset password
Account management
2
1.0000
faq-003
Order delivery time
Order Logistics
2
0.7757
faq-006
Delivery Area and Shipping Fee
Order Logistics
1
0.7662
faq-004
Return and Refund Process
After-Sales Service
2
0.7635
faq-009
Payment Methods Explanation
Payment-Related
1
0.7580
Note:
Verification passed. The FAQ semantic search is working properly and supports filtering by expiration time.

Step 19: Search with confidence threshold (similarity > 0.5)

Input SQL:
SELECT
tv.doc_id,
tv.title,
tv.category,
1 - (tv.embedding <=> (SELECT embedding FROM text_vectors WHERE doc_id = 'faq-004'))
AS similarity
FROM text_vectors tv
WHERE tv.status = 1
AND 1 - (tv.embedding <=> (SELECT embedding FROM text_vectors WHERE doc_id = 'faq-004')) > 0.5
ORDER BY tv.embedding <=> (SELECT embedding FROM text_vectors WHERE doc_id = 'faq-004')
LIMIT 5;
Output result (5 rows, time taken: 2.3ms):
doc_id
title
category
similarity
faq-004
Return and Refund Process
After-Sales Service
1.0000
faq-001
How to reset password
Account management
0.7635
faq-008
Account Security Settings
Account management
0.7594
faq-006
Delivery Area and Shipping Fee
Order Logistics
0.7557
faq-003
Order delivery time
Order Logistics
0.7528
Note:
Verification passed, all returned results similarity > 0.5, confidence threshold filtering is effective.
Intelligent customer service routing logic: similarity > 0.85 → direct reply, 0.5 ~ 0.85 → recommend candidates, < 0.5 → transfer to human agent.

Step 20: Hybrid search (vector 70% + full-text 30%)

Input SQL:
SELECT
tv.doc_id,
tv.title,
LEFT(tv.content, 50) AS content_preview,
1 - (tv.embedding <=> (SELECT embedding FROM text_vectors WHERE doc_id = 'faq-003'))
AS vector_score,
ts_rank(to_tsvector('simple', tv.content), plainto_tsquery('simple', 'shipment'))
AS text_score,
0.7 * (1 - (tv.embedding <=> (SELECT embedding FROM text_vectors WHERE doc_id = 'faq-003')))
+ 0.3 * ts_rank(to_tsvector('simple', tv.content), plainto_tsquery('simple', 'shipment'))
AS hybrid_score
FROM text_vectors tv
WHERE tv.status = 1
ORDER BY hybrid_score DESC
LIMIT 5;
Output results (5 rows, took 2.5ms):
doc_id
title
vector_score
text_score
hybrid_score
faq-003
Order delivery time
1.0000
0.0
0.7000
faq-001
How to reset password
0.7757
0.0
0.5430
faq-011
Product Warranty Policy
0.7712
0.0
0.5398
faq-009
Payment Methods Explanation
0.7677
0.0
0.5374
faq-007
How to Contact Customer Service
0.7652
0.0
0.5356
Note:
Example data uses random vectors, and the keyword 'shipment' in full-text search only matches in the title of faq-003. In a production environment, real Embedding will make hybrid_score more discriminative.

Step 21: Similar Question Recommendation (Vector Search of Dialogue Messages)

Input SQL:
SELECT
cm.session_id,
cm.role,
LEFT(cm.content, 50) AS content_preview,
1 - (cm.embedding <=> (SELECT embedding FROM chat_messages WHERE content LIKE '%password%' LIMIT 1))
AS similarity
FROM chat_messages cm
WHERE cm.role = 'user'
ORDER BY cm.embedding <=> (SELECT embedding FROM chat_messages WHERE content LIKE '%password%' LIMIT 1)
LIMIT 5;
output results (5 rows, took 2.0ms):
session_id
role
content_preview
similarity
sess-001
user
What should I do if I forgot my password?
1.0000
sess-002
user
This product has a quality issue.
0.7460
sess-001
user
How long does it take for the password reset to take effect?
0.7460
sess-002
user
When will my order be delivered?
0.7454
sess-003
user
How to apply for a refund?
0.7338
Note:
Verification passed. Vector search based on user's historical questions can be used for the "guess what you want to ask" feature.

10. EXPLAIN ANALYZE Index Verification

Step 22: EXPLAIN ANALYZE Verify Query Plan

Input SQL:
EXPLAIN (ANALYZE, BUFFERS)
SELECT
iv.image_id,
1 - (iv.embedding <=> (SELECT embedding FROM image_vectors WHERE image_id = 'img-001'))
AS similarity
FROM image_vectors iv
WHERE iv.status = 1
ORDER BY iv.embedding <=> (SELECT embedding FROM image_vectors WHERE image_id = 'img-001')
LIMIT 10;
Output result (took 2.0ms):
Limit (cost=3.48..3.50 rows=8 width=24) (actual time=0.097...)
Buffers: shared hit=67
InitPlan 1 (returns $0)
-> Seq Scan on image_vectors (cost=0.00..1.10 rows=1 ...)
Filter: ((image_id)::text = 'img-001'::text)
Rows Removed by Filter: 7
Buffers: shared hit=1
InitPlan 2 (returns $1)
-> Seq Scan on image_vectors image_vectors_1 (cost=0.00..1.10 ...)
Filter: ((image_id)::text = 'img-001'::text)
Rows Removed by Filter: 7
Buffers: shared hit=1
-> Sort (cost=1.28..1.30 rows=8 width=24) (actual time=0....)
Sort Key: ((iv.embedding <=> $1))
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=67
-> Seq Scan on image_vectors iv (cost=0.00..1.16 ...)
Filter: (status = 1)
Buffers: shared hit=67
Planning Time: 0.101 ms
Execution Time: 0.115 ms
Analysis: Currently, only 8 rows of data, the PostgreSQL optimizer correctly chose Seq Scan (more efficient for small tables). When the data volume grows to tens of thousands of rows or more, HNSW Index Scan will be automatically enabled. In previous verification with 500,000 rows, it was confirmed that the HNSW index was hit, with a query latency of only 1.753ms.

11. Ops Monitoring SQL

Step 23: Vector Table Storage Statistics

Input 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 row_count
FROM pg_stat_user_tables s
JOIN pg_class c ON s.relid = c.oid
WHERE c.relname IN ('image_vectors', 'text_vectors', 'chat_sessions', 'chat_messages')
ORDER BY pg_total_relation_size(c.oid) DESC;
Output results (4 rows, took 3.9ms):
table_name
total_size
data_size
index_size
row_count
text_vectors
344 kB
8192 bytes
232 kB
12
image_vectors
232 kB
8192 bytes
152 kB
8
chat_messages
200 kB
8192 bytes
104 kB
8
chat_sessions
56 kB
8192 bytes
48 kB
3

Step 24: Index usage

Input SQL:
SELECT
indexrelname AS index_name,
idx_scan AS times_used,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY idx_scan DESC;
Output results (21 indexes, took 2.8ms):
index_name
times_used
index_size
image_vectors_pkey
0
16KB
image_vectors_image_id_key
0
16KB
text_vectors_pkey
0
16KB
text_vectors_doc_id_key
0
16KB
chat_sessions_pkey
0
16KB
chat_sessions_session_id_key
0
16KB
chat_messages_pkey
0
16KB
idx_image_vectors_category
0
16KB
idx_image_vectors_status
0
16KB
idx_text_vectors_category
0
16KB
idx_text_vectors_content_type
0
16KB
idx_text_vectors_priority
0
16KB
idx_chat_messages_session
0
16KB
idx_chat_sessions_user
0
16KB
idx_image_vectors_tags
0
24KB
idx_text_vectors_tags
0
24KB
idx_image_vectors_metadata
0
24KB
idx_text_vectors_metadata
0
24KB
idx_image_vectors_embedding_cosine
0
40KB
idx_text_vectors_embedding_cosine
0
104KB
idx_chat_messages_embedding_cosine
0
72KB
Note:
The indexes have not been used yet since the system was freshly deployed. After the system is run in the production environment, this monitoring can be used to identify unused indexes.

Step 25: Buffer hit rate

Input SQL:
SELECT
ROUND(
sum(heap_blks_hit)::numeric /
NULLIF(sum(heap_blks_hit) + sum(heap_blks_read), 0) * 100, 2
) AS buffer_hit_ratio_pct
FROM pg_statio_user_tables;
Output result (1 row, took 2.7ms):
buffer_hit_ratio_pct
- (No statistics available for new tables yet)
Note:
Newly created tables have no buffer read statistics. It is recommended that in the production environment, this value should be maintained at >95%. If it falls below 90%, shared_buffers should be increased.

12. Index warm-up

Step 26: Warm up the HNSW index to shared buffers

Input SQL:
-- Install the pg_prewarm extension
CREATE EXTENSION IF NOT EXISTS pg_prewarm;

-- Prewarm the image vectors HNSW index
SELECT pg_prewarm('idx_image_vectors_embedding_cosine') AS pages_prewarmed;

-- Prewarm the text vectors HNSW index
SELECT pg_prewarm('idx_text_vectors_embedding_cosine') AS pages_prewarmed;
Output results:
Operation
pages_prewarmed
Duration
Install pg_prewarm
-
1.7ms
Prewarm the image vectors index
5 pages
2.0ms
Prewarm the text vectors index
13 pages
1.7ms
Note:
pg_prewarm loads index pages into shared_buffers, reducing disk I/O for the first query. It is recommended to execute this after database restart or after a large amount of data is imported.

13. Technical Architecture Summary

┌─────────────────────────────────────────────────────┐
│ Intelligent Customer Service Application Layer │
├──────────┬──────────┬──────────┬────────────────────┤
│ Image Search │ FAQ Search │ Hybrid Search │ Similar Question Recommendation │
├──────────┴──────────┴──────────┴────────────────────┤
│ pgvector HNSW index layer │
│ • image_vectors: 768-dimensional (CLIP ViT-L/14)
│ • text_vectors: 1024-dimensional (BGE-large-zh-v1.5)
│ • chat_messages: 1024-dimensional (dialogue vector)
├─────────────────────────────────────────────────────┤
│ PostgreSQL 15.14 + pgvector 0.8.2
│ • Cosine distance: <=> operator │
│ • HNSW parameters: m=16, ef_search=100
│ • Auxiliary index: B-Tree + GIN (partial index)
└─────────────────────────────────────────────────────┘

Help and Support

Was this page helpful?

Help us improve! Rate your documentation experience in 5 mins.

Feedback