CocoIndex 语义化图片搜索实战:用 CLIP 与 Qdrant 构建“按含义检索“的实时照片索引
CocoIndex 语义化图片搜索实战用 CLIP 与 Qdrant 构建按含义检索的实时照片索引【免费下载链接】cocoindexIncremental engine for long horizon agents Star if you like it!项目地址: https://gitcode.com/GitHub_Trending/co/cocoindex本文以 image_search 示例 为骨架讲解如何用 CocoIndex 在纯异步 Python 中构建一个按语义而非标签搜索本地照片的应用CLIP 将图片与文本嵌入同一向量空间向量落入 Qdrant索引以 live 模式随 FastAPI 服务常驻运行。读完本文你将掌握 CocoIndex 的coco.fn(memoTrue)增量处理、mount_collection_target托管 Qdrant 集合、update(liveTrue)实时索引等核心能力并能独立复现一个开箱即用的图片语义检索服务。核心思想一个模型、两个编码器、一个共享向量空间传统图片搜索依赖文件名与手工标签而 CLIP 模型 的思路完全不同它同时拥有图像编码器与文本编码器且两者输出落在同一个向量空间。索引阶段用图像编码器把每张图片嵌入为向量查询阶段用文本编码器把一句话嵌入为向量——long neck 与长颈鹿照片在空间中天然相邻余弦相似度最高全程不需要任何标注或 caption。在 CocoIndex 中这种转换被声明为原生 Python 代码target_state transformation(source_state)。底层由 Rust 引擎承担增量处理、变更跟踪与 Qdrant 集合托管在live 模式下往文件夹里丢一张新照片索引在一秒内即可更新完毕无需任何重建步骤。整体架构与数据流索引链路极短——图片没有文本需要分块每张图只需一次嵌入扫描本地图片文件夹live 监听匹配.jpg/.jpeg/.png嵌入用 CLIP 图像编码器为每张图片生成向量存储以路径的稳定uuid5作为 Qdrant point ID文件名写入 payload。查询侧则用同一个 CLIP 模型的文本编码器将查询语句嵌入对图片向量做余弦检索。每张图片作为独立的 processing component 参与流水线因此删除一张照片其 Qdrant point 也会被自动清除。示例自带 4 张样例图片cat1.jpeg、dog1.jpeg、elephant1.jpg、giraffe.jpg查询 long neck 时长颈鹿稳居第一其余动物按 CLIP 相似度依次排列——它们从未被任何单词标记过。流水线源码拆解pipeline.py示例的索引逻辑集中在 pipeline.py它定义了一个 CocoIndexapp并在模块顶部声明了几个关键常量QDRANT_COLLECTION ImageSearch CLIP_MODEL_NAME openai/clip-vit-large-patch14 TOP_K 5 def qdrant_url() - str: return os.getenv(QDRANT_URL, http://localhost:6334/)QDRANT_COLLECTION目标集合名CLIP_MODEL_NAME使用的预训练模型对应 CLIP ViT-Large/Patch14投影维度 768qdrant_url()读取环境变量QDRANT_URL默认指向本地 Qdrant 容器的 gRPC 端口6334与 create_client 中的prefer_grpcTrue配合。模型加载与嵌入模型与处理器通过functools.cache只加载一次functools.cache def get_clip_model() - tuple[CLIPModel, CLIPProcessor]: model CLIPModel.from_pretrained(CLIP_MODEL_NAME) processor CLIPProcessor.from_pretrained(CLIP_MODEL_NAME) return model, processor图片嵌入与文本嵌入分别走图像/文本编码器_projected_features兼容新旧两版 transformers 的输出结构新版取pooler_output旧版直接返回投影特征张量def embed_query(text: str) - list[float]: # 查询侧——同一模型文本编码器 model, processor get_clip_model() inputs processor(text[text], return_tensorspt, paddingTrue) with torch.no_grad(): out model.get_text_features(**inputs) return _projected_features(out)[0].tolist() def embed_image_bytes(img_bytes: bytes) - list[float]: # 索引侧——图像编码器 model, processor get_clip_model() image Image.open(io.BytesIO(img_bytes)).convert(RGB) inputs processor(imagesimage, return_tensorspt) with torch.no_grad(): out model.get_image_features(**inputs) return _projected_features(out)[0].tolist()核心处理函数coco.fn(memoTrue)每张图片由一个独立的处理组件负责memoTrue让未变化的图片永不重复嵌入coco.fn(memoTrue) # 未变化的图片不会重新嵌入 async def process_file(file: FileLike, target: qdrant.CollectionTarget) - None: content await file.read() embedding embed_image_bytes(content) point qdrant.PointStruct( id_image_id(file.file_path.path), # 路径的 uuid5 —— 稳定 vectorembedding, payload{filename: str(file.file_path.path)}, ) target.declare_point(point)其中_image_id以uuid.uuid5将路径映射为稳定的 point IDpipeline.pydef _image_id(path: pathlib.PurePath) - str: return str(uuid.uuid5(uuid.NAMESPACE_URL, str(path)))这与 Qdrant 连接器对 point ID 的校验规则一脉相承Qdrant 只接受无符号 64 位整数或 UUID。连接器在 declare_point 时会调用_validate_point_id提前校验若传入任意字符串会立刻抛出带明确指引的ValueErrorpython/cocoindex/connectors/qdrant/_target.py#L727-L764其提示正是建议用uuid.uuid5从任意字符串键派生稳定 ID——示例正是这一最佳实践的落地。app_main声明源、目标与转换coco.fn async def app_main(sourcedir: pathlib.Path) - None: model, _ get_clip_model() dim: int model.config.projection_dim # type: ignore[assignment] target_collection await qdrant.mount_collection_target( QDRANT_DB, collection_nameQDRANT_COLLECTION, schemaawait qdrant.CollectionSchema.create( vectorsqdrant.QdrantVectorDef( schemaVectorSchema(dtypenp.dtype(np.float32), sizedim), distancecosine, ) ), ) files localfs.walk_dir( sourcedir, recursiveTrue, path_matcherPatternFilePathMatcher( included_patterns[**/*.jpg, **/*.jpeg, **/*.png] ), liveTrue, # 源支持 live 监听api.py 以 liveTrue 运行 app ) await coco.mount_each(process_file, files.items(), target_collection) app coco.App( coco.AppConfig(nameImageSearchQdrantV1), app_main, sourcedirpathlib.Path(./img), )几个值得注意的细节向量维度自动派生dim直接取自model.config.projection_dimCLIP ViT-Large/Patch14 为 768因此更换 CLIP 变体时集合 schema 自动跟随无需手改mount_collection_target托管集合集合的创建/更新/删除由系统全权管理见下文目标端剖析向量大小与距离度量在此声明为cosinelocalfs.walk_dir声明式扫描PatternFilePathMatcher用 glob 模式过滤**/*.jpg、**/*.jpeg、**/*.pngliveTrue开启文件监听coco.mount_each挂载每张图每个文件项都独立成为处理组件天然支持删除即清理。coco.lifespan则在应用生命周期内提供 Qdrant 客户端到上下文中coco.lifespan async def coco_lifespan(builder: coco.EnvironmentBuilder) - AsyncIterator[None]: client qdrant.create_client(qdrant_url(), prefer_grpcTrue) builder.provide(QDRANT_DB, client) builder.provide(QDRANT_CLIENT, client) yieldQdrant 目标端深度剖析示例使用到的mount_collection_target、CollectionSchema、QdrantVectorDef、CollectionTarget.declare_point均来自 python/cocoindex/connectors/qdrant/_target.py它实现了集合级 point 级两级目标状态系统集合级负责在 Qdrant 中创建/删除集合。_CollectionHandler.reconcile通过 statediff 机制对比期望 schema 与线上状态产出insert / upsert / replace / delete动作当向量定义发生变化如更换模型导致维度不同时判定为replace先删集合再重建并以child_invalidationdestructive通知子层python/cocoindex/connectors/qdrant/_target.py#L457-L505point 级_PointHandler对每个 point 做指纹比对reconcile用fingerprint_object((desired_state.vector, desired_state.payload))判断是否变化只把真正变化的 point 批量 upsert/deletepython/cocoindex/connectors/qdrant/_target.py#L255-L315。CollectionSchema.create是异步工厂方法支持单个无名稠密向量QdrantVectorDef或命名向量字典稠密向量与稀疏向量QdrantSparseVectorDef始终命名共享同一命名空间。QdrantVectorDef的关键参数为参数可选值默认说明schemaVectorSchemaProvider/MultiVectorSchemaProvider/ContextKey无向量 schema示例中为VectorSchema(dtypenp.float32, sizedim)distancecosine/dot/euclidcosine距离度量由_distance_from_spec映射到 Qdrant 枚举multivector_comparatormax_simmax_sim仅多向量 schema 生效而CollectionTarget.declare_point(point)会校验 point ID 并声明目标状态python/cocoindex/connectors/qdrant/_target.py#L548-L563point 的批量写入由TargetActionSink.from_async_fn驱动经asyncio.to_thread落到client.upsert/client.delete避免阻塞事件循环。FastAPI 服务与 live 模式api.pyapi.py 是一个 FastAPI 应用其 lifespan 在服务启动阶段完成两件事启动 live 模式的索引更新并阻塞启动直到首轮全量扫描 READY此后索引在后台常驻、持续监听img/目录同时对外提供/search——没有独立的构建索引步骤asynccontextmanager async def lifespan(app: FastAPI) - AsyncIterator[None]: global _client async with coco.runtime(): _client qdrant.create_client(pipeline.qdrant_url(), prefer_grpcTrue) # 启动 live 更新阻塞启动直到首轮扫描 READY保证集合可查询随后转入后台运行 update_handle pipeline.app.update(liveTrue) async for snap in update_handle.watch(): if snap.status is coco.UpdateStatus.READY: break update_task asyncio.create_task(update_handle.result()) try: yield finally: update_task.cancel() with contextlib.suppress(asyncio.CancelledError): await update_task _client None要点pipeline.app.update(liveTrue)创建一次 live 更新句柄update_handle.watch()以异步流形式逐条产出更新快照直到snap.status变为coco.UpdateStatus.READY即初始扫描完成才放行服务启动asyncio.create_task(update_handle.result())把后续增量处理放入后台任务关闭时cancel()后台任务并清理客户端引用。搜索端点把文本查询嵌入后交给 Qdrant 检索返回文件名与相似度分数app.get(/search) async def search( q: str Query(..., descriptionSearch query), limit: int Query(5, descriptionNumber of results), ) - dict[str, Any]: query_embedding pipeline.embed_query(q) if _client is None: raise RuntimeError(Qdrant client is not initialized.) results pipeline._qdrant_search( _client, pipeline.QDRANT_COLLECTION, query_embedding, limit, ) return { results: [ {filename: (r.payload or {}).get(filename), score: r.score} for r in results ] }pipeline.py 的_qdrant_search同时兼容qdrant-client多个版本的检索 APIsearch/query_points/search_points并以with_payloadTrue带回 payload。此外服务通过app.mount(/img, StaticFiles(directoryimg))直接托管样例图片目录并配置了全开 CORS 中间件供前端跨端口调用。前端React Vite前端位于 examples/image_search/frontend是一个极简 React 应用App.jsx 以http://${window.location.hostname}:8000/search为 API 地址表单提交后fetch查询接口将返回的results渲染为图片卡片与Score分数图片地址由服务端/img静态目录提供即结果中的filename字段vite.config.js 配置开发服务器监听5173端口并允许局域网访问依赖仅react/react-dom开发依赖vitevitejs/plugin-react见 package.json。运行步骤依赖说明需要Qdrant向量存储与 CLIP 模型依赖torch、transformers、pillow均由pip install -e .一并拉取。1. 启动 Qdrantdocker run -d -p 6333:6333 -p 6334:6334 qdrant/qdrant2. 配置并安装依赖pip install -e .服务端通过环境变量QDRANT_URL指定 Qdrant 地址默认http://localhost:6334/即上方容器的 gRPC 端口。在examples/image_search目录下执行安装pyproject.toml要求 Python3.11核心依赖包括cocoindex[qdrant]1.0.7、fastapi、torch、transformers、pillow、qdrant-client、uvicorn与python-dotenvpyproject.toml。3. 以服务方式运行—— 示例自带img/文件夹一只猫、一只狗、一头大象、一只长颈鹿。服务在后台以 live 模式运行索引并阻塞启动直到首轮扫描完成因此没有单独的建索引命令python -m uvicorn api:app --reload --host 0.0.0.0 --port 80004. 启动前端cd frontend npm install npm run dev # http://localhost:5173在搜索框输入long neck长颈鹿排名第一其余动物按 CLIP 相似度依次排列——没有任何一张图被标注过任何单词匹配完全基于语义。小结这个示例展示了什么一个模型、两个编码器CLIP 在索引时嵌入图片、查询时嵌入文本两者共享 768 维向量空间匹配靠含义而非元数据Live 模式开箱即用流水线在 API 服务内部以 live 模式运行往img/丢一张照片一秒内即可被检索无需重建增量与自清理coco.fn(memoTrue)跳过未变化的图片每张照片是独立处理组件删除照片自动移除对应 Qdrant point托管的 Qdrant 目标端mount_collection_target创建并对齐集合向量维度直接取自model.config.projection_dim更换 CLIP 变体无需手工调整纯 Python、你的技术栈FastAPI React Qdrant无 DSL索引逻辑只是寥寥数个普通异步函数。若要继续深入可对比阅读仓库中的 text_embedding同一思想在纯文本上的应用、image_search_colpali更换为 ColPali 多向量方案的变体以及 Rust 版 image_search观察同一流水线在声明式 API 下的多语言落地。【免费下载链接】cocoindexIncremental engine for long horizon agents Star if you like it!项目地址: https://gitcode.com/GitHub_Trending/co/cocoindex创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考