HarmonyOS 新手入门:ArkData 向量数据库,先跑通最小闭环

举报
蓝瘦的蜕变 发表于 2026/07/06 18:53:28 2026/07/06
【摘要】 前面 RDB 已经写到事务和 Sendable。再往 AI 应用走一步,就会遇到“向量数据库”。 这次以华为官方文档《通过向量数据库实现数据持久化 (ArkTS)》为准:向量数据库从 API version 18 开始支持,它既能保存向量数据,也能继续处理标量的关系型数据。`floatvector` 用来保存向量化结果,查询时可以用向量距离做排序。

# HarmonyOS 新手入门:ArkData 向量数据库,先跑通最小闭环

前面 RDB 已经写到事务和 Sendable。再往 AI 应用走一步,就会遇到“向量数据库”。

这次以华为官方文档《通过向量数据库实现数据持久化 (ArkTS)》为准:向量数据库从 API version 18 开始支持,它既能保存向量数据,也能继续处理标量的关系型数据。floatvector 用来保存向量化结果,查询时可以用向量距离做排序。

先说结论

向量数据库不是全文搜索,也不是 RDB 的替代品。

它适合处理“像不像”“接近不接近”这类相似度问题。标题、分类、时间、权限这些确定条件,仍然适合放在普通字段里。

这篇先不接真实模型,只用二维教学向量把最小闭环跑通。

这个 Demo 做什么

页面把一段文本转换成二维教学向量,写入 RDB 的 floatvector(2) 字段,然后用数据库侧 SQL 做相似检索:

SELECT id, title, content, repr, created_at, repr <=> ? AS distance
FROM knowledge_notes_v2
ORDER BY repr <=> ?
LIMIT ?

这里 <=> 表示余弦距离,distance 越小越接近。官方文档还说明 <-> 是 L2 距离;创建向量索引时,查询距离度量要和索引度量保持一致。

它怎么用

核心就五步。

第一步,调用 relationalStore.isVectorSupported() 判断设备是否支持向量数据库。

第二步,打开 RDB 时在 StoreConfig 里设置 vector: true

第三步,建表时使用 floatvector(N),Demo 为了便于理解固定为 floatvector(2)

第四步,使用 Float32Array 做参数绑定写入向量字段。

第五步,查询时保持 ORDER BY + LIMIT,这样更符合官方文档对向量索引命中的描述。

容易踩坑的地方

这篇用二维教学向量,是为了让字段维度、插入、距离排序都清楚可见。真实项目可以把二维特征替换成模型生成的高维向量,但表结构里的 floatvector(N) 维度要和写入的 Float32Array 保持一致。

还有一个运行前提要直接说清楚:向量数据库是 API 18+ 能力。项目的 compatibleSdkVersion 不能停在 API 12 这类旧版本,否则可能出现 isVectorSupported() 返回支持,但 CREATE TABLE ... floatvector 仍然抛出 801 Capability not support

只读模式也要避开。StoreConfig.isReadOnly 如果为 true,建表、插入、更新都会失败;这个 Demo 会显式设置 isReadOnly: false

欢迎评论区一起分享~

你们会把向量数据库先用在哪类本地 AI 场景里?笔记检索、案例匹配、知识库,还是图片相似搜索?如果你已经接过真实 Embedding,也可以聊聊维度、索引和召回效果是怎么调的。

完整示例代码

文件位置:ArkDataVectorStoreDemo.ets

还有一个容易混淆的点:官方的“应用数据向量化 / Embedding”是把文本或图片转成向量的模型能力,和这里演示的 floatvector 存储能力不是同一个 API。那篇 Embedding 文档里的设备形态约束,要单独判断,不能直接等同于向量数据库建表能力。

所以这个 Demo 明确使用二维教学向量,不调用 @ohos.data.intelligence。它的目标是先把 floatvector 建表、写入、距离查询这条链路讲清楚;真实项目如果要接官方 Embedding,先确认目标设备是否支持该模型能力,再把模型输出维度同步到 floatvector(N)

import { common } from '@kit.AbilityKit';
import { relationalStore } from '@kit.ArkData';
import { promptAction, router } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

const DB_NAME: string = 'vector_store_demo.db';
const TABLE_NOTE: string = 'knowledge_notes_v2';
const VECTOR_INDEX: string = 'note_vector_idx';
const VECTOR_DIMENSION: number = 2;
const RESULT_LIMIT: number = 8;
const ERROR_CAPABILITY_NOT_SUPPORTED: number = 801;
const VECTOR_SOURCE_TEXT: string = '二维教学向量,不调用官方 Embedding';
const EMBEDDING_SCOPE_TEXT: string = 'Embedding 设备形态限制需单独判断';
const VECTOR_TABLE_UNAVAILABLE_TIP: string =
  'isVectorSupported 返回支持,但 CREATE TABLE floatvector 返回 801。当前 Demo 已显式设置 isReadOnly=false;Demo 使用二维教学向量,不调用官方 Embedding。若仍失败,请继续检查系统镜像或真机向量数据库能力。';

class VectorNoteItem {
  id: number = 0;
  title: string = '';
  content: string = '';
  distance: number = 0;
  vectorText: string = '';
  createdAt: string = '';

  constructor(id: number, title: string, content: string, distance: number, vectorText: string, createdAt: string) {
    this.id = id;
    this.title = title;
    this.content = content;
    this.distance = distance;
    this.vectorText = vectorText;
    this.createdAt = createdAt;
  }
}

class VectorInfoItem {
  label: string = '';
  value: string = '';

  constructor(label: string, value: string) {
    this.label = label;
    this.value = value;
  }
}

@Entry
@Component
struct ArkDataVectorStoreDemo {
  @State noteTitle: string = 'ArkData 入门';
  @State noteContent: string = 'Preferences 适合保存轻量配置,RDB 适合保存结构化业务数据。';
  @State queryText: string = '本地配置应该用什么保存';
  @State tips: string = '先保存知识片段,再用数据库侧向量距离检索相似内容。';
  @State items: VectorNoteItem[] = [];
  @State infoItems: VectorInfoItem[] = [];
  @State showResultSheet: boolean = false;
  @State vectorSupportedText: string = '待检测';
  @State stageText: string = '等待初始化';
  @State indexStatusText: string = '尚未创建';

  private store: relationalStore.RdbStore | undefined = undefined;

  private errorToText(err: BusinessError): string {
    return `code=${err.code}, message=${err.message}`;
  }

  private isCapabilityNotSupported(err: BusinessError): boolean {
    return err.code === ERROR_CAPABILITY_NOT_SUPPORTED;
  }

  private markVectorTableUnavailable(action: string): void {
    this.items = [];
    this.vectorSupportedText = '建表不支持(801)';
    this.indexStatusText = '未创建';
    this.tips = VECTOR_TABLE_UNAVAILABLE_TIP;
    this.updateInfoItems(action);
  }

  aboutToAppear(): void {
    this.vectorSupportedText = relationalStore.isVectorSupported() ? '支持' : '不支持';
    if (relationalStore.isVectorSupported()) {
      this.loadNotes();
    } else {
      this.items = [];
      this.tips = '当前设备不支持向量数据库,页面可查看教学流程;保存和检索需要支持向量能力的设备。';
      this.updateInfoItems('设备不支持向量能力');
    }
  }

  private async getStore(): Promise<relationalStore.RdbStore> {
    if (this.store !== undefined) {
      return this.store;
    }

    if (!relationalStore.isVectorSupported()) {
      throw new Error('Vector store is not supported on this device');
    }

    const context = getContext(this) as common.UIAbilityContext;
    const config: relationalStore.StoreConfig = {
      name: DB_NAME,
      securityLevel: relationalStore.SecurityLevel.S1,
      vector: true,
      isReadOnly: false
    };
    this.stageText = '正在打开向量 RDB';
    const store = await relationalStore.getRdbStore(context, config);
    this.stageText = '正在创建向量表';
    await store.executeSql(
      `CREATE TABLE IF NOT EXISTS ${TABLE_NOTE} (` +
        'id INTEGER PRIMARY KEY AUTOINCREMENT, ' +
        'title TEXT NOT NULL, ' +
        'content TEXT NOT NULL, ' +
        `repr floatvector(${VECTOR_DIMENSION}) NOT NULL, ` +
        'created_at TEXT NOT NULL)'
    );
    await this.prepareVectorIndex(store);
    this.store = store;
    return store;
  }

  private async prepareVectorIndex(store: relationalStore.RdbStore): Promise<void> {
    try {
      this.stageText = '正在创建向量索引';
      await store.executeSql(
        `CREATE INDEX IF NOT EXISTS ${VECTOR_INDEX} ON ${TABLE_NOTE} USING GSDISKANN(repr COSINE)`
      );
      this.indexStatusText = '已创建';
    } catch (err) {
      const error = err as BusinessError;
      this.indexStatusText = '创建失败,已降级为无索引向量查询';
      console.error(`Vector index create failed: stage=${this.stageText}, ${this.errorToText(error)}`);
    }
  }

  private createTeachingVector(text: string): Float32Array {
    const source = text.length > 0 ? text : '空文本';
    let arkScore = 0;
    let uiScore = 0;
    for (let index = 0; index < source.length; index++) {
      const char = source.charAt(index);
      const code = source.charCodeAt(index);
      if (char === 'A' || char === 'a' || char === '数' || char === '据') {
        arkScore += 2;
      } else if (char === 'U' || char === 'u' || char === '界' || char === '面') {
        uiScore += 2;
      } else if (code % 2 === 0) {
        arkScore += 1;
      } else {
        uiScore += 1;
      }
    }
    const total = Math.max(1, arkScore + uiScore);
    return Float32Array.from([arkScore / total, uiScore / total]);
  }

  private vectorToText(vector: Float32Array): string {
    if (vector.length < VECTOR_DIMENSION) {
      return '[]';
    }
    return `[${vector[0].toFixed(3)}, ${vector[1].toFixed(3)}]`;
  }

  private readVector(value: relationalStore.ValueType): Float32Array {
    if (value instanceof Float32Array) {
      return value;
    }
    return new Float32Array(0);
  }

  private updateInfoItems(action: string): void {
    this.infoItems = [
      new VectorInfoItem('数据库名称', DB_NAME),
      new VectorInfoItem('表名', TABLE_NOTE),
      new VectorInfoItem('向量字段', `repr floatvector(${VECTOR_DIMENSION})`),
      new VectorInfoItem('向量索引', `${VECTOR_INDEX} / GSDISKANN / COSINE / ${this.indexStatusText}`),
      new VectorInfoItem('向量来源', VECTOR_SOURCE_TEXT),
      new VectorInfoItem('Embedding', EMBEDDING_SCOPE_TEXT),
      new VectorInfoItem('只读模式', 'false'),
      new VectorInfoItem('向量能力', this.vectorSupportedText),
      new VectorInfoItem('初始化阶段', this.stageText),
      new VectorInfoItem('最近动作', action),
      new VectorInfoItem('当前展示数', `${this.items.length}`),
      new VectorInfoItem('检索方式', 'ORDER BY repr <=> ? LIMIT 8')
    ];
  }

  private readRows(resultSet: relationalStore.ResultSet, hasDistance: boolean): VectorNoteItem[] {
    const rows: VectorNoteItem[] = [];
    const idIndex = resultSet.getColumnIndex('id');
    const titleIndex = resultSet.getColumnIndex('title');
    const contentIndex = resultSet.getColumnIndex('content');
    const vectorIndex = resultSet.getColumnIndex('repr');
    const createdAtIndex = resultSet.getColumnIndex('created_at');
    const distanceIndex = hasDistance ? resultSet.getColumnIndex('distance') : -1;
    while (resultSet.goToNextRow()) {
      const vector = this.readVector(resultSet.getValue(vectorIndex));
      const distance = hasDistance ? resultSet.getDouble(distanceIndex) : 0;
      rows.push(new VectorNoteItem(
        resultSet.getLong(idIndex),
        resultSet.getString(titleIndex),
        resultSet.getString(contentIndex),
        distance,
        this.vectorToText(vector),
        resultSet.getString(createdAtIndex)
      ));
    }
    return rows;
  }

  private async loadNotes(action: string = '读取全部知识片段'): Promise<void> {
    let resultSet: relationalStore.ResultSet | undefined = undefined;
    if (!relationalStore.isVectorSupported()) {
      this.items = [];
      this.vectorSupportedText = '不支持';
      this.tips = '当前设备不支持向量数据库,无法创建 floatvector 表。';
      this.updateInfoItems('设备不支持向量能力');
      return;
    }

    try {
      const store = await this.getStore();
      this.stageText = '正在读取向量表';
      resultSet = await store.querySql(
        `SELECT id, title, content, repr, created_at FROM ${TABLE_NOTE} ORDER BY id DESC`
      );
      this.items = this.readRows(resultSet, false);
      this.stageText = '读取完成';
      this.updateInfoItems(action);
    } catch (err) {
      const error = err as BusinessError;
      if (this.isCapabilityNotSupported(error)) {
        this.markVectorTableUnavailable('向量表不可用');
        promptAction.showToast({ message: '向量表不可用' });
        console.error(`Vector store load failed: stage=${this.stageText}, ${this.errorToText(error)}`);
        return;
      }
      this.items = [];
      this.tips = `读取失败:阶段=${this.stageText}${this.errorToText(error)}`;
      this.updateInfoItems('读取失败');
      promptAction.showToast({ message: '读取失败' });
      console.error(`Vector store load failed: stage=${this.stageText}, ${this.errorToText(error)}`);
    } finally {
      if (resultSet !== undefined) {
        resultSet.close();
      }
    }
  }

  private async saveNote(): Promise<void> {
    try {
      if (!relationalStore.isVectorSupported()) {
        this.tips = '当前设备不支持向量数据库,无法保存向量字段。';
        this.updateInfoItems('保存被跳过');
        promptAction.showToast({ message: '设备不支持向量能力' });
        return;
      }

      const store = await this.getStore();
      const title = this.noteTitle.length > 0 ? this.noteTitle : '未命名知识片段';
      const content = this.noteContent.length > 0 ? this.noteContent : '空内容';
      const vector = this.createTeachingVector(`${title}\n${content}`);
      const values: relationalStore.ValuesBucket = {
        title: title,
        content: content,
        repr: vector,
        created_at: new Date().toString()
      };
      await store.insert(TABLE_NOTE, values);
      await this.loadNotes('保存知识片段');
      this.tips = `保存成功:二维向量 ${this.vectorToText(vector)} 已写入向量字段。`;
      promptAction.showToast({ message: '向量记录已保存' });
    } catch (err) {
      const error = err as BusinessError;
      if (this.isCapabilityNotSupported(error)) {
        this.markVectorTableUnavailable('保存被跳过');
        promptAction.showToast({ message: '向量表不可用' });
        console.error(`Vector store save failed: stage=${this.stageText}, ${this.errorToText(error)}`);
        return;
      }
      this.tips = `保存失败:阶段=${this.stageText}${this.errorToText(error)}`;
      promptAction.showToast({ message: '保存失败' });
      console.error(`Vector store save failed: stage=${this.stageText}, ${this.errorToText(error)}`);
    }
  }

  private async searchNotes(): Promise<void> {
    let resultSet: relationalStore.ResultSet | undefined = undefined;
    if (!relationalStore.isVectorSupported()) {
      this.tips = '当前设备不支持向量数据库,无法执行向量距离检索。';
      this.updateInfoItems('检索被跳过');
      promptAction.showToast({ message: '设备不支持向量能力' });
      return;
    }

    try {
      const store = await this.getStore();
      const queryVector = this.createTeachingVector(this.queryText);
      this.stageText = '正在执行向量距离检索';
      resultSet = await store.querySql(
        `SELECT id, title, content, repr, created_at, repr <=> ? AS distance FROM ${TABLE_NOTE} ` +
          'ORDER BY repr <=> ? LIMIT ?',
        [queryVector, queryVector, RESULT_LIMIT]
      );
      this.items = this.readRows(resultSet, true);
      this.stageText = '检索完成';
      this.updateInfoItems('向量距离检索');
      this.tips = `检索完成:查询向量 ${this.vectorToText(queryVector)},distance 越小越相似。`;
      this.showResultSheet = true;
    } catch (err) {
      const error = err as BusinessError;
      if (this.isCapabilityNotSupported(error)) {
        this.markVectorTableUnavailable('检索被跳过');
        promptAction.showToast({ message: '向量表不可用' });
        console.error(`Vector store search failed: stage=${this.stageText}, ${this.errorToText(error)}`);
        return;
      }
      this.tips = `检索失败:阶段=${this.stageText}${this.errorToText(error)}`;
      promptAction.showToast({ message: '检索失败' });
      console.error(`Vector store search failed: stage=${this.stageText}, ${this.errorToText(error)}`);
    } finally {
      if (resultSet !== undefined) {
        resultSet.close();
      }
    }
  }

  private async clearNotes(): Promise<void> {
    try {
      if (!relationalStore.isVectorSupported()) {
        this.items = [];
        this.tips = '当前设备不支持向量数据库,没有可清空的向量表。';
        this.updateInfoItems('清空被跳过');
        return;
      }

      const store = await this.getStore();
      await store.executeSql(`DELETE FROM ${TABLE_NOTE}`);
      await this.loadNotes('清空表数据');
      this.tips = '数据已清空。';
      promptAction.showToast({ message: '已清空' });
    } catch (err) {
      const error = err as BusinessError;
      if (this.isCapabilityNotSupported(error)) {
        this.markVectorTableUnavailable('清空被跳过');
        promptAction.showToast({ message: '向量表不可用' });
        console.error(`Vector store clear failed: stage=${this.stageText}, ${this.errorToText(error)}`);
        return;
      }
      this.tips = `清空失败:阶段=${this.stageText}${this.errorToText(error)}`;
      promptAction.showToast({ message: '清空失败' });
      console.error(`Vector store clear failed: stage=${this.stageText}, ${this.errorToText(error)}`);
    }
  }

  private async showVectorInfo(): Promise<void> {
    await this.loadNotes('查看向量数据库信息');
    this.showResultSheet = true;
  }

  @Builder
  private vectorInfoSheet() {
    Column({ space: 14 }) {
      Text('向量数据库信息')
        .width('100%')
        .fontSize(22)
        .fontWeight(700)
        .fontColor('#0F172A')

      Text('当前 Demo 使用官方文档的 floatvector(N)、GSDISKANN 索引和 <=> 余弦距离查询。')
        .width('100%')
        .fontSize(13)
        .fontColor('#64748B')

      Column({ space: 8 }) {
        ForEach(this.infoItems, (item: VectorInfoItem) => {
          Column({ space: 4 }) {
            Text(item.label)
              .fontSize(12)
              .fontColor('#64748B')
            Text(item.value)
              .fontSize(16)
              .fontColor('#0F172A')
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#F8FAFC')
          .borderRadius(8)
        }, (item: VectorInfoItem) => item.label)
      }
      .width('100%')

      Button('关闭')
        .width('100%')
        .height(44)
        .fontSize(16)
        .backgroundColor('#166534')
        .onClick(() => {
          this.showResultSheet = false;
        })
    }
    .width('100%')
    .padding({ left: 20, right: 20, top: 12, bottom: 20 })
  }

  build() {
    Scroll() {
      Column({ space: 18 }) {
        Row() {
          Text('返回')
            .fontSize(14)
            .fontColor('#166534')
            .onClick(() => {
              router.back();
            })
          Blank()
        }
        .width('100%')

        Text('向量数据库入门')
          .width('100%')
          .fontSize(28)
          .fontWeight(700)
          .fontColor('#182431')

        Text('把文本片段转换成二维教学向量,保存到 RDB 的向量字段,再用 SQL 距离排序找回相似内容。')
          .width('100%')
          .fontSize(14)
          .fontColor('#56616F')

        Text(`当前向量能力:${this.vectorSupportedText}`)
          .width('100%')
          .fontSize(13)
          .fontColor(this.vectorSupportedText === '支持' ? '#166534' : '#B45309')

        TextInput({ placeholder: '知识标题', text: this.noteTitle })
          .height(44)
          .fontSize(16)
          .backgroundColor('#F0FDF4')
          .fontColor('#0F172A')
          .onChange((value: string) => {
            this.noteTitle = value;
          })

        TextArea({ placeholder: '知识内容', text: this.noteContent })
          .height(96)
          .fontSize(15)
          .backgroundColor('#F0FDF4')
          .fontColor('#0F172A')
          .onChange((value: string) => {
            this.noteContent = value;
          })

        TextInput({ placeholder: '输入检索问题', text: this.queryText })
          .height(44)
          .fontSize(16)
          .backgroundColor('#ECFDF5')
          .fontColor('#0F172A')
          .onChange((value: string) => {
            this.queryText = value;
          })

        Row({ space: 10 }) {
          Button('保存')
            .layoutWeight(1)
            .height(44)
            .fontSize(15)
            .backgroundColor('#166534')
            .onClick(() => {
              this.saveNote();
            })
          Button('检索')
            .layoutWeight(1)
            .height(44)
            .fontSize(15)
            .fontColor('#0F172A')
            .backgroundColor('#BBF7D0')
            .onClick(() => {
              this.searchNotes();
            })
          Button('查看')
            .layoutWeight(1)
            .height(44)
            .fontSize(15)
            .fontColor('#0F172A')
            .backgroundColor('#DCFCE7')
            .onClick(() => {
              this.showVectorInfo();
            })
          Button('清空')
            .layoutWeight(1)
            .height(44)
            .fontSize(15)
            .fontColor('#0F172A')
            .backgroundColor('#CBD5E1')
            .onClick(() => {
              this.clearNotes();
            })
        }
        .width('100%')

        Column({ space: 8 }) {
          ForEach(this.items, (item: VectorNoteItem) => {
            Column({ space: 5 }) {
              Text(item.title)
                .width('100%')
                .fontSize(17)
                .fontWeight(700)
                .fontColor('#0F172A')
              Text(item.content)
                .width('100%')
                .fontSize(13)
                .fontColor('#334155')
              Text(`distance=${item.distance.toFixed(4)} | vector=${item.vectorText} | id=${item.id}`)
                .width('100%')
                .fontSize(12)
                .fontColor('#64748B')
              Text(item.createdAt)
                .width('100%')
                .fontSize(12)
                .fontColor('#94A3B8')
            }
            .width('100%')
            .padding(14)
            .backgroundColor('#F8FAFC')
            .borderRadius(8)
          }, (item: VectorNoteItem) => `${item.id}`)
        }
        .width('100%')

        Text(this.tips)
          .width('100%')
          .fontSize(13)
          .fontColor('#64748B')
      }
      .width('100%')
      .padding(24)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFFFFF')
    .bindSheet(this.showResultSheet, this.vectorInfoSheet(), {
      height: SheetSize.MEDIUM,
      dragBar: true,
      showClose: true,
      onDisappear: () => {
        this.showResultSheet = false;
      }
    })
  }
}
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。