Showing
6 changed files
with
459 additions
and
18 deletions
| 1 | from typing import List, Optional, Literal, Union, Dict, Any, Annotated | 1 | from typing import List, Optional, Literal, Union, Dict, Any, Annotated |
| 2 | -from pydantic import BaseModel, Field | ||
| 3 | -from fastapi import APIRouter | 2 | + |
| 3 | +from fastapi import APIRouter, Depends | ||
| 4 | +import numpy | ||
| 5 | +from pydantic import BaseModel, Field, TypeAdapter | ||
| 6 | + | ||
| 4 | from api.resp_bean import RespBean, success | 7 | from api.resp_bean import RespBean, success |
| 5 | 8 | ||
| 6 | router = APIRouter() | 9 | router = APIRouter() |
| @@ -34,7 +37,7 @@ class PutDataRequest(BaseModel): | @@ -34,7 +37,7 @@ class PutDataRequest(BaseModel): | ||
| 34 | embedding: List[float] = Field(..., description="向量") | 37 | embedding: List[float] = Field(..., description="向量") |
| 35 | embedding_version: str = Field(..., description="向量版本 小写数字下划线组成") | 38 | embedding_version: str = Field(..., description="向量版本 小写数字下划线组成") |
| 36 | kwargs: Optional[Dict[str, Any]] = Field(None, description="扩展字段,根据type不同字段不同") | 39 | kwargs: Optional[Dict[str, Any]] = Field(None, description="扩展字段,根据type不同字段不同") |
| 37 | - | 40 | + |
| 38 | def get_typed_kwargs(self, type: str) -> Optional[KwargsType]: | 41 | def get_typed_kwargs(self, type: str) -> Optional[KwargsType]: |
| 39 | """根据type自动转换kwargs为具体类型""" | 42 | """根据type自动转换kwargs为具体类型""" |
| 40 | if self.kwargs is None: | 43 | if self.kwargs is None: |
| @@ -52,6 +55,7 @@ class PutDataRequest(BaseModel): | @@ -52,6 +55,7 @@ class PutDataRequest(BaseModel): | ||
| 52 | class DelByIdRequest(BaseModel): | 55 | class DelByIdRequest(BaseModel): |
| 53 | """删除数据请求体""" | 56 | """删除数据请求体""" |
| 54 | ids: List[str] = Field(..., description="id 集合") | 57 | ids: List[str] = Field(..., description="id 集合") |
| 58 | + embedding_version: str = Field(..., description="向量版本 小写数字下划线组成") | ||
| 55 | 59 | ||
| 56 | 60 | ||
| 57 | class FilterItem(BaseModel): | 61 | class FilterItem(BaseModel): |
| @@ -85,35 +89,57 @@ class SearchResponseData(BaseModel): | @@ -85,35 +89,57 @@ class SearchResponseData(BaseModel): | ||
| 85 | 89 | ||
| 86 | 90 | ||
| 87 | # ============== 接口定义 ============== | 91 | # ============== 接口定义 ============== |
| 92 | +from service.data_service import DataService | ||
| 93 | +from service import get_data_service | ||
| 94 | + | ||
| 88 | 95 | ||
| 89 | @router.put("/{type}/put", response_model=RespBean, tags=["数据服务"], summary="写入数据") | 96 | @router.put("/{type}/put", response_model=RespBean, tags=["数据服务"], summary="写入数据") |
| 90 | @router.post("/{type}/put", response_model=RespBean, tags=["数据服务"], summary="写入数据") | 97 | @router.post("/{type}/put", response_model=RespBean, tags=["数据服务"], summary="写入数据") |
| 91 | -async def put_data(type: Literal["face", "sport_shot"], request: PutDataRequest): | 98 | +async def put_data( |
| 99 | + type: Literal["face", "sport_shot"], | ||
| 100 | + request: PutDataRequest, | ||
| 101 | + data_service: DataService = Depends(get_data_service) | ||
| 102 | +): | ||
| 92 | """ | 103 | """ |
| 93 | 写入数据 | 104 | 写入数据 |
| 94 | """ | 105 | """ |
| 106 | + request.embedding = numpy.array(request.embedding) | ||
| 95 | 107 | ||
| 96 | - pass | ||
| 97 | - return success() | 108 | + embedding_version = request.embedding_version |
| 109 | + tb_name = f'{type}_{embedding_version}' | ||
| 110 | + upserted_id = await data_service.upsert(tb_name, request.id, request.embedding, | ||
| 111 | + request.get_typed_kwargs(type).model_dump()) | ||
| 112 | + | ||
| 113 | + return success(data={'id': upserted_id}) | ||
| 98 | 114 | ||
| 99 | 115 | ||
| 100 | @router.delete("/{type}/del_by_id", response_model=RespBean, tags=["数据服务"], summary="删除数据") | 116 | @router.delete("/{type}/del_by_id", response_model=RespBean, tags=["数据服务"], summary="删除数据") |
| 101 | @router.post("/{type}/del_by_id", response_model=RespBean, tags=["数据服务"], summary="删除数据") | 117 | @router.post("/{type}/del_by_id", response_model=RespBean, tags=["数据服务"], summary="删除数据") |
| 102 | -async def del_by_id(type: Literal["face", "sport_shot"], request: DelByIdRequest): | 118 | +async def del_by_id( |
| 119 | + type: Literal["face", "sport_shot"], | ||
| 120 | + request: DelByIdRequest, | ||
| 121 | + data_service: DataService = Depends(get_data_service) | ||
| 122 | +): | ||
| 103 | """ | 123 | """ |
| 104 | 删除数据 | 124 | 删除数据 |
| 105 | 125 | ||
| 106 | - **type**: 数据类型 (face-人脸, sport_shot-体育镜头) | 126 | - **type**: 数据类型 (face-人脸, sport_shot-体育镜头) |
| 107 | - **ids**: id集合 | 127 | - **ids**: id集合 |
| 108 | """ | 128 | """ |
| 109 | - # TODO: 实现数据删除逻辑 | ||
| 110 | - pass | ||
| 111 | - return success() | 129 | + embedding_version = request.embedding_version |
| 130 | + tb_name = f'{type}_{embedding_version}' | ||
| 131 | + await data_service.delete_by_pks(tb_name, request.ids) | ||
| 112 | 132 | ||
| 133 | + return success() | ||
| 113 | 134 | ||
| 114 | 135 | ||
| 115 | @router.post("/{type}/search", response_model=RespBean[SearchResponseData], tags=["数据服务"], summary="检索数据") | 136 | @router.post("/{type}/search", response_model=RespBean[SearchResponseData], tags=["数据服务"], summary="检索数据") |
| 116 | -async def search_data(type: Literal["face", "sport_shot"], request: SearchRequest): | 137 | +async def search_data( |
| 138 | + type: Literal["face", "sport_shot"], | ||
| 139 | + request: SearchRequest, | ||
| 140 | + data_service: DataService = Depends(get_data_service), | ||
| 141 | + adapter=TypeAdapter(List[SearchResultItem]) | ||
| 142 | +): | ||
| 117 | """ | 143 | """ |
| 118 | 检索数据 | 144 | 检索数据 |
| 119 | 145 | ||
| @@ -123,6 +149,15 @@ async def search_data(type: Literal["face", "sport_shot"], request: SearchReques | @@ -123,6 +149,15 @@ async def search_data(type: Literal["face", "sport_shot"], request: SearchReques | ||
| 123 | - **topk**: 返回top k条结果 | 149 | - **topk**: 返回top k条结果 |
| 124 | - **filters**: 过滤条件列表 | 150 | - **filters**: 过滤条件列表 |
| 125 | """ | 151 | """ |
| 126 | - # TODO: 实现数据检索逻辑 | ||
| 127 | - pass | ||
| 128 | - return success() | 152 | + # |
| 153 | + embedding_version = request.embedding_version | ||
| 154 | + tb_name = f'{type}_{embedding_version}' | ||
| 155 | + | ||
| 156 | + embedding = request.embedding | ||
| 157 | + filters = request.filters | ||
| 158 | + from_, size = 0, request.topk | ||
| 159 | + | ||
| 160 | + result_data = await data_service.search(tb_name, embedding, filters, from_, size) | ||
| 161 | + search_results: List[SearchResultItem] = adapter.validate_python(result_data) | ||
| 162 | + | ||
| 163 | + return success(data=SearchResponseData(type=type, data_list=search_results)) |
| 1 | +from functools import lru_cache | ||
| 2 | + | ||
| 3 | +from elasticsearch import AsyncElasticsearch | ||
| 4 | + | ||
| 1 | from config import settings | 5 | from config import settings |
| 6 | +from .data_dao import DataDao | ||
| 2 | 7 | ||
| 3 | es_client = None | 8 | es_client = None |
| 4 | pg_client = None | 9 | pg_client = None |
| 5 | 10 | ||
| 6 | 11 | ||
| 7 | async def connect(): | 12 | async def connect(): |
| 8 | - if settings.db_established: | ||
| 9 | - from .es_client import get_es_client | 13 | + if settings.db_es_enable: |
| 10 | global es_client | 14 | global es_client |
| 11 | - es_client = get_es_client() | ||
| 12 | - await es_client.connect() | 15 | + es_client = AsyncElasticsearch([settings.db_es_url]) |
| 16 | + await es_client.ping() | ||
| 13 | if settings.db_postgres_enable: | 17 | if settings.db_postgres_enable: |
| 14 | from .postgres_client import get_pg_client | 18 | from .postgres_client import get_pg_client |
| 15 | global pg_client | 19 | global pg_client |
| @@ -24,3 +28,8 @@ async def disconnect(): | @@ -24,3 +28,8 @@ async def disconnect(): | ||
| 24 | es_client = None | 28 | es_client = None |
| 25 | if pg_client is not None: | 29 | if pg_client is not None: |
| 26 | await pg_client.disconnect() | 30 | await pg_client.disconnect() |
| 31 | + | ||
| 32 | + | ||
| 33 | +@lru_cache() | ||
| 34 | +def get_data_dao(): | ||
| 35 | + return DataDao(es_client) |
db/data_dao.py
0 → 100644
| 1 | +import json | ||
| 2 | +import logging | ||
| 3 | +import os | ||
| 4 | +import threading | ||
| 5 | +import typing | ||
| 6 | + | ||
| 7 | +from elasticsearch import AsyncElasticsearch | ||
| 8 | +import numpy | ||
| 9 | + | ||
| 10 | +logger = logging.getLogger(__name__) | ||
| 11 | + | ||
| 12 | + | ||
| 13 | +class DataDao: | ||
| 14 | + | ||
| 15 | + def __init__(self, es_client: AsyncElasticsearch): | ||
| 16 | + self.es_client = es_client | ||
| 17 | + | ||
| 18 | + self._lock = threading.RLock() | ||
| 19 | + | ||
| 20 | + async def search(self, tb_name: str, | ||
| 21 | + embedding=typing.Optional[typing.Union[numpy.ndarray, list]], | ||
| 22 | + filters=None, | ||
| 23 | + from_=typing.Optional[int], | ||
| 24 | + size=typing.Optional[int], | ||
| 25 | + ): | ||
| 26 | + search_body = dict() | ||
| 27 | + | ||
| 28 | + from_ = from_ if isinstance(from_, int) and from_ >= 0 else 0 | ||
| 29 | + size = size if isinstance(size, int) and size > 0 else 10 | ||
| 30 | + search_body.update({ | ||
| 31 | + 'from': from_, | ||
| 32 | + 'size': size, | ||
| 33 | + }) | ||
| 34 | + | ||
| 35 | + return_embedding = os.getenv('RETURN_EMBEDDING', 'false').lower().strip() | ||
| 36 | + if return_embedding != 'true': | ||
| 37 | + search_body.update({ | ||
| 38 | + "_source": { | ||
| 39 | + "excludes": ["embedding"] | ||
| 40 | + } | ||
| 41 | + }) | ||
| 42 | + | ||
| 43 | + condition_query = { | ||
| 44 | + "match_all": {} | ||
| 45 | + } | ||
| 46 | + if isinstance(filters, typing.Collection) and len(filters) > 0: | ||
| 47 | + tb_mapping = await self._get_mapping(tb_name) | ||
| 48 | + exist_properties = tb_mapping.get('mappings', {}).get('properties', {}) | ||
| 49 | + | ||
| 50 | + clauses = [] | ||
| 51 | + for filter in filters: | ||
| 52 | + k = filter.name | ||
| 53 | + v = filter.value | ||
| 54 | + opt = filter.opt | ||
| 55 | + if k not in exist_properties: | ||
| 56 | + continue | ||
| 57 | + clause = get_filter_clause(k, v, opt, exist_properties[k]) | ||
| 58 | + if clause: | ||
| 59 | + clauses.append(clause) | ||
| 60 | + condition_query = { | ||
| 61 | + "bool": { | ||
| 62 | + "filter": clauses | ||
| 63 | + } | ||
| 64 | + } | ||
| 65 | + if embedding is not None: | ||
| 66 | + use_knn = os.getenv('USE_KNN', 'false').lower().strip() | ||
| 67 | + if use_knn != 'true': | ||
| 68 | + query_body = { | ||
| 69 | + "script_score": { | ||
| 70 | + "query": condition_query, | ||
| 71 | + "script": { | ||
| 72 | + "source": """ | ||
| 73 | + // 1. 计算原始分数 (范围 [0, 2]) | ||
| 74 | + double rawScore = cosineSimilarity(params.query_vector, 'embedding') + 1.0; | ||
| 75 | + | ||
| 76 | + // 2. 归一化到 [0, 1] | ||
| 77 | + double normalizedScore = rawScore / 2.0; | ||
| 78 | + | ||
| 79 | + // 3. 强制截断:确保最小值为 0.0,最大值为 1.0 | ||
| 80 | + // Math.max 防止出现负数,Math.min 防止出现 > 1.0 的数 | ||
| 81 | + double clampedScore = Math.max(0.0, Math.min(1.0, normalizedScore)); | ||
| 82 | + | ||
| 83 | + // 4. 四舍五入到 4 位小数 | ||
| 84 | + // 原理:乘以 10000 -> 四舍五入取整 -> 除以 10000 | ||
| 85 | + // clampedScore = Math.round(clampedScore * 10000.0) / 10000.0; | ||
| 86 | + return clampedScore; | ||
| 87 | + """, | ||
| 88 | + "params": { | ||
| 89 | + "query_vector": embedding.tolist() if isinstance(embedding, numpy.ndarray) else list(embedding) | ||
| 90 | + } | ||
| 91 | + } | ||
| 92 | + } | ||
| 93 | + } | ||
| 94 | + search_body.update({ | ||
| 95 | + "query": query_body | ||
| 96 | + }) | ||
| 97 | + else: | ||
| 98 | + search_body.update({ | ||
| 99 | + "knn": { | ||
| 100 | + "field": "embedding", | ||
| 101 | + "query_vector": embedding.tolist() if isinstance(embedding, numpy.ndarray) else list(embedding), | ||
| 102 | + "k": from_ + size, | ||
| 103 | + "num_candidates": (from_ + size) * 20, | ||
| 104 | + "filter": condition_query, | ||
| 105 | + } | ||
| 106 | + }) | ||
| 107 | + else: | ||
| 108 | + query_body = condition_query | ||
| 109 | + search_body.update({ | ||
| 110 | + "query": query_body | ||
| 111 | + }) | ||
| 112 | + | ||
| 113 | + logger.info(f'index {tb_name} search body: {json.dumps(search_body, ensure_ascii=False, indent=4)}') | ||
| 114 | + resp = await self.es_client.search(index=tb_name, body=search_body) | ||
| 115 | + logger.info(f'index {tb_name} search response: {resp}') | ||
| 116 | + | ||
| 117 | + body = resp.body | ||
| 118 | + hits = body.get('hits', {}).get('hits', []) | ||
| 119 | + result_data = [ | ||
| 120 | + { | ||
| 121 | + 'id': hit['_id'], | ||
| 122 | + 'score': round(hit['_score'], 4), | ||
| 123 | + 'kwargs': hit['_source'], | ||
| 124 | + } | ||
| 125 | + for hit in hits | ||
| 126 | + ] | ||
| 127 | + return result_data | ||
| 128 | + | ||
| 129 | + async def delete_by_pks(self, tb_name: str, pks: typing.List[str]): | ||
| 130 | + query_body = { | ||
| 131 | + "query": { | ||
| 132 | + "terms": { | ||
| 133 | + "_id": pks | ||
| 134 | + } | ||
| 135 | + } | ||
| 136 | + } | ||
| 137 | + resp = await self.es_client.delete_by_query(index=tb_name, body=query_body) | ||
| 138 | + logger.info(f'index {tb_name} delete_by_query response: {resp}') | ||
| 139 | + | ||
| 140 | + body = resp.body | ||
| 141 | + # return body.get('deleted') | ||
| 142 | + | ||
| 143 | + async def _get_mapping(self, tb_name: str): | ||
| 144 | + resp = await self.es_client.indices.get_mapping(index=tb_name) | ||
| 145 | + logger.info(f'index {tb_name} get_mapping response: {resp}') | ||
| 146 | + return resp.body.get(tb_name, {}) | ||
| 147 | + | ||
| 148 | + async def _tb_exist(self, tb_name, mapping=None): | ||
| 149 | + resp = await self.es_client.indices.exists(index=tb_name) | ||
| 150 | + result = resp.body | ||
| 151 | + if result and mapping is not None: | ||
| 152 | + tb_mapping = await self._get_mapping(tb_name) | ||
| 153 | + exist_properties = tb_mapping.get('mappings', {}).get('properties', {}) | ||
| 154 | + | ||
| 155 | + expect_properties = mapping.get('mappings', {}).get('properties', {}) | ||
| 156 | + if exist_properties and expect_properties: | ||
| 157 | + shared_keys = set(exist_properties.keys()).intersection(set(expect_properties.keys())) | ||
| 158 | + for k in shared_keys or []: | ||
| 159 | + if exist_properties[k]['type'] != expect_properties[k]['type']: | ||
| 160 | + raise Exception( | ||
| 161 | + f'index {tb_name} `{k}` type not match, expect: {expect_properties[k]["type"]}, actual: {exist_properties[k]["type"]}') | ||
| 162 | + else: | ||
| 163 | + if exist_properties[k]['type'] == 'dense_vector': | ||
| 164 | + if exist_properties[k].get('dims') != expect_properties[k].get('dims'): | ||
| 165 | + raise Exception( | ||
| 166 | + f'index {tb_name} dims not match, expect: {expect_properties[k].get("dims")}, actual: {exist_properties[k].get("dims")}') | ||
| 167 | + return result | ||
| 168 | + | ||
| 169 | + async def _create_index(self, tb_name, mapping=typing.Optional[dict]) -> bool: | ||
| 170 | + result = await self._tb_exist(tb_name=tb_name, mapping=mapping) | ||
| 171 | + if result: | ||
| 172 | + return True | ||
| 173 | + with self._lock: | ||
| 174 | + result = await self._tb_exist(tb_name=tb_name, mapping=mapping) | ||
| 175 | + if result: | ||
| 176 | + return True | ||
| 177 | + resp = await self.es_client.indices.create(index=tb_name, body=mapping) | ||
| 178 | + logger.info(f'index {tb_name} create response: {resp}') | ||
| 179 | + return resp.body.get('acknowledged') is True | ||
| 180 | + | ||
| 181 | + async def upsert(self, tb_name, id, embedding, params: dict): | ||
| 182 | + doc = { | ||
| 183 | + 'embedding': embedding, | ||
| 184 | + **params | ||
| 185 | + } | ||
| 186 | + | ||
| 187 | + auto_create_index = os.getenv('AUTO_CREATE_INDEX', 'true').lower().strip() | ||
| 188 | + if auto_create_index == 'true': | ||
| 189 | + mapping = generate_mapping(doc) | ||
| 190 | + # logger.info(f'Possible mapping: {json.dumps(mapping, indent=4)}') | ||
| 191 | + result = await self._create_index(tb_name=tb_name, mapping=mapping) | ||
| 192 | + | ||
| 193 | + if isinstance(embedding, numpy.ndarray): | ||
| 194 | + embedding = embedding.tolist() | ||
| 195 | + # elif isinstance(embedding, typing.Iterable): | ||
| 196 | + # if isinstance(embedding, typing.Mapping): | ||
| 197 | + # pass | ||
| 198 | + # else: | ||
| 199 | + # embedding = list(embedding) | ||
| 200 | + doc['embedding'] = embedding | ||
| 201 | + | ||
| 202 | + resp = await self.es_client.index(index=tb_name, id=id, body=doc) | ||
| 203 | + logger.info(f'index {tb_name} index response: {resp}') | ||
| 204 | + | ||
| 205 | + body = resp.body | ||
| 206 | + assert body.get('result') in ['created', 'updated'], f'数据插入失败, id: {id}' | ||
| 207 | + return body.get('_id') | ||
| 208 | + | ||
| 209 | + | ||
| 210 | +def get_filter_clause(k, v, opt, type_property): | ||
| 211 | + property_type = type_property.get('type') | ||
| 212 | + if property_type == 'text': | ||
| 213 | + property_fields = type_property.get('fields') or {} | ||
| 214 | + if 'keyword' in property_fields: | ||
| 215 | + k = f'{k}.keyword' | ||
| 216 | + | ||
| 217 | + opt = opt.lower() | ||
| 218 | + # "eq", "neq", "lt", "gt", "lte", "gte", "like", "in" | ||
| 219 | + if opt in ['eq', 'in']: | ||
| 220 | + v = list(v) if isinstance(v, typing.Collection) else [v] | ||
| 221 | + should_list = [] | ||
| 222 | + if None in v or len(v) == 0: | ||
| 223 | + should_list.append({ | ||
| 224 | + "bool": { | ||
| 225 | + "must_not": [ | ||
| 226 | + { | ||
| 227 | + "exists": { | ||
| 228 | + "field": k | ||
| 229 | + } | ||
| 230 | + } | ||
| 231 | + ] | ||
| 232 | + } | ||
| 233 | + }) | ||
| 234 | + no_null_v = [item for item in v if item is not None] | ||
| 235 | + if len(no_null_v) > 0: | ||
| 236 | + should_list.append({ | ||
| 237 | + "terms": { | ||
| 238 | + k: no_null_v | ||
| 239 | + } | ||
| 240 | + }) | ||
| 241 | + return should_list[0] if len(should_list) == 1 else { | ||
| 242 | + "bool": { | ||
| 243 | + "should": should_list, | ||
| 244 | + "minimum_should_match": 1 | ||
| 245 | + } | ||
| 246 | + } | ||
| 247 | + elif opt == 'neq': | ||
| 248 | + must_list = [] | ||
| 249 | + v = list(v) if isinstance(v, typing.Collection) else [v] | ||
| 250 | + | ||
| 251 | + if None in v or len(v) == 0: | ||
| 252 | + must_list.append({ | ||
| 253 | + "bool": { | ||
| 254 | + "must": [ | ||
| 255 | + { | ||
| 256 | + "exists": { | ||
| 257 | + "field": k | ||
| 258 | + } | ||
| 259 | + } | ||
| 260 | + ] | ||
| 261 | + } | ||
| 262 | + }) | ||
| 263 | + no_null_v = [item for item in v if item is not None] | ||
| 264 | + if len(no_null_v) > 0: | ||
| 265 | + must_list.append({ | ||
| 266 | + "bool": { | ||
| 267 | + "must_not": { | ||
| 268 | + "terms": { | ||
| 269 | + k: no_null_v | ||
| 270 | + } | ||
| 271 | + } | ||
| 272 | + } | ||
| 273 | + }) | ||
| 274 | + return must_list[0] if len(must_list) == 1 else { | ||
| 275 | + "bool": { | ||
| 276 | + "must": must_list | ||
| 277 | + } | ||
| 278 | + } | ||
| 279 | + elif opt in ['lt', 'lte', 'gt', 'gte']: | ||
| 280 | + v = list(v) if isinstance(v, typing.Collection) else [v] | ||
| 281 | + if len(v) > 0 and v[0] is not None: | ||
| 282 | + return { | ||
| 283 | + "range": { | ||
| 284 | + k: {opt: list(v)[0]} | ||
| 285 | + } | ||
| 286 | + } | ||
| 287 | + else: | ||
| 288 | + return { | ||
| 289 | + "range": { | ||
| 290 | + k: {} | ||
| 291 | + } | ||
| 292 | + } | ||
| 293 | + elif opt == 'like': | ||
| 294 | + v = list(v) if isinstance(v, typing.Collection) else [v] | ||
| 295 | + if len(v) > 0 and v[0] is not None: | ||
| 296 | + return { | ||
| 297 | + "wildcard": { | ||
| 298 | + k: f"*{list(v)[0]}*" | ||
| 299 | + } | ||
| 300 | + } | ||
| 301 | + else: | ||
| 302 | + return { | ||
| 303 | + "bool": { | ||
| 304 | + "must_not": [ | ||
| 305 | + { | ||
| 306 | + "exists": { | ||
| 307 | + "field": k | ||
| 308 | + } | ||
| 309 | + } | ||
| 310 | + ] | ||
| 311 | + } | ||
| 312 | + } | ||
| 313 | + else: | ||
| 314 | + raise Exception(f'opt {opt} not support') | ||
| 315 | + | ||
| 316 | + | ||
| 317 | +def generate_mapping(doc): | ||
| 318 | + def _inner(v): | ||
| 319 | + if isinstance(v, numpy.ndarray): | ||
| 320 | + return 'dense_vector' | ||
| 321 | + elif isinstance(v, str): | ||
| 322 | + return 'text' | ||
| 323 | + elif isinstance(v, int): | ||
| 324 | + return 'long' | ||
| 325 | + elif isinstance(v, float): | ||
| 326 | + return 'float' | ||
| 327 | + elif isinstance(v, typing.Mapping): | ||
| 328 | + return None | ||
| 329 | + elif isinstance(v, typing.Collection): | ||
| 330 | + if len(v) == 0: | ||
| 331 | + return None | ||
| 332 | + else: | ||
| 333 | + return _inner(list(v)[0]) | ||
| 334 | + else: | ||
| 335 | + return None | ||
| 336 | + | ||
| 337 | + properties = dict() | ||
| 338 | + for key, value in (doc or {}).items(): | ||
| 339 | + key_type = _inner(value) | ||
| 340 | + if key_type: | ||
| 341 | + properties[key] = { | ||
| 342 | + 'type': key_type, | ||
| 343 | + } | ||
| 344 | + if key_type == 'dense_vector': | ||
| 345 | + properties[key]['dims'] = len(value) | ||
| 346 | + properties[key].update({ | ||
| 347 | + 'dims': len(value), | ||
| 348 | + 'index': True, | ||
| 349 | + 'similarity': 'cosine' | ||
| 350 | + }) | ||
| 351 | + elif key_type == 'text': | ||
| 352 | + properties[key]['fields'] = { | ||
| 353 | + "keyword": { | ||
| 354 | + "type": "keyword", | ||
| 355 | + # "ignore_above": 256 | ||
| 356 | + } | ||
| 357 | + } | ||
| 358 | + es_mapping = { | ||
| 359 | + "settings": { | ||
| 360 | + "number_of_replicas": int(os.getenv('ES_NUMBER_OF_REPLICAS', 0)), | ||
| 361 | + }, | ||
| 362 | + "mappings": { | ||
| 363 | + "properties": properties | ||
| 364 | + } | ||
| 365 | + } | ||
| 366 | + return es_mapping |
| @@ -12,6 +12,7 @@ dependencies = [ | @@ -12,6 +12,7 @@ dependencies = [ | ||
| 12 | "omegaconf==2.3.0", | 12 | "omegaconf==2.3.0", |
| 13 | "elasticsearch>=9.3.0", | 13 | "elasticsearch>=9.3.0", |
| 14 | "packaging==26.0", | 14 | "packaging==26.0", |
| 15 | + "aiohttp==3.13.5", | ||
| 15 | ] | 16 | ] |
| 16 | 17 | ||
| 17 | [project.optional-dependencies] | 18 | [project.optional-dependencies] |
service/data_service.py
0 → 100644
| 1 | +import logging | ||
| 2 | + | ||
| 3 | +from db import DataDao | ||
| 4 | + | ||
| 5 | +logger = logging.getLogger(__name__) | ||
| 6 | + | ||
| 7 | + | ||
| 8 | +class DataService: | ||
| 9 | + | ||
| 10 | + def __init__(self, data_dao: DataDao): | ||
| 11 | + self.data_dao = data_dao | ||
| 12 | + | ||
| 13 | + async def upsert(self, tb_name, id, embedding, params): | ||
| 14 | + return await self.data_dao.upsert(tb_name, id, embedding, params) | ||
| 15 | + | ||
| 16 | + async def delete_by_pks(self, tb_name, ids): | ||
| 17 | + return await self.data_dao.delete_by_pks(tb_name, ids) | ||
| 18 | + | ||
| 19 | + async def search(self, tb_name, embedding, filters, from_, size): | ||
| 20 | + return await self.data_dao.search(tb_name, embedding, filters, from_, size) |
-
Please register or login to post a comment