向量搜索可视化:sqlite-vec+Chart.js实现

向量搜索可视化:sqlite-vec+Chart.js实现

【免费下载链接】sqlite-vec Work-in-progress vector search SQLite extension that runs anywhere. 【免费下载链接】sqlite-vec 项目地址: https://gitcode.com/GitHub_Trending/sq/sqlite-vec

你是否曾在调试向量搜索时,面对一堆冰冷的距离数值感到困惑?想知道向量之间的空间分布关系却无从下手?本文将带你用sqlite-vec构建向量数据库,结合Chart.js实现搜索结果的可视化展示,让抽象的向量距离变得直观可感。读完本文,你将掌握从向量数据存储、查询到结果可视化的完整流程,学会用散点图、热力图等方式分析向量搜索结果。

技术栈概览

组件作用版本要求
sqlite-vecSQLite向量搜索扩展0.0.1-alpha.9+
Chart.js前端数据可视化库4.4.8+
Node.jsJavaScript运行环境18.0.0+
better-sqlite3Node.js SQLite驱动9.6.0+
技术架构流程图 ![mermaid](https://web-api.gitcode.com/mermaid/svg/eNpLy8kvT85ILCpRCHHhUgACx-inffOfLu9-NnXDs951sQq6unY1z6fMf9Yx4emEiS_b-2sUnKJdc5NSU1Iy89IhQrFgjU5gpU939T_tmP60Z9rTtTOeNq2oUXCOLi7MySxJ1S1LTX6xcAVErUv0s_lLX6xfBNUP1unt5_dswpznWxbVKLgi6Xm6Z-qzyX0Qba5ghS_WLXy-bvqL7XOfL9tdo-AWDdH1fPfkZ_PmQNS5gdVBfPBi75pnvUAz3aOfdvY-X73-af_6F8vbgA6EehCswR2iYcemZ_MnP529D-jOGgWPaGdQuOhlFcO1QBQXl1TmpCp4KKRl5uRYKadZpukUlxTlZ6daKRsbG0PZuuWZKSUZVkYFFQBc96Wn)

环境准备与安装

快速安装依赖

# 创建项目目录并初始化
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();
  }
}

性能优化建议

  1. 数据采样:当结果数量超过50条时,自动采样展示
  2. Web Worker:将PCA降维等计算密集型任务移至Web Worker
  3. 缓存机制:缓存相同查询的可视化结果
  4. 渐进式渲染:先渲染低精度图表,再逐步提高精度
// 采样函数示例
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等高维数据降维算法

希望这个工具能帮助你在向量搜索开发中更高效地调试和优化,让抽象的向量数据变得触手可及。如果你有任何改进建议或使用心得,欢迎在评论区分享!

【免费下载链接】sqlite-vec Work-in-progress vector search SQLite extension that runs anywhere. 【免费下载链接】sqlite-vec 项目地址: https://gitcode.com/GitHub_Trending/sq/sqlite-vec

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值