
裝置端語意搜尋:無需伺服器的 AI 搜尋
如何建構完全在使用者手機上運行的語意搜尋。本地嵌入、向量相似度和自然語言查詢,無需伺服器或 API 即可搜尋使用者內容。
當使用 者不知道確切的詞彙時,關鍵字搜尋就會失敗。「上週那封關於預算會議的電子郵件」無法匹配主旨為「第三季財務審查」的郵件。語意搜尋理解意義,而非僅僅是關鍵字。
標準做法是將語意搜尋放在具有向量資料庫的伺服器上。但對於使用者內容在本地的行動應用(筆記、訊息、照片、文件),將該內容發送到伺服器就違背了將其保留在裝置上的初衷。
裝置端語意搜尋讓一切保持在本地。嵌入模型在手機上運行。向量索引存在本地儲存中。搜尋查詢永遠不會離開裝置。
語意搜尋的工作原理
- 索引: 每段內容使用小型模型轉換為嵌入向量(一組代表其意義的數字)
- 儲存: 嵌入向量與內容一起儲存在本地資料庫中
- 查詢: 使用者的搜尋查詢使用相同的模型轉換為嵌入向量
- 比對: 查詢向量使用餘弦相似度與所有已儲存的向量進行比較
- 排序: 結果按相似度分數排序返回
奧妙在於嵌入。關於同 一主題的兩段文字會產生相似的向量,即使它們沒有共同的關鍵字。
嵌入模型
裝置端嵌入模型小巧且快速。不同於生成式 LLM(600MB-1.7GB),嵌入模型通常為 20-80MB:
| 模型 | 大小 | 維度 | 速度(iPhone 15) |
|---|---|---|---|
| all-MiniLM-L6-v2 | 23MB | 384 | 500+ 嵌入/秒 |
| nomic-embed-text-v1.5 | 55MB | 768 | 200+ 嵌入/秒 |
| bge-small-en-v1.5 | 33MB | 384 | 400+ 嵌入/秒 |
以每秒 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:混合搜尋
結合語意搜尋與關鍵字搜尋以獲得最佳結果:
- 運行關鍵字搜尋(SQLite FTS5)進行精確匹配
- 運行語意搜尋進行基於意義的匹配
- 合併並去除重複結果
- 按綜合分數排序(關鍵字匹配優先提升)
這同時處理了精確查詢(「與 John 的會議」)和模糊查詢(「那封關於專案時程的郵件」)。
效能預算
| 元件 | 儲存 | RAM | 速度 |
|---|---|---|---|
| 嵌入模型 | 23-55MB | 推論時 50-100MB | 200-500 嵌入/秒 |
| 向量索引(10K 項,384d) | 約 15MB | 約 15MB | 每次搜尋 5ms 以內 |
| 向量索引(100K 項,384d) | 約 150MB | 約 30MB(使用 ANN 索引) | 每次搜尋 10ms 以內 |
語意搜尋的總額外佔用:40-200MB 儲存、65-130MB 搜尋時 RAM。這只是生成式 LLM 所需的一小部分,即使在受限裝置上也是可行的。
使用案例
筆記應用
按意義搜尋所有筆記。「上週關於產品發布的會議記錄」能找到相關筆記,無論確切用詞為何。
郵件用戶端
按主題而非僅按寄件者或主旨找到郵件。「關於合約續約的對話」找出正確的郵件串。
照片應用
搭配影像描述(裝置端)實現基於文字的照片搜尋。「海邊的日落」即使沒有手動標記也能找到匹配的照片。
文件管理器
按內容和意義搜尋 PDF、文件和檔案。
搭配裝置端 LLM 使用
語意搜尋與裝置端生成模型自然搭配。使用搜尋結果作為 LLM 的上下文:
- 使用者提出問題
- 語意搜尋從使用者資料中檢索相關內容
- LLM 使用檢索到的內容作為上下文生成答案
這就是裝置端 RAG。無需 伺服器。整個流程(嵌入、搜尋、生成)都在本地運行。
對於生成元件,使用像 Ertas 這樣的平台在你的領域資料上微調模型。微調模型搭配本地語意搜尋,創造出強大且完全私密的 AI 助手。
Ship AI that runs on your users' devices.
Free plan with 30 credits/mo, no card required. Paid plans from $25/mo USD.
Ship AI that runs on your users' devices.
Free plan with 30 credits/mo, no card required. Paid plans from $25/mo USD.


