跳转至

Flutter 向量计算

将文本转换为高维向量(Embedding),实现语义搜索、相似度计算和聚类。


功能预览

向量计算页面

左侧导航选择 🔗 向量计算 → 输入两段文本 → 计算相似度 → 柱状图可视化

💡 示例:输入 "Flutter 是什么" 和 "Dart 是什么",相似度约为 0.85; 输入 "Flutter 是什么" 和 "苹果是什么",相似度约为 0.15。数值越接近 1 表示语义越相近。


目录


基础概念

Embedding 将文本映射为高维向量空间中的一个点。语义相近的文本在向量空间中距离更近。

// 输入一段文本
"如何写 Flutter 异步代码"

// 输出一个浮点数数组 (例如 1536 维)
[0.123, -0.456, 0.789, ...]

典型应用场景:

  • 语义搜索:用查询向量与文档向量做相似度排序
  • 聚类分析:对大量文本做主题聚类
  • 推荐系统:基于用户兴趣向量推荐相似内容

调用向量 API

Future<List<double>> getEmbedding(String text) async {
  final response = await http.post(
    Uri.parse('${dotenv.env['APINEXUS_BASE_URL']!}/embeddings'),
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ${dotenv.env['APINEXUS_API_KEY']!}',
    },
    body: json.encode({
      'model': 'text-embedding-3-small',
      'input': text,
    }),
  );
  final data = json.decode(response.body);
  final embedding = data['data'][0]['embedding'] as List;
  return embedding.cast<double>();
}

批量调用(多个文本一次请求):

Future<List<List<double>>> getBatchEmbeddings(
  List<String> texts,
) async {
  final response = await http.post(
    Uri.parse('${dotenv.env['APINEXUS_BASE_URL']!}/embeddings'),
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ${dotenv.env['APINEXUS_API_KEY']!}',
    },
    body: json.encode({
      'model': 'text-embedding-3-small',
      'input': texts,
    }),
  );
  final data = json.decode(response.body);
  final results = data['data'] as List;
  return [
    for (final item in results)
      (item['embedding'] as List).cast<double>()
  ];
}

余弦相似度

计算两个向量的余弦相似度,范围 [-1, 1],越接近 1 表示越相似:

double cosineSimilarity(List<double> a, List<double> b) {
  assert(a.length == b.length);
  double dotProduct = 0;
  double normA = 0;
  double normB = 0;
  for (int i = 0; i < a.length; i++) {
    dotProduct += a[i] * b[i];
    normA += a[i] * a[i];
    normB += b[i] * b[i];
  }
  return dotProduct / (sqrt(normA) * sqrt(normB));
}

示例:

final helloVec = await getEmbedding('你好');
final hiVec = await getEmbedding('嗨');
final goodbyeVec = await getEmbedding('再见');

cosineSimilarity(helloVec, hiVec);       // 接近 1
cosineSimilarity(helloVec, goodbyeVec);  // 较低

简单的语义搜索

class SemanticSearch {
  final Map<String, List<double>> _index = {};

  Future<void> addDocuments(List<String> docs) async {
    final vectors = await getBatchEmbeddings(docs);
    for (int i = 0; i < docs.length; i++) {
      _index[docs[i]] = vectors[i];
    }
  }

  List<MapEntry<String, double>> search(String query, {int topK = 5}) {
    final queryVec = await getEmbedding(query);
    final results = <MapEntry<String, double>>[];
    _index.forEach((doc, vec) {
      results.add(MapEntry(doc, cosineSimilarity(queryVec, vec)));
    });
    results.sort((a, b) => b.value.compareTo(a.value));
    return results.take(topK).toList();
  }
}

常用模型

模型 维度 说明
text-embedding-3-small 1536 轻量快速,适合大多数场景
text-embedding-3-large 3072 更精准,计算资源消耗更高
text-embedding-ada-002 1536 兼容老代码
dashscope-text-embedding-v3 1536 中文语义更强
bge-m3 1024 多语言,性价比高

完整示例

在 Flutter UI 中实现语义搜索:

class SearchPage extends StatefulWidget {
  const SearchPage({super.key});

  @override
  State<SearchPage> createState() => _SearchPageState();
}

class _SearchPageState extends State<SearchPage> {
  final _search = SemanticSearch();
  final _controller = TextEditingController();
  List<MapEntry<String, double>> _results = [];
  bool _loaded = false;
  bool _searching = false;

  @override
  void initState() {
    super.initState();
    _loadData();
  }

  Future<void> _loadData() async {
    await _search.addDocuments([
      'Flutter 是跨平台 UI 框架',
      'Dart 支持异步编程 async/await',
      'Provider 是轻量级状态管理方案',
      '使用 http 包发起网络请求',
      'ListView.builder 实现大列表懒加载',
      'Navigator 管理页面路由栈',
    ]);
    setState(() => _loaded = true);
  }

  Future<void> _doSearch() async {
    if (!_loaded || _searching || _controller.text.isEmpty) return;
    setState(() => _searching = true);
    final results = await _search.search(_controller.text);
    setState(() {
      _results = results;
      _searching = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('语义搜索')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: const InputDecoration(hintText: '搜索关键词'),
                  ),
                ),
                const SizedBox(width: 8),
                ElevatedButton(
                  onPressed: _doSearch,
                  child: Text(_searching ? '搜索中...' : '搜索'),
                ),
              ],
            ),
            const SizedBox(height: 16),
            Expanded(
              child: ListView.builder(
                itemCount: _results.length,
                itemBuilder: (_, i) => ListTile(
                  title: Text(_results[i].key),
                  subtitle: Text('相似度: ${_results[i].value.toStringAsFixed(3)}'),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

继续学习