移动端部署EmbeddingGemma-300M:5个核心技巧实现离线语义搜索

移动端部署EmbeddingGemma-300M:5个核心技巧实现离线语义搜索
1. 项目概述为什么要在移动端跑EmbeddingGemma-300M最近在折腾移动端AI应用的朋友估计都绕不开一个词RAG检索增强生成。简单说就是让AI在回答你问题前先在自己的知识库里搜一搜找到最相关的信息再基于这些信息生成答案。这比让AI凭空瞎编要靠谱得多。而RAG的“搜索引擎”核心就是一个好的文本嵌入模型它负责把文本无论是你的问题还是知识库里的文档转换成计算机能理解的“向量”然后通过计算向量之间的相似度来找到最匹配的内容。EmbeddingGemma-300M就是谷歌家推出的一个专门干这活的“小个子专家”。别看它只有3亿参数比动辄百亿、千亿的大语言模型苗条得多但它在文本表示任务上非常专注效果不俗最关键的是它天生就是为移动端和边缘设备设计的。这意味着我们可以在Android手机或者iPhone上直接运行这个模型实现完全离线的、低延迟的语义搜索。想象一下你的App里有一个庞大的帮助文档库用户输入问题App瞬间就能在本地找到最相关的几篇文章无需联网没有隐私泄露风险体验丝滑。这就是我们这次要实战的目标。但把模型塞进手机里跑起来和写个“Hello World”可不一样。从模型格式转换、推理引擎选择、内存优化到前后端数据交互每一步都有坑。我花了差不多两周时间在Android和iOS上分别趟了一遍把关键的技术难点和实操技巧都摸清楚了。这篇文章我就把这5个最核心、最能帮你省时间的技巧掰开揉碎了讲清楚目标是让你看完就能动手避开我踩过的那些坑。2. 核心技巧一模型转换与量化——从PyTorch到TFLite/ONNX的实战路径模型转换是移动端部署的第一步也是最容易卡住的一步。EmbeddingGemma-300M官方提供了PyTorch格式的权重但移动端尤其是Android的主流推理引擎是TensorFlow Lite。iOS生态则对Core ML更友好但ONNX也是一个通用性很强的选择。2.1 转换工具链选择与避坑首选路径PyTorch - ONNX - TensorFlow Lite (for Android) / Core ML (for iOS)为什么不直接从PyTorch到TFLite因为官方的PyTorch到TFLite转换工具如torch.jit.trace再转换对动态模型结构的支持不够友好而EmbeddingGemma这类Transformer模型往往包含一些动态操作如可变长度序列。ONNX作为一个中间表示生态更成熟转换成功率更高。安装依赖你需要一个配置好的Python环境。核心包是torch,transformers,onnx,onnxruntime。对于TFLite转换还需要tensorflow。建议使用虚拟环境管理。pip install torch transformers onnx onnxruntime # 如果需要转TFLite pip install tensorflow # 如果需要转Core ML需要macOS环境和coremltools pip install coremltools加载PyTorch模型并导出ONNX这是最关键的一步。你需要准备一个“伪输入”来让PyTorch模型执行一次前向传播ONNX记录下这个计算图。from transformers import AutoTokenizer, AutoModel import torch model_name google/embedding-gemma-300m tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModel.from_pretrained(model_name) model.eval() # 设置为评估模式 # 准备示例输入 dummy_input tokenizer(这是一个测试句子, return_tensorspt) # 注意EmbeddingGemma的输入是input_ids和attention_mask input_ids dummy_input[input_ids] attention_mask dummy_input[attention_mask] # 导出ONNX模型 torch.onnx.export( model, (input_ids, attention_mask), # 模型输入必须是一个元组 embeddinggemma-300m.onnx, input_names[input_ids, attention_mask], output_names[last_hidden_state], # 输出通常是最后一层隐藏状态 dynamic_axes{ input_ids: {0: batch_size, 1: sequence_length}, attention_mask: {0: batch_size, 1: sequence_length}, last_hidden_state: {0: batch_size, 1: sequence_length} }, # 声明动态维度支持可变长度输入 opset_version14, # 使用较新的opset版本兼容性更好 do_constant_foldingTrue )注意dynamic_axes参数至关重要。它告诉ONNX我们的输入序列长度(sequence_length)和批大小(batch_size)是可变的。没有这个转换出来的模型可能只支持固定长度的输入实用性大打折扣。2.2 量化让模型在手机上“跑得动”的关键原始FP32模型对于移动端来说依然太大、太慢。量化是将模型权重和激活值从高精度如FP32转换为低精度如INT8的过程能显著减少模型体积、提升推理速度、降低功耗。实操中的量化策略选择动态范围量化最简单仅量化权重为INT8激活值仍为FP32。在TFLite中很容易实现。import tensorflow as tf converter tf.lite.TFLiteConverter.from_onnx_model(embeddinggemma-300m.onnx) converter.optimizations [tf.lite.Optimize.DEFAULT] # 启用默认优化包含动态范围量化 tflite_model converter.convert() with open(embeddinggemma-300m_dynamic.tflite, wb) as f: f.write(tflite_model)优点几乎无损兼容性好。缺点加速效果相对有限。全整数量化将权重和激活值都量化为INT8。这需要一个小型的代表性数据集来校准激活值的动态范围。def representative_dataset(): # 准备一些代表性的文本样本模拟真实输入分布 samples [查询示例1, 文档内容示例2, 另一个搜索词] for sample in samples: inputs tokenizer(sample, return_tensorspt, paddingTrue, truncationTrue, max_length512) yield [inputs[input_ids].numpy().astype(np.int32), inputs[attention_mask].numpy().astype(np.int32)] converter.representative_dataset representative_dataset converter.target_spec.supported_ops [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] converter.inference_input_type tf.int32 # 输入类型 converter.inference_output_type tf.float32 # 输出类型通常保持浮点 tflite_model_int8 converter.convert()优点体积最小推理速度最快能利用硬件加速如ARM的Dot Product指令。缺点需要校准数据集精度可能有微小损失对模型中的某些特殊操作如LayerNorm支持可能不完美需要测试。我的经验对于EmbeddingGemma-300M做语义搜索动态范围量化通常是安全且有效的起点。模型体积能从约1.2GBFP32压缩到300MB左右速度提升明显且精度损失在语义相似度计算中几乎感知不到。只有在极端追求速度和存储空间比如希望模型小于150MB时才需要考虑全整数量化并且务必用你的业务数据测试召回效果。3. 核心技巧二移动端推理引擎的选型与集成模型准备好了接下来要选择在手机端加载和运行它的“引擎”。这个选择直接影响开发效率、性能和兼容性。3.1 Android端TensorFlow Lite Interpreter vs. Google AI Edge RAG库方案A直接使用TFLite Interpreter这是最灵活、最通用的方式。你将.tflite模型文件打包进App的assets或从网络下载然后用TFLite的Java/Android API加载运行。// Kotlin 示例代码片段 import org.tensorflow.lite.Interpreter import java.nio.ByteBuffer import java.nio.ByteOrder class EmbeddingTFLiteHelper(context: Context) { private lateinit var interpreter: Interpreter init { // 1. 从assets加载模型 val assetManager context.assets val modelFile assetManager.open(embeddinggemma-300m_dynamic.tflite) val modelBuffer modelFile.readBytes() val byteBuffer ByteBuffer.allocateDirect(modelBuffer.size).apply { order(ByteOrder.nativeOrder()) put(modelBuffer) } // 2. 创建解释器 val options Interpreter.Options().apply { // 可选设置线程数 setNumThreads(4) // 可选启用XNNPACK委托Android平台高性能CPU推理 // addDelegate(XNNPackDelegate()) } interpreter Interpreter(byteBuffer, options) } fun infer(inputIds: ArrayIntArray, attentionMask: ArrayIntArray): FloatArray { // 准备输入输出 val inputs mapOf(0 to inputIds, 1 to attentionMask) val outputs HashMapInt, Any() val outputShape intArrayOf(batchSize, seqLength, hiddenSize) // 需要根据模型确定 val outputBuffer Array(1) { Array(seqLength) { FloatArray(hiddenSize) } } outputs[0] outputBuffer interpreter.runForMultipleInputsOutputs(inputs, outputs) // 处理输出例如取[CLS] token的表示或做平均池化 return poolOutput(outputBuffer[0]) } }优点完全控制可以集成任何TFLite模型。缺点需要自己处理tokenization文本转ID、padding、输出池化等所有预处理和后处理逻辑代码量较大。方案B使用Google AI Edge RAG库如果官方文档如snippet中提到的提供了针对EmbeddingGemma的Google AI Edge RAG库支持强烈建议优先使用。这个库是谷歌为移动端RAG场景量身定制的它很可能已经封装了模型加载、推理、文本预处理分词和向量相似度计算等一系列功能。// 假设的API调用方式需查阅官方库文档 val ragClient EdgeRagClient.builder(context) .setEmbeddingModel(EmbeddingModel.EMBEDDING_GEMMA_300M) .build() val queryEmbedding ragClient.embed(用户查询文本) val results ragClient.search(queryEmbedding, myDocumentCollection)优点开发效率极高性能经过深度优化可能直接利用硬件加速如GPU、NPU。缺点灵活性受库本身功能限制依赖谷歌的发布和支持。我的选择如果项目时间紧且官方库功能满足需求毫不犹豫选方案B。如果需要进行深度定制例如使用特殊的分词方式、修改池化策略或者官方库不支持你的特定模型版本那么方案A是可靠的备选。3.2 iOS端Core ML vs. ONNX Runtime方案ACore ML这是苹果原生生态的首选。你需要将模型转换为.mlmodel或.mlpackage格式。转换使用coremltools在macOS上进行。import coremltools as ct model ct.converters.onnx.convert( modelembeddinggemma-300m.onnx, minimum_deployment_targetct.target.iOS16, # 根据你的最低支持版本设置 compute_unitsct.ComputeUnit.ALL, # 允许使用CPU、GPU和神经引擎 ) model.save(EmbeddingGemma300m.mlpackage)集成将生成的.mlpackage拖入Xcode项目。Xcode会自动生成Swift类。调用let model try! EmbeddingGemma300m(configuration: MLModelConfiguration()) let input EmbeddingGemma300mInput(input_ids: inputIds, attention_mask: attentionMask) let output try! model.prediction(input: input) let embeddings output.last_hidden_state优点与iOS系统深度集成能自动调用Apple Neural EngineANE获得最佳能效比内存管理优秀。缺点模型转换有时会遇到不支持的算子需要手动实现或寻找替代方案调试相对黑盒。方案BONNX Runtime这是一个跨平台推理引擎通过CocoaPods或Swift Package Manager集成。import OrtExtensions // 初始化环境 let env try ORTEnv(loggingLevel: .warning) let session try ORTSession(env: env, modelPath: modelURL.path, sessionOptions: nil) // 准备输入 let inputIdsTensor try ORTValue( tensorData: NSMutableData(data: inputIdsData), elementType: .int32, shape: [batchSize, seqLength] ) // ... 类似准备attention_mask // 运行推理 let outputs try session.run( inputs: [input_ids: inputIdsTensor, attention_mask: attentionMaskTensor], outputNames: [last_hidden_state], runOptions: nil ) let embeddingTensor outputs[last_hidden_state]!优点跨平台模型格式统一算子支持广泛社区活跃。缺点在iOS上通常无法直接利用ANE性能可能略逊于优化良好的Core ML模型需要自行管理内存和线程。我的经验对于追求极致性能和电池寿命的iOS应用优先尝试Core ML路径。尽管转换可能遇到问题但一旦成功收益巨大。如果Core ML转换失败或需要保持与Android端模型格式的统一ONNX Runtime是一个优秀的备选它的性能在CPU上也非常可靠。4. 核心技巧三预处理与后处理的移动端高效实现模型推理本身只是一部分把文本变成模型能吃的input_ids再把模型输出的last_hidden_state变成我们需要的句子向量这两头预处理和后处理在移动端的实现效率同样关键。4.1 Tokenization的移动端移植EmbeddingGemma使用与Gemma相同的TokenizerSentencePiece。在服务器端我们直接用transformers库。在移动端我们需要一个轻量级的实现。方案集成Tokenizer的原生实现Android (Java/Kotlin)寻找一个纯Java/Kotlin实现的SentencePiece库例如一些开源项目提供的spm-java封装。或者将transformers库中的tokenizer相关逻辑主要是vocab文件和sentencepiece模型提取出来用更底的方式实现。这比较复杂。更实际的建议如果使用Google AI Edge RAG库它极大概率内置了Tokenizer。如果使用纯TFLite一个折中方案是在服务器端或构建阶段将你的文档库全部预处理成向量并存入移动端数据库。对于用户实时输入的查询如果数量不大可以暂时用一个小巧的、算法简单的分词器如按空格分割进行近似或者接受轻微的性能开销在后台线程运行一个移植的Tokenizer。iOS (Swift)苹果的NaturalLanguage框架功能强大但对于特定的SentencePiece模型支持有限。更好的选择是使用开源库例如通过Swift Package Manager集成SentencePieceSwift。同样如果使用Core ML可以尝试将Tokenizer也封装到Core ML模型中作为预处理层但这需要修改模型结构难度较高。使用ONNX Runtime时可以寻找ONNX格式的Tokenizer模型与主模型串联执行。我的实操简化策略 对于文档侧你的知识库预处理在服务器上完成是最佳实践。在服务器上用完整的transformerspipeline将文档转换为向量然后将向量和对应的文本摘要一起打包下发到App。App只需要存储和检索向量。 对于查询侧用户输入如果查询量不大我选择在App内集成一个轻量级Tokenizer。我找到了一个C实现的SentencePiece库通过JNIAndroid和BridgeiOS封装成原生接口。虽然增加了少量体积但保证了与训练时的一致性搜索质量最高。4.2 输出池化从Token向量到句子向量模型输出是[BatchSize, SequenceLength, HiddenSize]的张量即每个token都有一个向量。我们需要将其压缩成一个句子向量。常见方法有CLS Token取第一个特殊token[CLS]的向量作为句子表示。EmbeddingGemma不一定有[CLS]需要确认。均值池化对所有token的向量取平均值。这是最通用和稳定的方法。加权平均池化根据attention mask忽略padding部分的token对有效token向量取平均。移动端高效实现示例Android/Kotlin均值池化private fun poolOutput(tokenVectors: ArrayFloatArray, attentionMask: IntArray): FloatArray { val hiddenSize tokenVectors[0].size val sentenceVector FloatArray(hiddenSize) var validTokenCount 0 for ((i, tokenVec) in tokenVectors.withIndex()) { // 根据attention mask判断是否为有效token1为有效 if (i attentionMask.size attentionMask[i] 1) { validTokenCount for (j in 0 until hiddenSize) { sentenceVector[j] tokenVec[j] } } } if (validTokenCount 0) { for (j in 0 until hiddenSize) { sentenceVector[j] / validTokenCount } } return sentenceVector }注意这个后处理步骤虽然简单但必须在移动端高效完成。避免在循环中创建大量临时对象。上述代码直接操作基本数组效率较高。对于iOS使用Accelerate.framework中的vDSP函数进行向量运算性能会更高。5. 核心技巧四向量检索与本地数据库选型当你有了一堆文档向量和一个查询向量后如何在毫秒级内找到最相似的几个这就是向量相似度搜索最近邻搜索。在移动端我们需要一个轻量级、高效的嵌入式向量数据库或检索库。5.1 轻量级向量检索方案对比方案原理优点缺点适用场景暴力扫描计算查询向量与所有文档向量的余弦相似度/点积排序。实现简单100%准确。时间复杂度O(N*D)N为文档数D为向量维度。文档上千后速度变慢。文档数量少 500简单原型。FAISS (Mobile)Facebook开源的向量相似度搜索库有iOS/Android编译版本。支持IVF、HNSW等索引。检索速度极快尤其在大规模数据集上精度可调。集成稍复杂需要编译或寻找预编译库索引需要预先构建占用额外空间。文档数量多数千到数万对检索速度要求高。SQLite with FTS/自定义将向量作为BLOB存入SQLite用自定义C扩展或近似计算进行检索。利用现有SQLite生态数据管理方便无需额外库。检索效率一般需要自己实现或集成近似算法大规模数据性能不佳。文档数量中等且已有SQLite用于存储其他元数据。专业嵌入式向量DB如Chroma、Lance的移动端版本或Weaviate的嵌入式模式。功能完整API友好可能支持自动持久化、过滤等。相对年轻社区和稳定性待考验可能带来较大的二进制体积。需要复杂过滤、多模态检索等高级功能。5.2 实战推荐FAISS Mobile SQLite 混合方案对于大多数移动端语义搜索场景我推荐使用FAISS构建向量索引用SQLite存储文本元数据的混合方案。离线构建索引在服务器端使用Python的FAISS库为所有文档向量构建一个索引例如IndexFlatIP用于点积或IndexFlatL2用于欧氏距离。将索引文件.index和对应的文档ID列表、向量数据文件一起打包。移动端集成FAISSAndroid使用faiss-android的AAR包或编译FAISS C库并通过JNI调用。iOS使用CocoaPods集成faiss-ios或通过Swift Package Manager引入FAISS的C源码进行编译。数据同步与加载App启动或更新时下载最新的索引文件和元数据包。将FAISS索引加载到内存中。将文档ID、文本标题、摘要等元数据存入SQLite数据库并通过文档ID与向量索引中的位置关联。检索流程// Kotlin伪代码 fun search(query: String): ListDocument { // 1. 将查询文本转换为向量 queryVector val queryVector embeddingModel.embed(query) // 2. 使用FAISS搜索最相似的K个向量返回它们的索引位置和相似度分数 val (indices, scores) faissIndex.search(queryVector, k10) // 3. 根据索引位置从SQLite中查询对应的文档元数据 val docIds indices.map { index - idList[index] } val documents sqliteDb.queryDocumentsByIds(docIds) // 4. 将分数与文档信息结合返回排序结果 return documents.zip(scores).sortedByDescending { it.second } }注意事项内存管理FAISS索引和向量数据会完全加载到内存。对于EmbeddingGemma-300M假设向量维度为7681万个文档就需要约 10000 * 768 * 4 bytes ≈ 30 MB。加上索引结构内存占用可能在50-100MB。务必在性能较差的设备上进行测试。索引更新如果文档库需要增量更新完整的重建索引在移动端开销很大。可以考虑设计一个机制小批量更新时在内存中维护一个小的增量索引如IndexFlat并与主索引合并查询定期从服务器拉取全量新索引。距离度量EmbeddingGemma产生的向量通常使用余弦相似度进行比较。在FAISS中如果向量是归一化的即模长为1那么余弦相似度等价于点积内积。因此在存入FAISS前务必对每个文档向量进行L2归一化查询向量也进行归一化然后使用IndexFlatIP点积索引进行搜索这样效率最高。6. 核心技巧五性能优化与内存管理实战在移动设备上运行AI模型性能速度、功耗和内存是必须跨过的坎。以下是针对这个项目的具体优化点。6.1 推理性能优化使用硬件加速委托AndroidTFLite支持多种委托。对于支持GPU的设备可以启用GpuDelegate。对于有专用NPU的设备如一些高端华为、三星手机可以使用NnApiDelegate它会在支持时调用NPU。在初始化Interpreter时配置val options Interpreter.Options() // 尝试添加NNAPI委托它会自动回退到CPU try { val nnApiDelegate NnApiDelegate() options.addDelegate(nnApiDelegate) } catch (e: Exception) { Log.w(TFLite, NNAPI delegate not available, using CPU) } // 或者添加GPU委托 // val gpuDelegate GpuDelegate() // options.addDelegate(gpuDelegate)iOSCore ML会自动尝试将模型运行在Neural Engine上。确保在转换Core ML模型时设置了compute_units .all或.cpuAndGPU。对于ONNX Runtime可以尝试CoreML Execution Provider。批处理虽然移动端搜索通常是单条查询但如果你需要在后台批量预处理一些用户数据使用批处理batch_size 1能显著提升吞吐量。确保你的模型支持动态批处理维度。输入输出缓冲区复用避免在每次推理时都分配新的输入输出数组。在初始化时分配好固定大小的ByteBuffer或数组每次推理时复用它们。6.2 内存优化与防止OOM模型分片与按需加载如果量化后的模型仍然很大比如超过200MB可以考虑将模型文件分片存储。在App启动时只加载必要的部分如元数据在首次使用推理功能时再动态加载模型主体。及时释放资源TFLite的Interpreter、ONNX Runtime的Session、FAISS的Index都是重量级对象。确保在App进入后台或相关功能页面销毁时及时调用它们的close()或释放方法。将向量检索等耗时操作放在后台线程但要注意后台线程的内存峰值。监控与降级// 在初始化前检查可用内存 val am context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager val memoryInfo ActivityManager.MemoryInfo() am.getMemoryInfo(memoryInfo) if (memoryInfo.lowMemory || memoryInfo.availMem MIN_REQUIRED_MEMORY) { // 启用低精度模式或使用更小的索引或提示用户 loadLightweightModel() } else { loadFullModel() }6.3 功耗与发热控制避免频繁唤醒与推理对用户输入进行防抖处理不要在用户每个字符输入时都进行语义搜索。可以设置一个合理的延迟如500ms。控制推理频率在列表滚动等连续交互过程中暂停或降低搜索频率。使用性能模式APIAndroid在初始化TFLite Interpreter时可以尝试设置不同的ExecutionPreference如LOW_LATENCY或LOW_POWER但实际效果因设备和驱动而异。iOS对于Core ML可以使用MLModelConfiguration的computeUnits属性来选择.cpuOnly以降低功耗但速度会慢。7. 常见问题与排查技巧实录在实际集成过程中你一定会遇到各种奇怪的问题。这里记录了几个最典型的案例和解决方法。问题1模型转换成功但在移动端加载时崩溃或输出异常。可能原因A输入输出维度或类型不匹配。排查在Python端用ONNX Runtime或TFLite加载转换后的模型用相同的输入数据运行对比输出是否与原始PyTorch模型一致。这是验证转换正确性的黄金标准。技巧编写一个简单的Python脚本将测试用例输入文本、期望的向量保存成文件。在移动端集成后用同样的输入运行将输出向量与Python端的输出进行余弦相似度对比如果大于0.99基本可以认为转换正确。可能原因B动态形状支持问题。排查确认转换ONNX时正确设置了dynamic_axes。在移动端尝试用不同长度的输入进行测试。技巧对于TFLite有些操作对动态形状支持不好。如果遇到问题可以尝试在转换时固定一个常用的序列长度如128或256在预处理时将所有文本截断或填充到这个固定长度。问题2检索结果不准相似度分数都很低或没有区分度。可能原因A向量未归一化。解决如前所述确保存入检索库的文档向量和查询向量都进行了L2归一化。这是影响余弦相似度计算准确性的最常见原因。验证计算几个已知相似的文档对之间的余弦相似度看是否接近1计算不相关的文档对看是否接近0。可能原因B池化策略不一致。解决检查移动端后处理池化的逻辑是否与你在服务器端预处理文档向量时使用的逻辑完全一致都是均值池化且都考虑了attention mask。可能原因C量化导致精度损失。解决换用动态范围量化或FP16量化重新测试。全整数量化有时对嵌入模型影响较大。问题3在部分低端Android设备上运行缓慢甚至ANR。可能原因A未使用多线程。解决在创建TFLite Interpreter时务必设置options.setNumThreads(4)。这能让推理充分利用多核CPU。可能原因BUI线程执行耗时操作。解决确保所有的模型加载、推理、向量检索操作都在后台线程如Kotlin的Coroutine的IO调度器、Swift的DispatchQueue.global(qos: .userInitiated)中执行。可能原因C内存抖动。排查使用Android Studio Profiler或Xcode Instruments监控内存分配。检查是否在循环中频繁创建大量小对象如FloatArray。解决复用缓冲区使用原生数组而非高级集合。问题4iOS Core ML模型转换失败报错“Unsupported Opset”或“Unsupported operator”。可能原因ONNX模型中包含Core ML不支持的算子。解决使用coremltools的更高版本。在转换时尝试指定convert_to参数为mlprogram新的模型格式它支持的算子集更广。使用coremltools的unify_dtype和compute_precision参数进行调试。如果以上都不行考虑使用ONNX Runtime作为后备方案或者寻找是否有社区提供的已转换好的Core ML模型。问题5FAISS索引文件在移动端加载慢、占用内存大。可能原因使用了过于复杂的索引类型如IVF4096,Flat。解决对于移动端IndexFlatIP归一化后的点积或IndexFlatL2是最简单、最可靠的选择。它们虽然搜索是O(N)但内存占用小加载快且100%准确。对于数万级别的文档在现代手机CPU上暴力搜索10个最近邻通常在10毫秒以内完全可以接受。只有在文档数超过10万时才需要考虑IndexIVFFlat这类基于聚类的索引但它会引入额外的训练和近似误差。