Skip to content

Commit e96c897

Browse files
committed
feat: Migration knowledge base retrieval node
1 parent 13b8d50 commit e96c897

2 files changed

Lines changed: 233 additions & 0 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# coding=utf-8
2+
"""
3+
@project: MaxKB
4+
@Author:虎虎虎
5+
@file: __init__.py
6+
@date:2026/7/2 10:00
7+
@desc:
8+
"""
9+
from .search_knowledge_node import SearchKnowledgeNode
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
# coding=utf-8
2+
"""
3+
@project: MaxKB
4+
@Author:虎虎虎
5+
@file: search_knowledge_node.py
6+
@date:2026/7/2 10:00
7+
@desc:
8+
"""
9+
import os
10+
import re
11+
from typing import List, Dict
12+
13+
from django.core import validators
14+
from django.db import connection
15+
from django.db.models import QuerySet
16+
from django.utils.translation import gettext_lazy as _
17+
from rest_framework import serializers
18+
19+
from application.workflow.common import WorkflowType
20+
from application.workflow.i_node import INode
21+
from common.config.embedding_config import VectorStore
22+
from common.constants.permission_constants import RoleConstants
23+
from common.database_model_manage.database_model_manage import DatabaseModelManage
24+
from common.db.search import native_search
25+
from common.utils.common import flat_map, get_file_content
26+
from common.utils.shared_resource_auth import filter_authorized_ids
27+
from knowledge.models import Document, Paragraph, Knowledge, SearchMode
28+
from maxkb.conf import PROJECT_DIR
29+
from models_provider.tools import get_model_instance_by_model_workspace_id
30+
31+
32+
class DatasetSettingSerializer(serializers.Serializer):
33+
top_n = serializers.IntegerField(required=True, label=_("Reference segment number"))
34+
similarity = serializers.FloatField(required=True, max_value=2, min_value=0, label=_('similarity'))
35+
search_mode = serializers.CharField(required=True, validators=[
36+
validators.RegexValidator(regex=re.compile("^embedding|keywords|blend$"),
37+
message=_("The type only supports embedding|keywords|blend"), code=500)
38+
], label=_("Retrieval Mode"))
39+
max_paragraph_char_number = serializers.IntegerField(required=True,
40+
label=_("Maximum number of words in a quoted segment"))
41+
42+
43+
class SearchKnowledgeNodeSerializer(serializers.Serializer):
44+
knowledge_id_list = serializers.ListField(required=True, child=serializers.UUIDField(required=True),
45+
label=_("Dataset id list"))
46+
knowledge_setting = DatasetSettingSerializer(required=True)
47+
question_reference_address = serializers.ListField(required=True)
48+
show_knowledge = serializers.BooleanField(required=True,
49+
label=_("The results are displayed in the knowledge sources"))
50+
search_scope_type = serializers.ChoiceField(
51+
required=False, choices=['custom', 'referencing'], label=_("search scope type"),
52+
allow_null=True, default='custom'
53+
)
54+
search_scope_source = serializers.ChoiceField(
55+
required=False, choices=['document', 'knowledge'],
56+
label=_("search scope variable type"), default='knowledge'
57+
)
58+
search_scope_reference = serializers.ListField(
59+
required=False, label=_("search scope variable"), default=list
60+
)
61+
62+
63+
def _get_paragraph_list(chat_record, node_id):
64+
return flat_map([chat_record.details[key].get('paragraph_list', []) for key in chat_record.details if
65+
(chat_record.details[
66+
key].get('type', '') == 'search-dataset-node') and chat_record.details[key].get(
67+
'paragraph_list', []) is not None and key == node_id])
68+
69+
70+
def _get_embedding_id(dataset_id_list):
71+
dataset_list = QuerySet(Knowledge).filter(id__in=dataset_id_list)
72+
if len(set([dataset.embedding_model_id for dataset in dataset_list])) > 1:
73+
raise Exception("关联知识库的向量模型不一致,无法召回分段。")
74+
if len(dataset_list) == 0:
75+
raise Exception("知识库设置错误,请重新设置知识库")
76+
return dataset_list[0].embedding_model_id
77+
78+
79+
def _reset_title(title):
80+
if title is None or len(title.strip()) == 0:
81+
return ""
82+
else:
83+
return f"#### {title}\n"
84+
85+
86+
def _reset_meta(meta):
87+
if not meta.get('allow_download', False):
88+
return {'allow_download': False}
89+
return meta
90+
91+
92+
def _reset_paragraph(paragraph: Dict, embedding_list: List):
93+
filter_embedding_list = [embedding for embedding in embedding_list if
94+
str(embedding.get('paragraph_id')) == str(paragraph.get('id'))]
95+
if filter_embedding_list is not None and len(filter_embedding_list) > 0:
96+
find_embedding = filter_embedding_list[-1]
97+
return {
98+
**paragraph,
99+
'similarity': find_embedding.get('similarity'),
100+
'is_hit_handling_method': find_embedding.get('similarity') > paragraph.get(
101+
'directly_return_similarity') and paragraph.get('hit_handling_method') == 'directly_return',
102+
'update_time': paragraph.get('update_time').strftime("%Y-%m-%d %H:%M:%S"),
103+
'create_time': paragraph.get('create_time').strftime("%Y-%m-%d %H:%M:%S"),
104+
'id': str(paragraph.get('id')),
105+
'knowledge_id': str(paragraph.get('knowledge_id')),
106+
'document_id': str(paragraph.get('document_id')),
107+
'meta': _reset_meta(paragraph.get('meta'))
108+
}
109+
110+
111+
def _list_paragraph(embedding_list: List, vector):
112+
paragraph_id_list = [row.get('paragraph_id') for row in embedding_list]
113+
if paragraph_id_list is None or len(paragraph_id_list) == 0:
114+
return []
115+
paragraph_list = native_search(QuerySet(Paragraph).filter(id__in=paragraph_id_list),
116+
get_file_content(
117+
os.path.join(PROJECT_DIR, "apps", "application", 'sql',
118+
'list_knowledge_paragraph_by_paragraph_id.sql')),
119+
with_table_name=True)
120+
if len(paragraph_list) != len(paragraph_id_list):
121+
exist_paragraph_list = [row.get('id') for row in paragraph_list]
122+
for paragraph_id in paragraph_id_list:
123+
if paragraph_id not in exist_paragraph_list:
124+
vector.delete_by_paragraph_id(paragraph_id)
125+
return paragraph_list
126+
127+
128+
class SearchKnowledgeNode(INode):
129+
serializer_class = SearchKnowledgeNodeSerializer
130+
supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.TOOL]
131+
type = 'search-knowledge-node'
132+
133+
def execute(self):
134+
node_params = self.get_parameters()
135+
workflow_params = self.get_workflow_parameters()
136+
137+
knowledge_id_list = node_params.get('knowledge_id_list', [])
138+
knowledge_setting = node_params.get('knowledge_setting', {})
139+
question_reference_address = node_params.get('question_reference_address', [])
140+
show_knowledge = node_params.get('show_knowledge', False)
141+
search_scope_type = node_params.get('search_scope_type', 'custom')
142+
search_scope_source = node_params.get('search_scope_source', 'knowledge')
143+
search_scope_reference = node_params.get('search_scope_reference', [])
144+
145+
question = str(self.workflow_manage.get_reference_field(
146+
question_reference_address[0], question_reference_address[1:]))
147+
148+
exclude_paragraph_id_list = []
149+
if workflow_params.get('re_chat', False):
150+
history_chat_record = workflow_params.get('history_chat_record', [])
151+
paragraph_id_list = [p.get('id') for p in flat_map(
152+
[_get_paragraph_list(chat_record, self.get_node_id()) for chat_record in history_chat_record if
153+
chat_record.problem_text == question])]
154+
exclude_paragraph_id_list = list(set(paragraph_id_list))
155+
156+
self.write_context('question', question)
157+
self.write_context('show_knowledge', show_knowledge)
158+
159+
document_id_list = None
160+
if search_scope_type == 'referencing':
161+
if search_scope_source == 'knowledge':
162+
knowledge_id_list = self._get_reference_content(search_scope_reference)
163+
else:
164+
document_id_list = self._get_reference_content(search_scope_reference)
165+
knowledge_id_list = [str(k) for k in QuerySet(Document).filter(
166+
id__in=document_id_list
167+
).values_list('knowledge_id', flat=True).distinct()]
168+
169+
get_knowledge_list_of_authorized = DatabaseModelManage.get_model('get_knowledge_list_of_authorized')
170+
chat_user_type = workflow_params.get('chat_user_type')
171+
if get_knowledge_list_of_authorized is not None and RoleConstants.CHAT_USER.value.name == chat_user_type:
172+
knowledge_id_list = get_knowledge_list_of_authorized(
173+
workflow_params.get('chat_user_id'), knowledge_id_list)
174+
175+
workspace_id = workflow_params.get('workspace_id')
176+
knowledge_id_list = filter_authorized_ids('knowledge', knowledge_id_list, workspace_id)
177+
178+
if len(knowledge_id_list) == 0:
179+
self._write_empty_result(question)
180+
return
181+
182+
model_id = _get_embedding_id(knowledge_id_list)
183+
embedding_model = get_model_instance_by_model_workspace_id(model_id, workspace_id)
184+
embedding_value = embedding_model.embed_query(question)
185+
vector = VectorStore.get_embedding_vector()
186+
187+
exclude_document_id_list = [str(document.id) for document in
188+
QuerySet(Document).filter(knowledge_id__in=knowledge_id_list, is_active=False)]
189+
190+
embedding_list = vector.query(question, embedding_value, knowledge_id_list, document_id_list,
191+
exclude_document_id_list, exclude_paragraph_id_list, True,
192+
knowledge_setting.get('top_n'), knowledge_setting.get('similarity'),
193+
SearchMode(knowledge_setting.get('search_mode')))
194+
195+
connection.close()
196+
197+
if embedding_list is None:
198+
self._write_empty_result(question)
199+
return
200+
201+
paragraph_list = _list_paragraph(embedding_list, vector)
202+
result = [_reset_paragraph(paragraph, embedding_list) for paragraph in paragraph_list]
203+
result = sorted(result, key=lambda p: p.get('similarity'), reverse=True)
204+
205+
self.write_context('paragraph_list', result)
206+
self.write_context('is_hit_handling_method_list',
207+
[row for row in result if row.get('is_hit_handling_method')])
208+
self.write_context('data', '\n'.join(
209+
[f"{_reset_title(paragraph.get('title', ''))}{paragraph.get('content')}" for paragraph in
210+
result])[0:knowledge_setting.get('max_paragraph_char_number', 5000)])
211+
self.write_context('directly_return', '\n'.join(
212+
[paragraph.get('content') for paragraph in result if paragraph.get('is_hit_handling_method')]))
213+
214+
def _write_empty_result(self, question):
215+
self.write_context('paragraph_list', [])
216+
self.write_context('is_hit_handling_method_list', [])
217+
self.write_context('data', '')
218+
self.write_context('directly_return', '')
219+
self.write_context('question', question)
220+
221+
def _get_reference_content(self, fields: List[str]):
222+
if fields:
223+
return self.workflow_manage.get_reference_field(fields[0], fields[1:])
224+
return None

0 commit comments

Comments
 (0)