Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions apps/knowledge/serializers/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,16 +392,34 @@ class Query(serializers.Serializer):
status = serializers.CharField(required=False, label=_('status'), allow_null=True, allow_blank=True)
order_by = serializers.CharField(required=False, label=_('order by'), allow_null=True, allow_blank=True)
tag = serializers.CharField(required=False, label=_('tag'), allow_null=True, allow_blank=True)
tag_ids = serializers.ListField(child=serializers.UUIDField(),allow_null=True,required=False,allow_empty=True)
no_tag = serializers.BooleanField(required=False,default=False, allow_null=True)

def get_query_set(self):
query_set = QuerySet(model=Document)
query_set = query_set.filter(**{'knowledge_id': self.data.get("knowledge_id")})

tag_ids = self.data.get('tag_ids')
no_tag = self.data.get('no_tag')
if 'name' in self.data and self.data.get('name') is not None:
query_set = query_set.filter(**{'name__icontains': self.data.get('name')})
if 'hit_handling_method' in self.data and self.data.get('hit_handling_method') not in [None, '']:
query_set = query_set.filter(**{'hit_handling_method': self.data.get('hit_handling_method')})
if 'is_active' in self.data and self.data.get('is_active') is not None:
query_set = query_set.filter(**{'is_active': self.data.get('is_active')})
if no_tag and tag_ids:
matched_doc_ids = QuerySet(DocumentTag).filter(tag_id__in=tag_ids).values_list('document_id', flat=True)
tagged_doc_ids = QuerySet(DocumentTag).values_list('document_id', flat=True)
query_set = query_set.filter(
Q(id__in=matched_doc_ids) | ~Q(id__in=tagged_doc_ids)
)
elif no_tag:
tagged_doc_ids = QuerySet(DocumentTag).values_list('document_id', flat=True)
query_set = query_set.exclude(id__in=tagged_doc_ids)
elif tag_ids:
matched_doc_ids = QuerySet(DocumentTag).filter(tag_id__in=tag_ids).values_list('document_id', flat=True)
query_set = query_set.filter(id__in=matched_doc_ids)

if 'status' in self.data and self.data.get('status') is not None:
task_type = self.data.get('task_type')
status = self.data.get('status')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the code can be optimized:

  1. Simplify Boolean Logic: Instead of multiple if statements, consider using conditional logic with OR and AND.

  2. Combine Data Retrieval into One Query: Avoid making additional database queries inside loops when possible.

  3. Use More Pythonic Constructs: For example, you could use set operations to optimize filtering more efficiently.

Here's a revised version of the code with some optimizations:

class Query(serializers.Serializer):
    knowledge_id = serializers.CharField(required=False, label=_('knowledge_id'), allow_null=True, allow_blank=True)
    name = serializers.CharField(required=False, label=_('name'), allow_null=True, allow_blank=True)
    hit_handling_method = serializers.CharField(required=False, label=_('hit handling method'), allow_null=True, allow_blank=True)
    is_active = serializers.BooleanField(required=False, label=_('is_active'), allow_null=True)
    order_by = serializers.CharField(required=False, label=_('order by'), allow_null=True, allow_blank=True)
    tag = serializers.CharField(required=False, label=_('tag'), allow_null=True, allow_blank=True)
    tag_ids = serializers.ListField(child=serializers.UUIDField(), required=False, allow_empty=True)
    no_tag = serializers.BooleanField(required=False, default=False, allow_null=True)

    def get_query_set(self):
        query_set = Document.objects.filter(knowledge_id=self.data.get('knowledge_id'))

        if self.data.get('name'):
            query_set = query_set.filter(name__icontains=self.data.get('name'))
        
        if self.data.get('hit_handling_method') not in [None, '']:
            query_set = query_set.filter(hit_handling_method=self.data.get('hit_handling_method'))
        
        if self.data.get('is_active') is not None:
            query_set = query_set.filter(is_active=self.data.get('is_active'))
        
        # Use set intersection or difference based on no_tag and tag_ids
        tagged_doc_ids = list(DocumentTag.objects.values_list('document_id', flat=True))
        no_tag_condition = self.data.get('no_tag')
        tag_ids_condition = self.data.get('tag_ids')

        if tag_ids:
            matched_doc_ids &= set(tag_ids)  
        elif no_tag_condition:
            matched_doc_ids -= set(tagged_doc_ids)
        else:
            matched_doc_ids &= {doc.id for doc in query_set}

        query_set = document_qs.filter(id__in=matched_doc_ids)

        return query_set

Key Changes:

  • Combine data retrieval conditions inside one query.
  • Use set operations (& and -) for efficient filtering based on no_tag.
  • Simplify boolean logic within conditional blocks.

These changes make the logic cleaner and potentially perform better, especially with larger datasets.

Expand Down
27 changes: 22 additions & 5 deletions apps/knowledge/sql/list_document.sql
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
SELECT * from (
SELECT
"document".* ,
to_json("document"."meta") as meta,
to_json("document"."status_meta") as status_meta,
(SELECT "count"("id") FROM "paragraph" WHERE document_id="document"."id") as "paragraph_count"
"document".*,
to_json("document"."meta") as meta,
to_json("document"."status_meta") as status_meta,
(SELECT "count"("id") FROM "paragraph" WHERE document_id = "document"."id") as "paragraph_count",
tag_agg.tag_count as "tag_count",
COALESCE(tag_agg.tags, '[]'::json) as "tags"
FROM
"document" "document"
"document" "document"
LEFT JOIN LATERAL (
SELECT
COUNT(*)::int as tag_count,
json_agg(
json_build_object(
'id', "tag"."id",
'key', "tag"."key",
'value', "tag"."value"
)
ORDER BY "tag"."key", "tag"."value"
) as tags
FROM "document_tag" "document_tag"
INNER JOIN "tag" "tag" ON "tag"."id" = "document_tag"."tag_id"
WHERE "document_tag"."document_id" = "document"."id"
) tag_agg ON TRUE
${document_custom_sql}
) temp
${order_by_query}
4 changes: 4 additions & 0 deletions apps/knowledge/views/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,13 +639,17 @@ class Page(APIView):
[PermissionConstants.KNOWLEDGE.get_workspace_knowledge_permission()], CompareConstants.AND),
)
def get(self, request: Request, workspace_id: str, knowledge_id: str, current_page: int, page_size: int):
raw_tags = request.query_params.getlist("tags[]")

return result.success(DocumentSerializers.Query(
data={
'workspace_id': workspace_id,
'knowledge_id': knowledge_id,
'folder_id': request.query_params.get('folder_id'),
'name': request.query_params.get('name'),
'tag': request.query_params.get('tag'),
'tag_ids': [tag for tag in raw_tags if tag != 'NO_TAG'],
'no_tag': 'NO_TAG' in raw_tags,
'desc': request.query_params.get("desc"),
'user_id': request.query_params.get('user_id'),
'status': request.query_params.get('status'),
Expand Down
1 change: 1 addition & 0 deletions ui/src/locales/lang/en-US/views/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export default {
key: 'Tag',
value: 'Value',
addTag: 'Add Tag',
noTag: 'No Tag',
setting: 'Tag Settings',
create: 'Create Tag',
createValue: 'Create Tag Value',
Expand Down
1 change: 1 addition & 0 deletions ui/src/locales/lang/zh-CN/views/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export default {
key: '标签',
value: '标签值',
addTag: '添加标签',
noTag: '无标签',
addValue: '添加标签值',
setting: '标签设置',
create: '创建标签',
Expand Down
1 change: 1 addition & 0 deletions ui/src/locales/lang/zh-Hant/views/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export default {
label: '標籤管理',
key: '標籤',
value: '標籤值',
noTag: '無標籤',
addTag: '添加標籤',
setting: '標籤設置',
create: '創建標籤',
Expand Down
220 changes: 143 additions & 77 deletions ui/src/views/document/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -210,74 +210,6 @@
:label="$t('views.document.fileStatus.label')"
width="130"
>
<template #header>
<div>
<span>{{ $t('views.document.fileStatus.label') }}</span>
<el-dropdown trigger="click" @command="dropdownHandle">
<el-button
style="margin-top: 1px"
link
:type="filterMethod['status'] ? 'primary' : ''"
>
<el-icon>
<Filter />
</el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu style="width: 100px">
<el-dropdown-item
:class="filterMethod['status'] ? '' : 'is-active'"
:command="beforeCommand('status', '')"
class="justify-center"
>{{ $t('common.status.all') }}
</el-dropdown-item>
<el-dropdown-item
:class="filterMethod['status'] === State.SUCCESS ? 'is-active' : ''"
class="justify-center"
:command="beforeCommand('status', State.SUCCESS)"
>{{ $t('common.status.success') }}
</el-dropdown-item>
<el-dropdown-item
:class="filterMethod['status'] === State.FAILURE ? 'is-active' : ''"
class="justify-center"
:command="beforeCommand('status', State.FAILURE)"
>{{ $t('common.status.fail') }}
</el-dropdown-item>
<el-dropdown-item
:class="
filterMethod['status'] === State.STARTED &&
filterMethod['task_type'] == TaskType.EMBEDDING
? 'is-active'
: ''
"
class="justify-center"
:command="beforeCommand('status', State.STARTED, TaskType.EMBEDDING)"
>{{ $t('views.document.fileStatus.EMBEDDING') }}
</el-dropdown-item>
<el-dropdown-item
:class="filterMethod['status'] === State.PENDING ? 'is-active' : ''"
class="justify-center"
:command="beforeCommand('status', State.PENDING)"
>{{ $t('views.document.fileStatus.PENDING') }}
</el-dropdown-item>
<el-dropdown-item
:class="
filterMethod['status'] === State.STARTED &&
filterMethod['task_type'] === TaskType.GENERATE_PROBLEM
? 'is-active'
: ''
"
class="justify-center"
:command="
beforeCommand('status', State.STARTED, TaskType.GENERATE_PROBLEM)
"
>{{ $t('views.document.fileStatus.GENERATE') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<template #default="{ row }">
<StatusValue :status="row.status" :status-meta="row.status_meta"></StatusValue>
</template>
Expand Down Expand Up @@ -357,6 +289,76 @@
</div>
</template>
</el-table-column>
<el-table-column width="160">
<template #header>
<div>
<span>{{ $t('views.document.tag.label') }}</span>
<el-dropdown trigger="click" @visible-change="handleTagVisibleChange">
<el-button
style="margin-top: 1px"
link
:type="filterMethod['tags']?.length > 0 ? 'primary' : ''"
>
<el-icon>
<Filter />
</el-icon>
</el-button>
<template #dropdown>
<div>
<el-cascader-panel
v-model="tagFilterValue"
:options="tagFilterOptions"
:props="{
multiple: true,
checkStrictly: true,
emitPath: false,
showPrefix: false,
}"
@change="(val: any) => dropdownHandle({ attr: 'tags', command: val })"
/>
</div>
</template>
</el-dropdown>
</div>
</template>
<template #default="{ row }">
<el-popover
trigger="hover"
placement="bottom"
:disabled="!row.tag_count"
:width="160"
>
<div v-for="tag in row.tags" :key="tag.id" flex class="pt-4">
<span class="mr-8 color-input-placeholder">{{ tag.key }}</span
>{{ tag.value }}
</div>

<template #reference>
<el-space :size="4">
<el-button
size="small"
style="padding: 1px 6px"
@click.stop="openTagSettingDrawer(row)"
:disabled="!permissionPrecise.doc_tag(id)"
>
<AppIcon iconName="app-tag"></AppIcon>
<span>{{ row.tag_count || 0 }}</span>
</el-button>
<el-button
size="small"
plain
style="padding: 1px 6px; border-style: dashed"
:disabled="!permissionPrecise.doc_tag(id)"
@click.stop="openAddTagDialog(row.id)"
>
<el-icon class="color-secondary"><Plus /></el-icon>
<span class="color-secondary">{{ $t('views.document.tag.key') }}</span>
</el-button>
</el-space>
</template>
</el-popover>
</template>
</el-table-column>
<el-table-column width="170">
<template #header>
<div>
Expand Down Expand Up @@ -734,15 +736,23 @@
:workspaceId="knowledgeDetail?.workspace_id"
/>
<GenerateRelatedDialog ref="GenerateRelatedDialogRef" @refresh="getList" :apiType="apiType" />
<TagDrawer ref="tagDrawerRef" />
<TagSettingDrawer ref="tagSettingDrawerRef" />
<TagDrawer ref="tagDrawerRef" @tag-changed="onTagChanged" />
<TagSettingDrawer
ref="tagSettingDrawerRef"
@refresh="
() => {
onTagChanged()
getList()
}
"
/>
<AddTagDialog ref="addTagDialogRef" @addTags="addTags" :apiType="apiType" />
<!-- 执行详情 -->
<ExecutionRecord ref="ListActionRef"></ExecutionRecord>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { ref, onMounted, onBeforeUnmount, computed, reactive } from 'vue'
import { useRouter, useRoute, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import type { ElTable } from 'element-plus'
import ImportDocumentDialog from './component/ImportDocumentDialog.vue'
Expand Down Expand Up @@ -1359,6 +1369,61 @@ function openGenerateDialog(row?: any) {
GenerateRelatedDialogRef.value.open(arr, 'document')
}

const tagFilterValue = ref<string[]>([])
const tagFilterDirty = ref(false)
const tagFilterOptions = ref<any[]>([])
const tagFilterLoaded = ref(false)
const tagFilterLoading = ref(false)

function buildTagCascaderOptions(tags: any[]) {
const options = tags.map((group: any) => ({
label: group.key,
value: group.key,
children: (group.values || []).map((item: any) => ({
label: item.value,
value: item.id, // 叶子节点 tag.id
})),
}))

options.push({
label: t('views.document.tag.noTag'),
value: 'NO_TAG',
children: [],
})

return options
}

async function ensureTagFilterOptions(needRefresh = false) {
// 非刷新 && 已加载 && 非脏数据
if (!needRefresh && tagFilterLoaded.value && !tagFilterDirty.value) return

try {
tagFilterLoading.value = true
const params = {}
const res: any = await loadSharedApi({
type: 'knowledge',
systemType: apiType.value,
isShared: isShared.value,
}).getTags(id, params, tagFilterLoading)

tagFilterOptions.value = buildTagCascaderOptions(res?.data || [])
tagFilterLoaded.value = true
tagFilterDirty.value = false
} finally {
tagFilterLoading.value = false
}
}

async function handleTagVisibleChange(visible: boolean) {
if (!visible) return
await ensureTagFilterOptions()
}

function onTagChanged() {
tagFilterDirty.value = true
}

const tagDrawerRef = ref()
function openTagDrawer() {
tagDrawerRef.value.open()
Expand All @@ -1371,13 +1436,14 @@ function openTagSettingDrawer(doc: any) {

const addTagDialogRef = ref()

function openAddTagDialog() {
addTagDialogRef.value?.open()
function openAddTagDialog(rowId?: string) {
addTagDialogRef.value?.open(rowId)
}

function addTags(tags: any) {
const arr: string[] = multipleSelection.value.map((v) => v.id)

function addTags(tags: any, rowId?: string) {
const arr: string[] = multipleSelection.value.length
? multipleSelection.value.map((v) => v.id)
: [rowId]
loadSharedApi({ type: 'document', systemType: apiType.value })
.postMulDocumentTags(id, { tag_ids: tags, document_ids: arr }, loading)
.then(() => {
Expand All @@ -1399,7 +1465,7 @@ onMounted(() => {
}
getList()
// 初始化定时任务
initInterval()
// initInterval()
})

onBeforeUnmount(() => {
Expand Down
Loading