Back to blog
    端侧语义搜索: 无需服务器的AI驱动搜索
    semantic searchon-device AIembeddingsmobile AIimplementationsegment:mobile-builder

    端侧语义搜索: 无需服务器的AI驱动搜索

    如何构建完全在用户手机上运行的语义搜索。本地嵌入、向量相似度,以及无需服务器或API即可对用户内容进行自然语言查询。

    EErtas Team·

    当用户不知道确切的词语时,关键词搜索就会失败。"上周关于预算会议的那封邮件"无法匹配标题为"第三季度财务回顾"的邮件。语义搜索理解含义,而非仅仅匹配关键词。

    标准方案将语义搜索放在带向量数据库的服务器上。但对于用户内容存储在本地的移动应用(笔记、消息、照片、文档),将这些内容发送到服务器就违背了保持端侧处理的初衷。

    端侧语义搜索让一切保持在本地。嵌入模型在手机上运行。向量索引存储在本地。搜索查询永远不会离开设备。

    语义搜索的工作原理

    1. 索引: 每段内容使用小型模型转换为嵌入向量(代表其含义的数字列表)
    2. 存储: 嵌入向量与内容一起存储在本地数据库中
    3. 查询: 用户的搜索查询使用同一模型转换为嵌入向量
    4. 匹配: 查询向量与所有存储的向量使用余弦相似度进行比较
    5. 排序: 结果按相似度分数排列返回

    核心在于嵌入。关于同一主题的两段文本会产生相似的向量,即使它们没有共同的关键词。

    嵌入模型

    端侧嵌入模型小巧且快速。不同于生成式LLM(600MB-1.7GB),嵌入模型通常只有20-80MB:

    模型大小维度速度 (iPhone 15)
    all-MiniLM-L6-v223MB384500+嵌入/秒
    nomic-embed-text-v1.555MB768200+嵌入/秒
    bge-small-en-v1.533MB384400+嵌入/秒

    以每秒200-500个嵌入的速度,索引1,000条笔记只需2-5秒。查询嵌入几乎是即时的(5ms以内)。

    运行嵌入模型

    你可以通过以下方式运行嵌入模型:

    ONNX Runtime Mobile: 支持ONNX格式的嵌入模型。适用于iOS(通过Swift)和Android(通过Kotlin)。是移动端嵌入推理最成熟的选项。

    // iOS使用ONNX Runtime
    let session = try ORTSession(env: env, modelPath: embeddingModelPath)
    let inputTensor = try ORTValue(tensorData: tokenizedInput, shape: shape)
    let outputs = try session.run(withInputs: ["input_ids": inputTensor])
    let embedding = outputs["embeddings"]!.tensorData()

    llama.cpp嵌入模式: llama.cpp可以使用嵌入标志从GGUF模型生成嵌入。这让你可以使用同一个推理引擎来进行生成和嵌入。

    向量存储

    SQLite加自定义扩展

    最简单的移动端方案: 将向量作为BLOB存储在SQLite中,在应用代码中计算相似度。

    // Android: 存储嵌入
    fun storeEmbedding(db: SQLiteDatabase, contentId: Long, embedding: FloatArray) {
        val blob = ByteBuffer.allocate(embedding.size * 4)
        embedding.forEach { blob.putFloat(it) }
        db.execSQL(
            "INSERT INTO embeddings (content_id, vector) VALUES (?, ?)",
            arrayOf(contentId, blob.array())
        )
    }
    
    // 按相似度搜索
    fun search(db: SQLiteDatabase, queryEmbedding: FloatArray, limit: Int): List<SearchResult> {
        val cursor = db.rawQuery("SELECT content_id, vector FROM embeddings", null)
        val results = mutableListOf<SearchResult>()
    
        while (cursor.moveToNext()) {
            val blob = cursor.getBlob(1)
            val stored = FloatArray(blob.size / 4)
            ByteBuffer.wrap(blob).asFloatBuffer().get(stored)
    
            val similarity = cosineSimilarity(queryEmbedding, stored)
            results.add(SearchResult(cursor.getLong(0), similarity))
        }
    
        return results.sortedByDescending { it.similarity }.take(limit)
    }

    这种方式简单,适用于最多约10,000个条目的集合。超过此数量时,线性扫描会变慢。

    SQLite加向量扩展

    对于更大的集合,使用支持近似最近邻(ANN)搜索的SQLite向量扩展:

    • sqlite-vss: 使用Faiss进行向量搜索的SQLite扩展。支持iOS和Android。
    • sqlite-vec: 专为嵌入式使用设计的轻量级向量搜索扩展。

    这些扩展在向量上创建索引,可以在数十万条目中实现亚毫秒级搜索。

    完整流程

    第1步: 索引内容

    当用户创建或修改内容(笔记、消息、文档)时,生成并存储其嵌入:

    func indexContent(_ content: Content) async {
        let embedding = await embeddingModel.encode(content.text)
        database.storeEmbedding(contentId: content.id, vector: embedding)
    }

    在后台运行索引。用户不应等待嵌入计算完成。

    第2步: 搜索

    当用户输入搜索查询时:

    func search(query: String) async -> [Content] {
        let queryEmbedding = await embeddingModel.encode(query)
        let results = database.similaritySearch(queryEmbedding, limit: 10)
        return results.map { fetchContent($0.contentId) }
    }

    搜索返回按语义相似度排序的结果。"预算会议记录"可以匹配"第三季度财务回顾",因为嵌入捕获了语义关系。

    第3步: 混合搜索

    将语义搜索与关键词搜索结合以获得最佳结果:

    1. 运行关键词搜索(SQLite FTS5)进行精确匹配
    2. 运行语义搜索进行含义匹配
    3. 合并并去重结果
    4. 按综合分数排序(关键词匹配加权)

    这同时处理精确查询("与John的会议")和模糊查询("关于项目时间线的那封邮件")。

    性能预算

    组件存储内存速度
    嵌入模型23-55MB推理时50-100MB200-500嵌入/秒
    向量索引 (10K条目, 384维)约15MB约15MB每次搜索5ms以内
    向量索引 (100K条目, 384维)约150MB约30MB (使用ANN索引)每次搜索10ms以内

    语义搜索的额外总体积: 40-200MB存储,搜索时65-130MB内存。这只是生成式LLM所需的一小部分,使其即使在资源受限的设备上也很实用。

    用例

    笔记应用

    按含义搜索所有笔记。"上周关于产品发布的会议记录"可以找到相关笔记,无论确切用词如何。

    邮件客户端

    按主题而非仅按发件人或主题行查找邮件。"关于合同续签的对话"可以找到正确的邮件线程。

    照片应用

    结合图像描述(端侧)实现基于文本的照片搜索。"海滩上的日落"可以找到匹配的照片,即使没有手动标签。

    文档管理器

    按内容和含义搜索PDF、文档和文件。

    与端侧LLM结合

    语义搜索与端侧生成模型自然配对。使用搜索结果作为LLM的上下文:

    1. 用户提问
    2. 语义搜索从用户数据中检索相关内容
    3. LLM使用检索到的内容作为上下文生成回答

    这就是端侧RAG。无需服务器。整个流程(嵌入、搜索、生成)在本地运行。

    对于生成组件,使用Ertas等平台在你的领域数据上微调模型。微调模型结合本地语义搜索,创造出强大且完全私密的AI助手。

    Ship AI that runs on your users' devices.

    Early bird pricing starts at $14.50/mo — locked in for life. Plans for builders and agencies.

    Ship AI that runs on your users' devices.

    Early bird pricing starts at $14.50/mo — locked in for life. Plans for builders and agencies.

    Keep reading