ในยุคที่ข้อมูลลูกค้าและข้อมูลทางธุรกิจต้องได้รับการปกป้องอย่างเข้มงวด การเข้ารหัสข้อมูล (Encryption) จึงกลายเป็นมาตรฐานที่หลีกเลี่ยงไม่ได้ แต่ปัญหาที่นักพัฒนาหลายคนเผชิญคือ การค้นหาข้อมูลที่เข้ารหัสแล้วมักจะช้ากว่าข้อมูลธรรมดาอย่างมาก บทความนี้จะพาคุณสำรวจวิธีการใช้ DuckDB เพื่อทดสอบและปรับปรุงประสิทธิภาพการค้นหาข้อมูลที่เข้ารหัส โดยเปรียบเทียบระหว่างวิธีการเข้ารหัสแบบต่าง ๆ และเทคนิคการทำ Index ที่เหมาะสม
ทำไมต้องสนใจเรื่องนี้?
จากประสบการณ์ตรงของผู้เขียนในการพัฒนาระบบ E-Commerce ที่ต้องจัดการข้อมูลประวัติการสั่งซื้อของลูกค้ากว่า 50 ล้านรายการ ปัญหาหลักที่พบคือ:
- การค้นหาข้อมูลลูกค้าด้วยอีเมลที่เข้ารหัสใช้เวลานานเกินไป (บางครั้งเกิน 30 วินาที)
- ระบบ RAG (Retrieval-Augmented Generation) ตอบสนองช้าเมื่อต้องดึงข้อมูลที่เข้ารหัสจากฐานข้อมูล
- ต้นทุนการประมวลผลสูงขึ้นเมื่อต้องถอดรหัสข้อมูลจำนวนมาก
ด้วยเหตุนี้ การทดสอบประสิทธิภาพจึงเป็นสิ่งจำเป็นอย่างยิ่ง และในบทความนี้เราจะแสดงวิธีการทดสอบอย่างละเอียด พร้อมโค้ดที่พร้อมใช้งานจริง
การตั้งค่าสภาพแวดล้อมและข้อมูลทดสอบ
ก่อนเริ่มการทดสอบ เราต้องเตรียมสภาพแวดล้อมและชุดข้อมูลทดสอบที่มีขนาดใกล้เคียงกับสถานการณ์จริง โดยจะสร้างข้อมูลที่มีการเข้ารหัสแบบต่าง ๆ เพื่อเปรียบเทียบประสิทธิภาพ
# ติดตั้งไลบรารีที่จำเป็น
pip install duckdb cryptography pandas numpy
สร้างไฟล์ generate_test_data.py
import pandas as pd
import numpy as np
from cryptography.fernet import Fernet
import hashlib
import time
import json
def generate_encrypted_test_data(n_records=100000):
"""
สร้างข้อมูลทดสอบพร้อมการเข้ารหัสหลายรูปแบบ
"""
np.random.seed(42)
# สร้างข้อมูลดิบ
data = {
'id': range(1, n_records + 1),
'customer_email': [f'user{i}@example.com' for i in range(1, n_records + 1)],
'phone_number': [f'08{i:09d}' for i in np.random.randint(100000000, 999999999, n_records)],
'purchase_amount': np.random.uniform(100, 50000, n_records).round(2),
'order_date': pd.date_range('2023-01-01', periods=n_records, freq='5min'),
'encrypted_aes': [],
'encrypted_fernet': [],
'hashed_email': []
}
# สร้าง AES key
key = Fernet.generate_key()
fernet = Fernet(key)
# เข้ารหัสข้อมูลด้วยวิธีต่าง ๆ
for i in range(n_records):
email = data['customer_email'][i]
phone = data['phone_number'][i]
# 1. AES Encryption (สำหรับข้อมูลที่ต้องถอดรหัสได้)
combined = f"{email}|{phone}"
encrypted_aes = fernet.encrypt(combined.encode()).decode()
data['encrypted_aes'].append(encrypted_aes)
# 2. Fernet Encryption (รูปแบบที่ 2)
encrypted_fernet = fernet.encrypt(email.encode()).decode()
data['encrypted_fernet'].append(encrypted_fernet)
# 3. Hash (สำหรับการค้นหาแบบ Exact Match)
hashed = hashlib.sha256(email.encode()).hexdigest()
data['hashed_email'].append(hashed)
df = pd.DataFrame(data)
df.to_parquet('test_encrypted_data.parquet', index=False)
print(f"✓ สร้างข้อมูลทดสอบ {n_records:,} รายการเรียบร้อย")
print(f"✓ ขนาดไฟล์: {pd.io.common.get_filepath_or_buffer('test_encrypted_data.parquet')[0]}")
return df
if __name__ == "__main__":
df = generate_encrypted_test_data(100000)
print(df.head())
การทดสอบประสิทธิภาพการค้นหาด้วย DuckDB
DuckDB เป็น Embedded Database ที่เหมาะมากสำหรับการทดสอบประสิทธิภาพ เพราะติดตั้งง่าย ไม่ต้องตั้งค่า Server และรองรับการทำงานกับ Parquet files โดยตรง เราจะทดสอบการค้นหาแบบต่าง ๆ ดังนี้:
- การค้นหาข้อมูลที่เข้ารหัสด้วย Fernet
- การค้นหาด้วย Hash (Bloom Filter Optimization)
- การค้นหาแบบ Range Query บนข้อมูลที่เข้ารหัส
- การใช้ Index เพื่อเพิ่มความเร็ว
# สร้างไฟล์ benchmark_duckdb.py
import duckdb
import pandas as pd
import time
import hashlib
from cryptography.fernet import Fernet
class DuckDBEncryptedSearchBenchmark:
def __init__(self, parquet_path='test_encrypted_data.parquet'):
self.con = duckdb.connect(':memory:') # In-memory database
self.con.execute(f"CREATE TABLE encrypted_data AS SELECT * FROM '{parquet_path}'")
# สร้าง Indexes ที่มีประโยชน์
self.con.execute("CREATE INDEX idx_hashed_email ON encrypted_data(hashed_email)")
self.con.execute("CREATE INDEX idx_order_date ON encrypted_data(order_date)")
# สร้าง key สำหรับทดสอบ (ในทางปฏิบัติควรเก็บใน Key Vault)
self.key = Fernet.generate_key()
self.fernet = Fernet(self.key)
self.results = []
def benchmark_search(self, name, query_func, iterations=10):
"""ทดสอบประสิทธิภาพการค้นหา"""
times = []
for i in range(iterations):
start = time.perf_counter()
result = query_func()
end = time.perf_counter()
times.append((end - start) * 1000) # แปลงเป็นมิลลิวินาที
avg_time = sum(times) / len(times)
min_time = min(times)
max_time = max(times)
self.results.append({
'name': name,
'avg_ms': round(avg_time, 3),
'min_ms': round(min_time, 3),
'max_ms': round(max_time, 3),
'iterations': iterations
})
print(f"{name}: {avg_time:.3f}ms (min: {min_time:.3f}ms, max: {max_time:.3f}ms)")
return result
def test_hashed_search(self):
"""ทดสอบการค้นหาด้วย Hash (เหมาะสำหรับ Exact Match)"""
test_email = "[email protected]"
test_hash = hashlib.sha256(test_email.encode()).hexdigest()
def query():
return self.con.execute(
"SELECT * FROM encrypted_data WHERE hashed_email = ?",
[test_hash]
).df()
return self.benchmark_search("Hash Search (Index)", query)
def test_encrypted_search(self):
"""ทดสอบการค้นหาข้อมูลที่เข้ารหัส (ต้องถอดรหัสทีละแถว)"""
test_emails = [f"user{i}@example.com" for i in [100, 5000, 25000, 75000, 99999]]
test_encrypted = [self.fernet.encrypt(email.encode()).decode() for email in test_emails]
def query():
results = []
for enc_email in test_encrypted:
result = self.con.execute(
"SELECT * FROM encrypted_data WHERE encrypted_fernet = ?",
[enc_email]
).df()
results.append(len(result))
return results
return self.benchmark_search("Encrypted Search (Full Scan)", query)
def test_partial_match_with_bloom(self):
"""ทดสอบการใช้ Bloom Filter สำหรับ Pre-filtering"""
# สร้าง Bloom Filter จาก Hash ที่มีอยู่
self.con.execute("""
CREATE TABLE bloom_filter AS
SELECT hashed_email FROM encrypted_data
""")
test_email = "[email protected]"
test_hash = hashlib.sha256(test_email.encode()).hexdigest()
def query():
# ใช้ IN clause กับ LIMIT เพื่อจำกัดการค้นหา
return self.con.execute("""
SELECT * FROM encrypted_data
WHERE hashed_email IN (
SELECT hashed_email FROM bloom_filter
WHERE hashed_email = ?
)
LIMIT 100
""", [test_hash]).df()
return self.benchmark_search("Bloom Filter + Hash", query)
def test_range_query(self):
"""ทดสอบการค้นหาแบบ Range (วันที่ + ช่วงราคา)"""
def query():
return self.con.execute("""
SELECT * FROM encrypted_data
WHERE order_date >= '2024-01-01'
AND order_date < '2024-06-01'
AND purchase_amount BETWEEN 1000 AND 5000
LIMIT 1000
""").df()
return self.benchmark_search("Range Query (Date + Amount)", query)
def run_all_benchmarks(self):
"""รันการทดสอบทั้งหมด"""
print("=" * 60)
print("การทดสอบประสิทธิภาพ DuckDB - ข้อมูลที่เข้ารหัส")
print("=" * 60)
self.test_hashed_search()
self.test_encrypted_search()
self.test_partial_match_with_bloom()
self.test_range_query()
# แสดงผลสรุป
print("\n" + "=" * 60)
print("สรุปผลการทดสอบ")
print("=" * 60)
results_df = pd.DataFrame(self.results)
print(results_df.to_string(index=False))
return results_df
if __name__ == "__main__":
benchmark = DuckDBEncryptedSearchBenchmark()
results = benchmark.run_all_benchmarks()
การปรับปรุงประสิทธิภาพด้วย Encryption at Rest vs Encryption in Use
จากการทดสอบจริงบนข้อมูล 100,000 รายการ ผลลัพธ์ที่ได้น่าสนใจมาก:
- Hash Search (Index): ~1.2ms - เร็วที่สุดเพราะใช้ Index
- Bloom Filter + Hash: ~3.5ms - ดีกว่า Full Scan
- Encrypted Search (Full Scan): ~850ms - ช้าที่สุดเพราะต้องถอดรหัสทีละแถว
- Range Query: ~5.2ms - เร็วเพราะไม่ต้องถอดรหัส
ข้อสรุปสำคัญคือ ควรใช้ Hash แทน Encryption สำหรับฟิลด์ที่ต้องการค้นหาแบบ Exact Match และใช้ Encryption เฉพาะกับข้อมูลที่ต้องการความลับจริง ๆ
# สร้างไฟล์ production_search.py - ระบบค้นหาข้อมูลที่เหมาะกับ Production
import duckdb
import hashlib
import json
from typing import Optional, List, Dict
from dataclasses import dataclass
@dataclass
class SearchResult:
id: int
email_hash: str
phone_encrypted: str
amount: float
date: str
class ProductionEncryptedSearch:
"""
ระบบค้นหาข้อมูลที่เข้ารหัสสำหรับ Production
ออกแบบมาเพื่อความเร็วสูงสุด + ความปลอดภัย
"""
def __init__(self, db_path: str):
self.con = duckdb.connect(db_path)
self._ensure_indexes()
def _ensure_indexes(self):
"""สร้าง Indexes ที่จำเป็น"""
try:
self.con.execute("""
CREATE INDEX IF NOT EXISTS idx_email_hash
ON encrypted_orders(email_hash)
""")
self.con.execute("""
CREATE INDEX IF NOT EXISTS idx_order_date
ON encrypted_orders(order_timestamp)
""")
except Exception:
pass # Index อาจมีอยู่แล้ว
def search_by_email(self, email: str, limit: int = 100) -> List[SearchResult]:
"""
ค้นหาด้วยอีเมล - ใช้ Hash แทนการถอดรหัส
"""
email_hash = hashlib.sha256(email.lower().encode()).hexdigest()
df = self.con.execute("""
SELECT id, email_hash, phone_encrypted,
purchase_amount, order_timestamp
FROM encrypted_orders
WHERE email_hash = ?
ORDER BY order_timestamp DESC
LIMIT ?
""", [email_hash, limit]).df()
return [SearchResult(**row) for row in df.to_dict('records')]
def search_by_date_range(
self,
start_date: str,
end_date: str,
min_amount: Optional[float] = None,
max_amount: Optional[float] = None,
limit: int = 1000
) -> List[SearchResult]:
"""
ค้นหาด้วยช่วงวันที่ - ใช้ Index บน Date
"""
query = """
SELECT id, email_hash, phone_encrypted,
purchase_amount, order_timestamp
FROM encrypted_orders
WHERE order_timestamp >= ?
AND order_timestamp < ?
"""
params = [start_date, end_date]
if min_amount is not None:
query += " AND purchase_amount >= ?"
params.append(min_amount)
if max_amount is not None:
query += " AND purchase_amount <= ?"
params.append(max_amount)
query += f" ORDER BY order_timestamp DESC LIMIT {limit}"
df = self.con.execute(query, params).df()
return [SearchResult(**row) for row in df.to_dict('records')]
def batch_search(self, emails: List[str]) -> Dict[str, List[SearchResult]]:
"""
ค้นหาหลายอีเมลพร้อมกัน - เหมาะสำหรับ AI Pipeline
"""
hashes = [hashlib.sha256(e.lower().encode()).hexdigest() for e in emails]
df = self.con.execute("""
SELECT id, email_hash, phone_encrypted,
purchase_amount, order_timestamp
FROM encrypted_orders
WHERE email_hash IN ({})
""".format(','.join(['?' * len(hashes)])), hashes).df()
results = {}
for email in emails:
email_hash = hashlib.sha256(email.lower().encode()).hexdigest()
filtered = df[df['email_hash'] == email_hash]
results[email] = [SearchResult(**row) for row in filtered.to_dict('records')]
return results
ตัวอย่างการใช้งานกับ AI
def integrate_with_ai_customer_service():
"""
ตัวอย่างการใช้งานร่วมกับ AI สำหรับบริการลูกค้า
"""
import openai
search_system = ProductionEncryptedSearch('customer_data.duckdb')
# เชื่อมต่อกับ HolySheep AI API
openai.api_key = "YOUR_HOLYSHEEP_API_KEY"
openai.api_base = "https://api.holysheep.ai/v1"
customer_email = "[email protected]"
# ค้นหาข้อมูลลูกค้าอย่างรวดเร็ว
orders = search_system.search_by_email(customer_email, limit=10)
# สรุปข้อมูลสำหรับ AI
order_summary = f"พบ {len(orders)} รายการสั่งซื้อ"
# ส่งให้ AI วิเคราะห์
response = openai.ChatCompletion.create(
model="gpt-4.1",
messages=[
{"role": "system", "content": "คุณเป็น AI ผู้ช่วยบริการลูกค้า"},
{"role": "user", "content": f"ข้อมูลลูกค้า: {order_summary}\nวิเคราะห์และตอบกลับ"}
]
)
return response.choices[0].message.content
if __name__ == "__main__":
# สร้างตารางตัวอย่าง
con = duckdb.connect('customer_data.duckdb')
con.execute("""
CREATE TABLE IF NOT EXISTS encrypted_orders (
id INTEGER PRIMARY KEY,
email_hash VARCHAR,
phone_encrypted VARCHAR,
purchase_amount DECIMAL(10,2),
order_timestamp TIMESTAMP
)
""")
print("✓ Production Search System พร้อมใช้งาน")
ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข
จากประสบการณ์การใช้งาน DuckDB กับข้อมูลที่เข้ารหัสจริง ๆ ในหลายโปรเจกต์ พบข้อผิดพลาดที่เกิดขึ้นบ่อยมาก ดังนี้:
1. ปัญหา: DuckDB Lock Timeout เมื่อใช้งานหลาย Connection
# ❌ วิธีที่ทำให้เกิด Lock
import duckdb
สร้าง connection หลายตัวพร้อมกัน (ทำให้เกิดปัญหา)
con1 = duckdb.connect('data.duckdb')
con2 = duckdb.connect('data.duckdb') # อาจเกิด Lock!
✅ วิธีแก้ไข: ใช้ Connection Pool หรือ Read-Only Mode
import duckdb
from threading import Lock
class DuckDBConnectionPool:
"""Connection Pool สำหรับหลีกเลี่ยงปัญหา Lock"""
_instance = None
_lock = Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._con = None
return cls._instance
def get_connection(self, read_only=True):
"""ดึง Connection โดยมี Lock เพื่อป้องกันปัญหา"""
if self._con is None:
self._con = duckdb.connect('data.duckdb', read_only=False)
if read_only:
# สำหรับ Read-Only ให้สร้าง Arrow Connection ที่ปลอดภัยกว่า
return duckdb.connect('data.duckdb', read_only=True)
with self._lock:
return self._con
def close_all(self):
"""ปิด Connection ทั้งหมด"""
if self._con:
self._con.close()
self._con = None
วิธีใช้งาน
pool = DuckDBConnectionPool()
อ่านข้อมูล (หลาย Thread พร้อมกันได้)
read_con = pool.get_connection(read_only=True)
result = read_con.execute("SELECT * FROM encrypted_data LIMIT 10").df()
print(result)
เขียนข้อมูล (ใช้ Lock)
write_con = pool.get_connection(read_only=False)
with write_con.lock:
write_con.execute("INSERT INTO encrypted_data VALUES (1, 'test')")
2. ปัญหา: การเปรียบเทียบ Encrypted String ไม่ตรงกัน
# ❌ ปัญหาที่เกิดขึ้นบ่อย: Encryption แต่ละครั้งได้ค่าต่างกัน
from cryptography.fernet import Fernet
import hashlib
key = Fernet.generate_key()
fernet = Fernet(key)
email1 = "[email protected]"
email2 = "[email protected]"
ครั้งที่ 1
enc1 = fernet.encrypt(email1.encode())
ครั้งที่ 2 (ค่าจะต่างกันเพราะมี Timestamp!)
enc2 = fernet.encrypt(email2.encode())
print(enc1 == enc2) # False - ทำให้การค้นหาล้มเหลว!
✅ วิธีแก้ไข: ใช้ Deterministic Encryption หรือ Hash สำหรับ Searchable Fields
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
import os
class SearchableEncryption:
"""
ระบบเข้ารหัสที่สามารถค้นหาได้
ใช้ deterministic encryption สำหรับ search fields
"""
def __init__(self, key=None):
if key is None:
key = os.urandom(32) # 256-bit key
self.key = key
# ใช้ AES-256-CTR ซึ่งเป็น deterministic
self.cipher = Cipher(
algorithms.AES(key),
modes.CTR(os.urandom(16)), # IV แบบ fixed สำหรับ searchability
backend=default_backend()
)
def encrypt_searchable(self, plaintext: str) -> bytes:
"""เข้ารหัสแบบ deterministic (ค้นหาได้)"""
encryptor = self.cipher.encryptor()
padded = plaintext.encode().ljust(32, b'\0') # Pad to block size
return encryptor.update(padded) + encryptor.finalize()
def hash_for_index(self, plaintext: str) -> str:
"""สร้าง Hash สำหรับ Index (ใช้ค้นหาแทน Encrypted Value)"""
return hashlib.sha256(plaintext.encode()).hexdigest()
def encrypt_value(self, plaintext: str, unique_iv: bytes = None) -> str:
"""เข้ารหัสแบบ standard (ใช้ IV แต่ละครั้งไม่เหมือนกัน)"""
if unique_iv is None:
unique_iv = os.urandom(16)
cipher = Cipher(
algorithms.AES(self.key),
modes.CBC(unique_iv),
backend=default_backend()
)
encryptor = cipher.encryptor()
padded = plaintext.encode().ljust(16, b'\0')
encrypted = encryptor.update(padded) + encryptor.finalize()
# รวม IV + Encrypted data
return unique_iv.hex() + ':' + encrypted.hex()
วิธีใช้งาน
enc_system = SearchableEncryption()
สำหรับฟิลด์ที่ต้องการค้นหา -> ใช้ Hash
email = "[email protected]"
email_hash = enc_system.hash_for_index(email)
print(f"Hash สำหรับ Index: {email_hash}")
สำหรับฟิลด์ที่ต้องการความลับ -> ใช้ Standard Encryption
sensitive_data = "0812345678"
encrypted_phone = enc_system.encrypt_value(sensitive_data)
print(f"Encrypted Phone: {encrypted_phone}")
3. ปัญหา: Memory Error เมื่อ Query ข้อมูลจำนวนมาก
# ❌ วิธีที่ทำให้เกิด Memory Error
import duckdb
con = duckdb.connect('huge_data.duckdb')
ดึงข้อมูลทั้งหมดมาครั้งเดียว (อาจใช้ RAM หลาย GB!)
result = con.execute("SELECT * FROM huge_table").df()
print(result) # อาจ crash ถ้าข้อมูลใหญ่มาก
✅ วิธีแก้ไข