2025-01-21 20:52:28 +08:00
#
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
2023-12-14 19:19:03 +08:00
import re
import json
import time
2024-04-17 12:17:14 +08:00
2024-11-12 14:59:41 +08:00
import copy
2025-12-25 21:18:13 +08:00
from elasticsearch_dsl import UpdateByQuery , Q , Search
2024-11-12 14:59:41 +08:00
from elastic_transport import ConnectionTimeout
2025-11-02 12:24:08 +08:00
from common . decorator import singleton
2025-12-25 21:18:13 +08:00
from common . doc_store . doc_store_base import MatchTextExpr , OrderByExpr , MatchExpr , MatchDenseExpr , FusionExpr
from common . doc_store . es_conn_base import ESConnectionBase
2025-10-28 09:46:32 +08:00
from common . float_utils import get_float
2025-11-06 09:36:38 +08:00
from common . constants import PAGERANK_FLD , TAG_FLD
2023-12-14 19:19:03 +08:00
2024-11-21 11:37:45 +08:00
ATTEMPT_TIME = 2
2026-03-02 14:02:36 +08:00
MAX_RESULT_WINDOW = 10000
SEARCH_AFTER_BATCH_SIZE = 1000
2024-11-21 11:37:45 +08:00
2026-04-07 18:52:18 -07:00
# Single-document atomic pagerank_fea adjust (chunk feedback). Clamps using params.min_w / max_w;
# removes field at zero for rank_feature compatibility.
_PAGERANK_FEA_ADJUST_SCRIPT = """
double cur = 0.0 ;
if ( ctx . _source . containsKey ( params . pf ) ) {
Object v = ctx . _source [ params . pf ] ;
if ( v != null ) {
if ( v instanceof Number ) {
cur = ( ( Number ) v ) . doubleValue ( ) ;
} else {
try { cur = Double . parseDouble ( v . toString ( ) ) ; } catch ( Exception e ) { cur = 0.0 ; }
}
}
}
double nw = cur + params . delta ;
if ( nw < params . min_w ) { nw = params . min_w ; }
if ( nw > params . max_w ) { nw = params . max_w ; }
if ( nw < = 0.0 ) {
if ( ctx . _source . containsKey ( params . pf ) ) {
ctx . _source . remove ( params . pf ) ;
}
} else {
ctx . _source [ params . pf ] = nw ;
}
"""
2025-01-09 17:07:21 +08:00
2024-01-15 08:46:22 +08:00
@singleton
2025-12-25 21:18:13 +08:00
class ESConnection ( ESConnectionBase ) :
2024-11-12 14:59:41 +08:00
"""
CRUD operations
"""
2024-11-14 12:29:15 +08:00
2026-03-02 14:02:36 +08:00
def _es_search_once ( self , index_names : list [ str ] , query : dict , track_total_hits : bool ) :
2026-07-02 23:22:07 +08:00
return self . es . search ( index = index_names , body = query , timeout = " 600s " , track_total_hits = track_total_hits )
2026-03-02 14:02:36 +08:00
def _search_with_search_after ( self , index_names : list [ str ] , query : dict , offset : int , limit : int ) :
q_base = copy . deepcopy ( query )
q_base . pop ( " from " , None )
q_base . pop ( " size " , None )
search_after = None
template_res = None
collected_hits = [ ]
remaining_skip = max ( 0 , offset )
remaining_take = max ( 0 , limit )
with_aggs = True
while remaining_skip > 0 :
batch = min ( SEARCH_AFTER_BATCH_SIZE , remaining_skip )
q_iter = copy . deepcopy ( q_base )
q_iter [ " size " ] = batch
if search_after is not None :
q_iter [ " search_after " ] = search_after
if not with_aggs :
q_iter . pop ( " aggs " , None )
res = self . _es_search_once ( index_names , q_iter , track_total_hits = template_res is None )
if template_res is None :
template_res = res
hits = res . get ( " hits " , { } ) . get ( " hits " , [ ] )
if not hits :
break
next_search_after = hits [ - 1 ] . get ( " sort " )
if not next_search_after or next_search_after == search_after :
break
search_after = next_search_after
remaining_skip - = len ( hits )
with_aggs = False
if len ( hits ) < batch :
break
while remaining_skip < = 0 and remaining_take > 0 :
batch = min ( SEARCH_AFTER_BATCH_SIZE , remaining_take )
q_iter = copy . deepcopy ( q_base )
q_iter [ " size " ] = batch
if search_after is not None :
q_iter [ " search_after " ] = search_after
if not with_aggs :
q_iter . pop ( " aggs " , None )
res = self . _es_search_once ( index_names , q_iter , track_total_hits = template_res is None )
if template_res is None :
template_res = res
hits = res . get ( " hits " , { } ) . get ( " hits " , [ ] )
if not hits :
break
collected_hits . extend ( hits )
remaining_take - = len ( hits )
next_search_after = hits [ - 1 ] . get ( " sort " )
if not next_search_after or next_search_after == search_after :
break
search_after = next_search_after
with_aggs = False
if len ( hits ) < batch :
break
if template_res is None :
q_count = copy . deepcopy ( q_base )
q_count [ " size " ] = 0
template_res = self . _es_search_once ( index_names , q_count , track_total_hits = True )
template_res [ " hits " ] [ " hits " ] = collected_hits
return template_res
2025-01-09 17:07:21 +08:00
def search (
2026-07-02 23:22:07 +08:00
self ,
select_fields : list [ str ] ,
highlight_fields : list [ str ] ,
condition : dict ,
match_expressions : list [ MatchExpr ] ,
order_by : OrderByExpr ,
offset : int ,
limit : int ,
index_names : str | list [ str ] ,
knowledgebase_ids : list [ str ] ,
agg_fields : list [ str ] | None = None ,
rank_feature : dict | None = None ,
2025-01-23 18:43:32 +08:00
) :
2024-11-12 14:59:41 +08:00
"""
Refers to https : / / www . elastic . co / guide / en / elasticsearch / reference / current / query - dsl . html
"""
2025-12-25 21:18:13 +08:00
if isinstance ( index_names , str ) :
index_names = index_names . split ( " , " )
assert isinstance ( index_names , list ) and len ( index_names ) > 0
2024-11-12 14:59:41 +08:00
assert " _id " not in condition
2024-11-20 11:47:39 +08:00
2025-12-25 21:18:13 +08:00
bool_query = Q ( " bool " , must = [ ] )
condition [ " kb_id " ] = knowledgebase_ids
2024-11-20 11:47:39 +08:00
for k , v in condition . items ( ) :
2024-11-22 12:25:42 +08:00
if k == " available_int " :
if v == 0 :
2025-12-25 21:18:13 +08:00
bool_query . filter . append ( Q ( " range " , available_int = { " lt " : 1 } ) )
2024-11-22 12:25:42 +08:00
else :
2026-07-02 23:22:07 +08:00
bool_query . filter . append ( Q ( " bool " , must_not = Q ( " range " , available_int = { " lt " : 1 } ) ) )
2024-11-20 11:47:39 +08:00
continue
2026-04-24 21:38:19 +08:00
if k == " id " :
if not v :
continue
if isinstance ( v , list ) :
2026-07-02 23:22:07 +08:00
bool_query . filter . append ( Q ( " bool " , should = [ Q ( " terms " , id = v ) , Q ( " terms " , _id = v ) ] , minimum_should_match = 1 ) )
2026-04-24 21:38:19 +08:00
elif isinstance ( v , str ) or isinstance ( v , int ) :
2026-07-02 23:22:07 +08:00
bool_query . filter . append ( Q ( " bool " , should = [ Q ( " term " , id = v ) , Q ( " term " , _id = v ) ] , minimum_should_match = 1 ) )
2026-04-24 21:38:19 +08:00
continue
2026-07-02 23:22:07 +08:00
if k == " must_not " :
if isinstance ( v , dict ) :
for kk , vv in v . items ( ) :
if kk == " exists " :
bool_query . must_not . append ( Q ( " exists " , field = vv ) )
continue
2024-12-08 14:21:12 +08:00
if not v :
continue
2024-11-20 11:47:39 +08:00
if isinstance ( v , list ) :
2025-12-25 21:18:13 +08:00
bool_query . filter . append ( Q ( " terms " , * * { k : v } ) )
2024-11-20 11:47:39 +08:00
elif isinstance ( v , str ) or isinstance ( v , int ) :
2025-12-25 21:18:13 +08:00
bool_query . filter . append ( Q ( " term " , * * { k : v } ) )
2024-11-20 11:47:39 +08:00
else :
2026-07-02 23:22:07 +08:00
raise Exception ( f " Condition ` { str ( k ) } = { str ( v ) } ` value type is { str ( type ( v ) ) } , expected to be int, str or list. " )
2024-11-20 11:47:39 +08:00
2024-11-12 14:59:41 +08:00
s = Search ( )
vector_similarity_weight = 0.5
2025-12-25 21:18:13 +08:00
for m in match_expressions :
2024-11-14 12:29:15 +08:00
if isinstance ( m , FusionExpr ) and m . method == " weighted_sum " and " weights " in m . fusion_params :
2026-07-02 23:22:07 +08:00
assert (
len ( match_expressions ) == 3
and isinstance ( match_expressions [ 0 ] , MatchTextExpr )
and isinstance ( match_expressions [ 1 ] , MatchDenseExpr )
and isinstance ( match_expressions [ 2 ] , FusionExpr )
)
2024-11-12 14:59:41 +08:00
weights = m . fusion_params [ " weights " ]
2025-03-18 11:13:44 +08:00
vector_similarity_weight = get_float ( weights . split ( " , " ) [ 1 ] )
2025-12-25 21:18:13 +08:00
for m in match_expressions :
2024-11-12 14:59:41 +08:00
if isinstance ( m , MatchTextExpr ) :
2024-11-27 12:45:43 +08:00
minimum_should_match = m . extra_options . get ( " minimum_should_match " , 0.0 )
if isinstance ( minimum_should_match , float ) :
minimum_should_match = str ( int ( minimum_should_match * 100 ) ) + " % "
2026-07-02 23:22:07 +08:00
bool_query . must . append ( Q ( " query_string " , fields = m . fields , type = " best_fields " , query = m . matching_text , minimum_should_match = minimum_should_match , boost = 1 ) )
2025-12-25 21:18:13 +08:00
bool_query . boost = 1.0 - vector_similarity_weight
2024-11-20 11:47:39 +08:00
2024-11-12 14:59:41 +08:00
elif isinstance ( m , MatchDenseExpr ) :
2026-07-02 23:22:07 +08:00
assert bool_query is not None
2024-11-12 14:59:41 +08:00
similarity = 0.0
if " similarity " in m . extra_options :
similarity = m . extra_options [ " similarity " ]
2026-07-02 23:22:07 +08:00
s = s . knn (
m . vector_column_name ,
m . topn ,
m . topn * 2 ,
query_vector = list ( m . embedding_data ) ,
filter = bool_query . to_dict ( ) ,
similarity = similarity ,
)
2024-11-14 12:29:15 +08:00
2025-12-25 21:18:13 +08:00
if bool_query and rank_feature :
2025-01-09 17:07:21 +08:00
for fld , sc in rank_feature . items ( ) :
if fld != PAGERANK_FLD :
fld = f " { TAG_FLD } . { fld } "
2025-12-25 21:18:13 +08:00
bool_query . should . append ( Q ( " rank_feature " , field = fld , linear = { } , boost = sc ) )
2025-01-09 17:07:21 +08:00
2025-12-25 21:18:13 +08:00
if bool_query :
s = s . query ( bool_query )
for field in highlight_fields :
2024-11-12 14:59:41 +08:00
s = s . highlight ( field )
2025-12-25 21:18:13 +08:00
if order_by :
2024-11-12 14:59:41 +08:00
orders = list ( )
2025-12-25 21:18:13 +08:00
for field , order in order_by . fields :
2024-11-12 14:59:41 +08:00
order = " asc " if order == 0 else " desc "
2024-12-10 16:32:58 +08:00
if field in [ " page_num_int " , " top_int " ] :
2026-07-02 23:22:07 +08:00
order_info = { " order " : order , " unmapped_type " : " float " , " mode " : " avg " , " numeric_type " : " double " }
2024-12-10 16:32:58 +08:00
elif field . endswith ( " _int " ) or field . endswith ( " _flt " ) :
order_info = { " order " : order , " unmapped_type " : " float " }
2026-04-30 14:52:43 +08:00
elif field == " id " :
2026-07-02 23:22:07 +08:00
continue # id as "text", not a "keyword", order by it will cause error
2024-12-10 16:32:58 +08:00
else :
2026-05-09 17:41:08 +08:00
order_info = { " order " : order , " unmapped_type " : " keyword " }
2024-12-10 16:32:58 +08:00
orders . append ( { field : order_info } )
2024-11-12 14:59:41 +08:00
s = s . sort ( * orders )
2025-12-25 21:18:13 +08:00
if agg_fields :
for fld in agg_fields :
2026-07-02 23:22:07 +08:00
s . aggs . bucket ( f " aggs_ { fld } " , " terms " , field = fld , size = 1000000 )
2025-01-09 17:07:21 +08:00
2026-03-02 14:02:36 +08:00
has_dense = any ( isinstance ( m , MatchDenseExpr ) for m in match_expressions )
has_explicit_sort = bool ( order_by and order_by . fields )
2026-07-02 23:22:07 +08:00
use_search_after = limit > 0 and ( offset + limit > MAX_RESULT_WINDOW ) and has_explicit_sort and not has_dense
2026-03-02 14:02:36 +08:00
if limit > 0 and not use_search_after :
2026-07-02 23:22:07 +08:00
s = s [ offset : offset + limit ]
2026-04-09 17:44:13 +08:00
# Filter _source to only requested fields for efficiency, and add vector
# fields to "fields" param so they appear in hit.fields when ES 9.x
# exclude_source_vectors is enabled (dense_vector not in _source).
if select_fields :
s = s . source ( select_fields )
2024-11-12 14:59:41 +08:00
q = s . to_dict ( )
2026-04-09 17:44:13 +08:00
# ES 9.x: dense_vector fields excluded from _source; request them via fields.
# Note: knn does NOT have a "fields" parameter - adding it inside the knn
# object causes BadRequestError on ES 9.x. We add "fields" at top level.
vector_fields = [ f for f in ( select_fields or [ ] ) if f . endswith ( " _vec " ) ]
if vector_fields :
q [ " fields " ] = vector_fields
2025-12-25 21:18:13 +08:00
self . logger . debug ( f " ESConnection.search { str ( index_names ) } query: " + json . dumps ( q ) )
2024-11-12 14:59:41 +08:00
2024-11-21 11:37:45 +08:00
for i in range ( ATTEMPT_TIME ) :
2023-12-14 19:19:03 +08:00
try :
2026-03-02 14:02:36 +08:00
if use_search_after :
res = self . _search_with_search_after ( index_names , q , offset , limit )
else :
# print(json.dumps(q, ensure_ascii=False))
res = self . _es_search_once ( index_names , q , track_total_hits = True )
2023-12-14 19:19:03 +08:00
if str ( res . get ( " timed_out " , " " ) ) . lower ( ) == " true " :
raise Exception ( " Es Timeout. " )
2025-12-25 21:18:13 +08:00
self . logger . debug ( f " ESConnection.search { str ( index_names ) } res: " + str ( res ) )
2023-12-14 19:19:03 +08:00
return res
2025-07-07 09:22:25 +08:00
except ConnectionTimeout :
2025-12-25 21:18:13 +08:00
self . logger . exception ( " ES request timeout " )
2025-07-07 09:22:25 +08:00
self . _connect ( )
continue
2023-12-14 19:19:03 +08:00
except Exception as e :
2026-01-28 13:29:34 +08:00
# Only log debug for NotFoundError(accepted when metadata index doesn't exist)
2026-07-02 23:22:07 +08:00
if " NotFound " in str ( e ) :
2026-01-28 13:29:34 +08:00
self . logger . debug ( f " ESConnection.search { str ( index_names ) } query: " + str ( q ) + " - " + str ( e ) )
else :
self . logger . exception ( f " ESConnection.search { str ( index_names ) } query: " + str ( q ) + str ( e ) )
2023-12-14 19:19:03 +08:00
raise e
2025-07-07 09:22:25 +08:00
2025-12-25 21:18:13 +08:00
self . logger . error ( f " ESConnection.search timeout for { ATTEMPT_TIME } times! " )
2024-11-20 11:47:39 +08:00
raise Exception ( " ESConnection.search timeout. " )
2023-12-14 19:19:03 +08:00
2025-12-25 21:18:13 +08:00
def insert ( self , documents : list [ dict ] , index_name : str , knowledgebase_id : str = None ) - > list [ str ] :
2024-11-12 14:59:41 +08:00
# Refers to https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
operations = [ ]
for d in documents :
assert " _id " not in d
assert " id " in d
d_copy = copy . deepcopy ( d )
2025-12-25 21:18:13 +08:00
d_copy [ " kb_id " ] = knowledgebase_id
2026-03-12 18:59:56 +08:00
# Use id as _id for uniqueness, also keep "id" as a regular field for sorting
meta_id = d_copy . get ( " id " , " " )
2026-07-02 23:22:07 +08:00
operations . append ( { " index " : { " _index " : index_name , " _id " : meta_id } } )
2024-11-12 14:59:41 +08:00
operations . append ( d_copy )
2023-12-14 19:19:03 +08:00
2024-11-12 14:59:41 +08:00
res = [ ]
2024-11-21 11:37:45 +08:00
for _ in range ( ATTEMPT_TIME ) :
2023-12-22 17:57:27 +08:00
try :
2024-11-22 12:00:25 +08:00
res = [ ]
2026-07-02 23:22:07 +08:00
r = self . es . bulk ( index = index_name , operations = operations , refresh = " wait_for " , timeout = " 60s " )
2024-11-12 14:59:41 +08:00
if re . search ( r " False " , str ( r [ " errors " ] ) , re . IGNORECASE ) :
return res
2023-12-22 17:57:27 +08:00
2024-11-12 14:59:41 +08:00
for item in r [ " items " ] :
for action in [ " create " , " delete " , " index " , " update " ] :
if action in item and " error " in item [ action ] :
res . append ( str ( item [ action ] [ " _id " ] ) + " : " + str ( item [ action ] [ " error " ] ) )
return res
2025-07-07 09:22:25 +08:00
except ConnectionTimeout :
2025-12-25 21:18:13 +08:00
self . logger . exception ( " ES request timeout " )
2025-07-07 09:22:25 +08:00
time . sleep ( 3 )
self . _connect ( )
continue
2023-12-14 19:19:03 +08:00
except Exception as e :
2024-11-22 12:00:25 +08:00
res . append ( str ( e ) )
2025-12-25 21:18:13 +08:00
self . logger . warning ( " ESConnection.insert got exception: " + str ( e ) )
2025-07-07 09:22:25 +08:00
2024-11-12 14:59:41 +08:00
return res
2023-12-14 19:19:03 +08:00
2025-12-25 21:18:13 +08:00
def update ( self , condition : dict , new_value : dict , index_name : str , knowledgebase_id : str ) - > bool :
doc = copy . deepcopy ( new_value )
2024-11-19 14:15:25 +08:00
doc . pop ( " id " , None )
2025-12-25 21:18:13 +08:00
condition [ " kb_id " ] = knowledgebase_id
2024-11-12 14:59:41 +08:00
if " id " in condition and isinstance ( condition [ " id " ] , str ) :
# update specific single document
2025-12-25 21:18:13 +08:00
chunk_id = condition [ " id " ]
2024-11-21 11:37:45 +08:00
for i in range ( ATTEMPT_TIME ) :
2026-04-07 18:52:18 -07:00
doc_part = copy . deepcopy ( doc )
remove_value = doc_part . pop ( " remove " , None )
remove_field = remove_value if isinstance ( remove_value , str ) else None
remove_dict = remove_value if isinstance ( remove_value , dict ) else None
for k in doc_part . keys ( ) :
2025-03-19 11:25:11 +08:00
if " feas " != k . split ( " _ " ) [ - 1 ] :
continue
try :
2026-07-02 23:22:07 +08:00
self . es . update ( index = index_name , id = chunk_id , script = f ' ctx._source.remove( " { k } " ); ' )
2025-03-19 11:25:11 +08:00
except Exception :
2026-07-02 23:22:07 +08:00
self . logger . exception ( f " ESConnection.update(index= { index_name } , id= { chunk_id } , doc= { json . dumps ( condition , ensure_ascii = False ) } ) got exception " )
2024-11-12 14:59:41 +08:00
try :
2026-04-07 18:52:18 -07:00
if remove_field is not None :
self . es . update (
index = index_name ,
id = chunk_id ,
script = f " ctx._source.remove( ' { remove_field } ' ); " ,
)
if remove_dict is not None :
scripts = [ ]
params = { }
for kk , vv in remove_dict . items ( ) :
scripts . append (
2026-07-02 23:22:07 +08:00
f " if (ctx._source.containsKey( ' { kk } ' ) && ctx._source. { kk } != null) {{ int i = ctx._source. { kk } .indexOf(params.p_ { kk } ); if (i >= 0) {{ ctx._source. { kk } .remove(i); }} }} "
2026-04-07 18:52:18 -07:00
)
params [ f " p_ { kk } " ] = vv
if scripts :
self . es . update (
index = index_name ,
id = chunk_id ,
script = { " source " : " " . join ( scripts ) , " params " : params } ,
)
if doc_part :
self . es . update ( index = index_name , id = chunk_id , doc = doc_part )
if remove_field is not None or remove_dict is not None or doc_part :
return True
2024-11-12 14:59:41 +08:00
except Exception as e :
2026-07-02 23:22:07 +08:00
self . logger . exception ( f " ESConnection.update(index= { index_name } , id= { chunk_id } , doc= { json . dumps ( condition , ensure_ascii = False ) } ) got exception: " + str ( e ) )
2025-01-22 19:43:14 +08:00
break
2024-12-03 14:30:35 +08:00
return False
2025-01-09 17:07:21 +08:00
# update unspecific maybe-multiple documents
2025-12-25 21:18:13 +08:00
bool_query = Q ( " bool " )
2025-01-09 17:07:21 +08:00
for k , v in condition . items ( ) :
if not isinstance ( k , str ) or not v :
continue
2025-01-26 18:45:36 +08:00
if k == " exists " :
2025-12-25 21:18:13 +08:00
bool_query . filter . append ( Q ( " exists " , field = v ) )
2025-01-09 17:07:21 +08:00
continue
2026-07-02 23:22:07 +08:00
if k == " must_not " :
if isinstance ( v , dict ) :
for kk , vv in v . items ( ) :
if kk == " exists " :
bool_query . must_not . append ( Q ( " exists " , field = vv ) )
continue
2025-01-09 17:07:21 +08:00
if isinstance ( v , list ) :
2025-12-25 21:18:13 +08:00
bool_query . filter . append ( Q ( " terms " , * * { k : v } ) )
2025-01-09 17:07:21 +08:00
elif isinstance ( v , str ) or isinstance ( v , int ) :
2025-12-25 21:18:13 +08:00
bool_query . filter . append ( Q ( " term " , * * { k : v } ) )
2025-01-09 17:07:21 +08:00
else :
2026-07-02 23:22:07 +08:00
raise Exception ( f " Condition ` { str ( k ) } = { str ( v ) } ` value type is { str ( type ( v ) ) } , expected to be int, str or list. " )
2025-01-09 17:07:21 +08:00
scripts = [ ]
params = { }
2025-12-25 21:18:13 +08:00
for k , v in new_value . items ( ) :
2025-01-09 17:07:21 +08:00
if k == " remove " :
2024-11-12 14:59:41 +08:00
if isinstance ( v , str ) :
2025-01-09 17:07:21 +08:00
scripts . append ( f " ctx._source.remove( ' { v } ' ); " )
if isinstance ( v , dict ) :
for kk , vv in v . items ( ) :
scripts . append ( f " int i=ctx._source. { kk } .indexOf(params.p_ { kk } );ctx._source. { kk } .remove(i); " )
params [ f " p_ { kk } " ] = vv
continue
if k == " add " :
if isinstance ( v , dict ) :
for kk , vv in v . items ( ) :
scripts . append ( f " ctx._source. { kk } .add(params.pp_ { kk } ); " )
params [ f " pp_ { kk } " ] = vv . strip ( )
continue
if ( not isinstance ( k , str ) or not v ) and k != " available_int " :
continue
if isinstance ( v , str ) :
2025-01-22 19:43:14 +08:00
v = re . sub ( r " ([ ' \ n \ r]| \\ .) " , " " , v )
2025-02-10 18:18:49 +08:00
params [ f " pp_ { k } " ] = v
scripts . append ( f " ctx._source. { k } =params.pp_ { k } ; " )
2025-01-22 19:43:14 +08:00
elif isinstance ( v , int ) or isinstance ( v , float ) :
scripts . append ( f " ctx._source. { k } = { v } ; " )
elif isinstance ( v , list ) :
2025-02-10 18:18:49 +08:00
scripts . append ( f " ctx._source. { k } =params.pp_ { k } ; " )
params [ f " pp_ { k } " ] = json . dumps ( v , ensure_ascii = False )
2025-01-09 17:07:21 +08:00
else :
2026-07-02 23:22:07 +08:00
raise Exception ( f " newValue ` { str ( k ) } = { str ( v ) } ` value type is { str ( type ( v ) ) } , expected to be int, str. " )
ubq = UpdateByQuery ( index = index_name ) . using ( self . es ) . query ( bool_query )
2025-01-09 17:07:21 +08:00
ubq = ubq . script ( source = " " . join ( scripts ) , params = params )
2024-12-03 14:30:35 +08:00
ubq = ubq . params ( refresh = True )
ubq = ubq . params ( slices = 5 )
ubq = ubq . params ( conflicts = " proceed " )
2025-01-09 17:07:21 +08:00
for _ in range ( ATTEMPT_TIME ) :
2024-12-03 14:30:35 +08:00
try :
_ = ubq . execute ( )
return True
2025-07-07 09:22:25 +08:00
except ConnectionTimeout :
2025-12-25 21:18:13 +08:00
self . logger . exception ( " ES request timeout " )
2025-07-07 09:22:25 +08:00
time . sleep ( 3 )
self . _connect ( )
continue
2024-12-03 14:30:35 +08:00
except Exception as e :
2025-12-25 21:18:13 +08:00
self . logger . error ( " ESConnection.update got exception: " + str ( e ) + " \n " . join ( scripts ) )
2025-01-22 19:43:14 +08:00
break
2023-12-14 19:19:03 +08:00
return False
2026-04-07 18:52:18 -07:00
def adjust_chunk_pagerank_fea (
self ,
chunk_id : str ,
index_name : str ,
knowledgebase_id : str ,
delta : float ,
min_w : float = 0.0 ,
max_w : float = 100.0 ,
row_id : int | None = None ,
) - > bool :
""" Atomically adjust pagerank_fea on one chunk (painless script). """
_ = row_id
for _ in range ( ATTEMPT_TIME ) :
try :
self . es . update (
index = index_name ,
id = chunk_id ,
retry_on_conflict = 3 ,
script = {
" source " : _PAGERANK_FEA_ADJUST_SCRIPT . strip ( ) ,
" lang " : " painless " ,
" params " : {
" pf " : PAGERANK_FLD ,
" delta " : float ( delta ) ,
" min_w " : float ( min_w ) ,
" max_w " : float ( max_w ) ,
} ,
} ,
)
self . logger . debug (
" ESConnection.adjust_chunk_pagerank_fea(index= %s , id= %s , delta= %s ) succeeded " ,
index_name ,
chunk_id ,
delta ,
)
return True
except ConnectionTimeout :
self . logger . exception ( " ES request timeout " )
time . sleep ( 3 )
self . _connect ( )
continue
except Exception as e :
self . logger . exception (
" ESConnection.adjust_chunk_pagerank_fea(index= %s , id= %s ): %s " ,
index_name ,
chunk_id ,
e ,
)
if re . search ( r " connection " , str ( e ) . lower ( ) ) :
time . sleep ( 3 )
self . _connect ( )
continue
break
return False
2025-12-25 21:18:13 +08:00
def delete ( self , condition : dict , index_name : str , knowledgebase_id : str ) - > int :
2024-11-12 14:59:41 +08:00
assert " _id " not in condition
2025-12-25 21:18:13 +08:00
condition [ " kb_id " ] = knowledgebase_id
2026-01-15 12:15:55 +05:30
# Build a bool query that combines id filter with other conditions
bool_query = Q ( " bool " )
# Handle chunk IDs if present
2024-11-12 14:59:41 +08:00
if " id " in condition :
chunk_ids = condition [ " id " ]
if not isinstance ( chunk_ids , list ) :
chunk_ids = [ chunk_ids ]
2026-01-15 12:15:55 +05:30
if chunk_ids :
# Filter by specific chunk IDs
bool_query . filter . append ( Q ( " ids " , values = chunk_ids ) )
# If chunk_ids is empty, we don't add an ids filter - rely on other conditions
2025-01-22 19:43:14 +08:00
2026-01-15 12:15:55 +05:30
# Add all other conditions as filters
for k , v in condition . items ( ) :
if k == " id " :
continue # Already handled above
if k == " exists " :
bool_query . filter . append ( Q ( " exists " , field = v ) )
elif k == " must_not " :
if isinstance ( v , dict ) :
for kk , vv in v . items ( ) :
if kk == " exists " :
bool_query . must_not . append ( Q ( " exists " , field = vv ) )
elif isinstance ( v , list ) :
bool_query . must . append ( Q ( " terms " , * * { k : v } ) )
elif isinstance ( v , str ) or isinstance ( v , int ) :
bool_query . must . append ( Q ( " term " , * * { k : v } ) )
elif v is not None :
raise Exception ( " Condition value must be int, str or list. " )
2025-01-22 19:43:14 +08:00
2026-01-15 12:15:55 +05:30
# If no filters were added, use match_all (for tenant-wide operations)
if not bool_query . filter and not bool_query . must and not bool_query . must_not :
qry = Q ( " match_all " )
else :
qry = bool_query
2025-12-25 21:18:13 +08:00
self . logger . debug ( " ESConnection.delete query: " + json . dumps ( qry . to_dict ( ) ) )
2024-11-21 11:37:45 +08:00
for _ in range ( ATTEMPT_TIME ) :
2023-12-14 19:19:03 +08:00
try :
2026-07-02 23:22:07 +08:00
res = self . es . delete_by_query ( index = index_name , body = Search ( ) . query ( qry ) . to_dict ( ) , refresh = True )
2024-11-12 14:59:41 +08:00
return res [ " deleted " ]
2025-07-07 09:22:25 +08:00
except ConnectionTimeout :
2025-12-25 21:18:13 +08:00
self . logger . exception ( " ES request timeout " )
2025-07-07 09:22:25 +08:00
time . sleep ( 3 )
self . _connect ( )
continue
2023-12-14 19:19:03 +08:00
except Exception as e :
2025-12-25 21:18:13 +08:00
self . logger . warning ( " ESConnection.delete got exception: " + str ( e ) )
2024-11-12 14:59:41 +08:00
if re . search ( r " (not_found) " , str ( e ) , re . IGNORECASE ) :
return 0
return 0
2023-12-14 19:19:03 +08:00
2024-11-12 14:59:41 +08:00
"""
Helper functions for search result
"""
2024-11-14 12:29:15 +08:00
2025-11-12 19:00:15 +08:00
def get_fields ( self , res , fields : list [ str ] ) - > dict [ str , dict ] :
2024-11-12 14:59:41 +08:00
res_fields = { }
if not fields :
return { }
2026-04-09 17:44:13 +08:00
hits = res . get ( " hits " , { } ) . get ( " hits " , [ ] )
for hit in hits :
doc_id = hit . get ( " _id " )
d = hit . get ( " _source " , { } )
# Also extract fields from ES "fields" response (used by dense_vector in ES 9.x)
hit_fields = hit . get ( " fields " , { } )
m = { }
for n in fields :
# First check _source
if d . get ( n ) is not None :
m [ n ] = d . get ( n )
# Then check fields (ES 9.x stores dense_vector here, not in _source)
elif n in hit_fields :
vals = hit_fields [ n ]
# ES fields response wraps dense_vector in 2 levels: [[v1,v2,...]] -> [v1,v2,...]
if isinstance ( vals , list ) and len ( vals ) == 1 :
vals = vals [ 0 ]
m [ n ] = vals
2024-11-12 14:59:41 +08:00
for n , v in m . items ( ) :
if isinstance ( v , list ) :
m [ n ] = v
continue
2025-06-19 11:12:53 +08:00
if n == " available_int " and isinstance ( v , ( int , float ) ) :
m [ n ] = v
continue
2024-11-12 14:59:41 +08:00
if not isinstance ( v , str ) :
m [ n ] = str ( m [ n ] )
# if n.find("tks") > 0:
2025-10-28 09:46:32 +08:00
# m[n] = remove_redundant_spaces(m[n])
2023-12-14 19:19:03 +08:00
2024-11-12 14:59:41 +08:00
if m :
2026-04-09 17:44:13 +08:00
res_fields [ doc_id ] = m
2024-11-12 14:59:41 +08:00
return res_fields