向量搜索可视化:sqlite-vec+Chart.js实现
你是否曾在调试向量搜索时,面对一堆冰冷的距离数值感到困惑?想知道向量之间的空间分布关系却无从下手?本文将带你用sqlite-vec构建向量数据库,结合Chart.js实现搜索结果的可视化展示,让抽象的向量距离变得直观可感。读完本文,你将掌握从向量数据存储、查询到结果可视化的完整流程,学会用散点图、热力图等方式分析向量搜索结果。
技术栈概览
| 组件 | 作用 | 版本要求 |
|---|---|---|
| sqlite-vec | SQLite向量搜索扩展 | 0.0.1-alpha.9+ |
| Chart.js | 前端数据可视化库 | 4.4.8+ |
| Node.js | JavaScript运行环境 | 18.0.0+ |
| better-sqlite3 | Node.js SQLite驱动 | 9.6.0+ |
技术架构流程图
环境准备与安装
快速安装依赖
# 创建项目目录并初始化
mkdir vec-visualization && cd vec-visualization
npm init -y
# 安装核心依赖
npm install sqlite-vec better-sqlite3 chart.js express
项目结构设计
vec-visualization/
├── public/ # 静态资源目录
│ ├── index.html # 可视化页面
│ └── app.js # 前端交互逻辑
├── server.js # Node.js后端服务
├── package.json # 项目配置
└── vectors.db # 向量数据库文件
后端实现:构建向量搜索引擎
初始化sqlite-vec扩展
// server.js
const Database = require('better-sqlite3');
const sqliteVec = require('sqlite-vec');
const express = require('express');
const app = express();
app.use(express.json());
app.use(express.static('public'));
// 连接数据库并加载向量扩展
const db = new Database('vectors.db');
sqliteVec.load(db);
// 验证安装版本
const { sqlite_version, vec_version } = db.prepare(`
SELECT sqlite_version() as sqlite_version, vec_version() as vec_version
`).get();
console.log(`SQLite版本: ${sqlite_version}, sqlite-vec版本: ${vec_version}`);
// 创建向量表 (4维向量示例)
db.exec(`
CREATE VIRTUAL TABLE IF NOT EXISTS products USING vec0(
embedding float[4],
name TEXT
)
`);
生成样本向量数据
// 生成随机向量函数
function generateRandomVector(dimensions = 4, min = 0, max = 1) {
return Array.from({ length: dimensions },
() => min + Math.random() * (max - min));
}
// 插入示例数据 (电子产品向量库)
const categories = ['手机', '笔记本', '平板', '耳机', '手表'];
const insertStmt = db.prepare(`
INSERT INTO products(name, embedding) VALUES (?, ?)
`);
// 事务批量插入100条样本数据
db.transaction(() => {
for (let i = 0; i < 100; i++) {
const category = categories[Math.floor(Math.random() * categories.length)];
const vector = generateRandomVector(4);
// 为同类产品添加特征偏移,使可视化效果更明显
if (category === '手机') vector[0] += 0.5;
if (category === '笔记本') vector[1] += 0.5;
if (category === '平板') vector[2] += 0.5;
if (category === '耳机') vector[3] += 0.5;
insertStmt.run(
`${category}-${i}`,
Buffer.from(new Float32Array(vector).buffer)
);
}
})();
实现向量搜索API
// 向量搜索API端点
app.post('/search', (req, res) => {
const { queryVector, limit = 10 } = req.body;
if (!queryVector || !Array.isArray(queryVector)) {
return res.status(400).json({ error: '请提供有效的查询向量' });
}
try {
// 执行KNN搜索
const results = db.prepare(`
SELECT
rowid,
name,
distance,
embedding
FROM products
WHERE embedding MATCH ?
ORDER BY distance
LIMIT ?
`).all(
Buffer.from(new Float32Array(queryVector).buffer),
limit
);
// 解析原始向量数据供前端可视化
const formattedResults = results.map(item => ({
...item,
embedding: new Float32Array(item.embedding).toJSON().data
}));
res.json(formattedResults);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 启动服务
const PORT = 3000;
app.listen(PORT, () => {
console.log(`服务已启动: http://localhost:${PORT}`);
});
前端可视化实现
页面结构设计
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
<title>sqlite-vec向量搜索可视化</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
<style>
body { font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
.control-panel { margin-bottom: 20px; padding: 20px; border: 1px solid #ccc; border-radius: 8px; }
.vector-input { width: 300px; }
.charts-container { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px; }
.chart-wrapper { height: 400px; border: 1px solid #eee; border-radius: 8px; padding: 10px; }
</style>
</head>
<body>
<h1>sqlite-vec向量搜索可视化工具</h1>
<div class="control-panel">
<h3>查询控制</h3>
<div>
<label>查询向量 (4维):</label>
<input type="text" id="queryVector" class="vector-input"
value="[0.3, 0.6, 0.2, 0.8]">
<button onclick="runSearch()">执行搜索</button>
<button onclick="randomQuery()">随机向量</button>
</div>
<div style="margin-top: 10px;">
<label>结果数量:</label>
<input type="number" id="resultLimit" value="10" min="1" max="50">
</div>
</div>
<div class="charts-container">
<div class="chart-wrapper">
<h3>向量分布散点图 (PCA降维)</h3>
<canvas id="scatterPlot"></canvas>
</div>
<div class="chart-wrapper">
<h3>距离分布条形图</h3>
<canvas id="distanceBarChart"></canvas>
</div>
<div class="chart-wrapper">
<h3>向量分量热力图</h3>
<canvas id="heatmapChart"></canvas>
</div>
<div class="chart-wrapper">
<h3>类别分布饼图</h3>
<canvas id="categoryPieChart"></canvas>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
核心可视化逻辑
// public/app.js
let scatterPlot, distanceBarChart, heatmapChart, categoryPieChart;
// 初始化图表
function initCharts() {
// 散点图 - 展示向量空间分布
scatterPlot = new Chart(document.getElementById('scatterPlot'), {
type: 'scatter',
data: { datasets: [{
label: '搜索结果向量',
data: [],
backgroundColor: 'rgba(75, 192, 192, 0.6)',
borderColor: 'rgba(75, 192, 192, 1)',
}, {
label: '查询向量',
data: [],
backgroundColor: 'rgba(255, 99, 132, 1)',
pointRadius: 10,
pointHoverRadius: 12
}]}
});
// 距离条形图
distanceBarChart = new Chart(document.getElementById('distanceBarChart'), {
type: 'bar',
data: { labels: [], datasets: [{
label: '向量距离',
data: [],
backgroundColor: 'rgba(54, 162, 235, 0.6)'
}]}
});
// 热力图 - 展示向量分量
heatmapChart = new Chart(document.getElementById('heatmapChart'), {
type: 'matrix',
data: { datasets: [{
label: '向量分量值',
data: [],
backgroundColor: (context) => {
const value = context.dataset.data[context.dataIndex].v;
return `rgba(255, ${255 - value * 255}, 0, 0.7)`;
}
}]}
});
// 类别饼图
categoryPieChart = new Chart(document.getElementById('categoryPieChart'), {
type: 'pie',
data: { labels: [], datasets: [{
data: [],
backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF']
}]}
});
}
// PCA降维 - 将高维向量降为2D用于散点图展示
function pcaReduce(vectors, dimensions = 2) {
// 简化版PCA实现,实际应用可使用ml-pca库
const matrix = vectors.map(v => [...v]);
// 计算均值
const means = matrix[0].map((_, i) =>
matrix.reduce((sum, row) => sum + row[i], 0) / matrix.length);
// 去中心化
const centered = matrix.map(row =>
row.map((val, i) => val - means[i]));
// 计算协方差矩阵
const cov = Array.from({ length: centered[0].length }, () =>
Array(centered[0].length).fill(0));
for (let i = 0; i < centered[0].length; i++) {
for (let j = 0; j < centered[0].length; j++) {
cov[i][j] = centered.reduce((sum, row) => sum + row[i] * row[j], 0) / (centered.length - 1);
}
}
// 此处简化处理,实际应计算特征值和特征向量
// 这里直接取前两个维度作为降维结果
return matrix.map(row => ({ x: row[0], y: row[1] }));
}
// 执行搜索并更新可视化
async function runSearch() {
const queryVector = JSON.parse(document.getElementById('queryVector').value);
const limit = parseInt(document.getElementById('resultLimit').value);
try {
const response = await fetch('/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ queryVector, limit })
});
const results = await response.json();
updateCharts(results, queryVector);
} catch (error) {
alert('搜索失败: ' + error.message);
}
}
// 更新所有图表数据
function updateCharts(results, queryVector) {
if (results.length === 0) return;
// 提取数据
const vectors = results.map(r => r.embedding);
const distances = results.map(r => r.distance);
const labels = results.map(r => r.name);
const categories = results.map(r => r.name.split('-')[0]);
// 更新散点图
const reducedVectors = pcaReduce(vectors);
const reducedQuery = pcaReduce([queryVector])[0];
scatterPlot.data.datasets[0].data = reducedVectors;
scatterPlot.data.datasets[1].data = [reducedQuery];
scatterPlot.update();
// 更新距离条形图
distanceBarChart.data.labels = labels;
distanceBarChart.data.datasets[0].data = distances;
distanceBarChart.update();
// 更新热力图
heatmapChart.data.datasets[0].data = vectors.flatMap((vec, row) =>
vec.map((val, col) => ({ x: col, y: row, v: val }))
);
heatmapChart.options.scales = {
x: { title: { display: true, text: '向量分量' }},
y: { title: { display: true, text: '结果索引' }}
};
heatmapChart.update();
// 更新类别饼图
const categoryCounts = {};
categories.forEach(cat => {
categoryCounts[cat] = (categoryCounts[cat] || 0) + 1;
});
categoryPieChart.data.labels = Object.keys(categoryCounts);
categoryPieChart.data.datasets[0].data = Object.values(categoryCounts);
categoryPieChart.update();
}
// 生成随机查询向量
function randomQuery() {
const randomVec = Array.from({ length: 4 }, () => Math.random().toFixed(2));
document.getElementById('queryVector').value = `[${randomVec}]`;
}
// 页面加载时初始化
window.onload = () => {
initCharts();
randomQuery(); // 生成初始随机向量
runSearch(); // 执行初始搜索
};
高级功能扩展
动态向量生成器
添加随机向量生成器和预设向量库,方便测试不同搜索场景:
// public/app.js 扩展
const presetQueries = {
"手机类向量": [0.6, 0.2, 0.1, 0.1],
"笔记本类向量": [0.2, 0.7, 0.1, 0.1],
"平板类向量": [0.2, 0.2, 0.6, 0.1],
"耳机类向量": [0.1, 0.1, 0.2, 0.7]
};
// 添加预设查询选择器到HTML
document.querySelector('.control-panel').innerHTML += `
<div style="margin-top: 10px;">
<label>预设查询: </label>
<select id="presetQueries" onchange="usePresetQuery()">
<option value="">自定义</option>
${Object.keys(presetQueries).map(key =>
`<option value="${key}">${key}</option>`).join('')}
</select>
</div>
`;
function usePresetQuery() {
const selected = document.getElementById('presetQueries').value;
if (selected) {
document.getElementById('queryVector').value =
JSON.stringify(presetQueries[selected]);
runSearch();
}
}
性能优化建议
- 数据采样:当结果数量超过50条时,自动采样展示
- Web Worker:将PCA降维等计算密集型任务移至Web Worker
- 缓存机制:缓存相同查询的可视化结果
- 渐进式渲染:先渲染低精度图表,再逐步提高精度
// 采样函数示例
function sampleResults(results, maxSamples = 50) {
if (results.length <= maxSamples) return results;
const step = Math.ceil(results.length / maxSamples);
return results.filter((_, i) => i % step === 0);
}
总结与展望
本文展示了如何将sqlite-vec的强大向量搜索能力与Chart.js的可视化效果相结合,构建直观的向量搜索调试工具。通过散点图、热力图等多种可视化方式,我们可以更清晰地理解向量之间的空间关系和搜索结果的分布特征。
未来可以进一步扩展:
- 添加向量相似度矩阵可视化
- 实现3D向量空间展示
- 支持向量动态修改与实时重新搜索
- 集成t-SNE等高维数据降维算法
希望这个工具能帮助你在向量搜索开发中更高效地调试和优化,让抽象的向量数据变得触手可及。如果你有任何改进建议或使用心得,欢迎在评论区分享!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



