Browse Source

feat: n to 1 retrieval legacy (#6554)

zxhlyh 8 months ago
parent
commit
6fe9aa69cc
35 changed files with 1324 additions and 244 deletions
  1. 11 16
      web/app/components/app/configuration/dataset-config/card-item/item.tsx
  2. 3 2
      web/app/components/app/configuration/dataset-config/index.tsx
  3. 217 37
      web/app/components/app/configuration/dataset-config/params-config/config-content.tsx
  4. 93 26
      web/app/components/app/configuration/dataset-config/params-config/index.tsx
  5. 112 0
      web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx
  6. 11 10
      web/app/components/app/configuration/dataset-config/select-dataset/index.tsx
  7. 2 2
      web/app/components/app/configuration/dataset-config/settings-modal/index.tsx
  8. 47 5
      web/app/components/app/configuration/index.tsx
  9. 25 0
      web/app/components/base/badge.tsx
  10. 152 13
      web/app/components/base/button/index.css
  11. 7 2
      web/app/components/base/button/index.tsx
  12. 11 4
      web/app/components/base/radio-card/index.tsx
  13. 1 1
      web/app/components/base/radio-card/simple/index.tsx
  14. 0 25
      web/app/components/base/radio-card/style.module.css
  15. 16 4
      web/app/components/base/slider/index.tsx
  16. 2 1
      web/app/components/datasets/common/check-rerank-model.ts
  17. 193 37
      web/app/components/datasets/common/retrieval-param-config/index.tsx
  18. 4 0
      web/app/components/datasets/settings/form/index.tsx
  19. 0 1
      web/app/components/tools/add-tool-modal/category.tsx
  20. 9 2
      web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx
  21. 41 20
      web/app/components/workflow/nodes/knowledge-retrieval/components/retrieval-config.tsx
  22. 7 2
      web/app/components/workflow/nodes/knowledge-retrieval/default.ts
  23. 14 0
      web/app/components/workflow/nodes/knowledge-retrieval/hooks.ts
  24. 15 3
      web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx
  25. 17 0
      web/app/components/workflow/nodes/knowledge-retrieval/types.ts
  26. 34 25
      web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts
  27. 121 2
      web/app/components/workflow/nodes/knowledge-retrieval/utils.ts
  28. 2 2
      web/config/index.ts
  29. 5 1
      web/context/debug-configuration.ts
  30. 29 0
      web/hooks/use-knowledge.ts
  31. 23 0
      web/i18n/en-US/dataset.ts
  32. 23 0
      web/i18n/zh-Hans/dataset.ts
  33. 43 0
      web/models/datasets.ts
  34. 18 1
      web/models/debug.ts
  35. 16 0
      web/types/app.ts

+ 11 - 16
web/app/components/app/configuration/dataset-config/card-item/item.tsx

@@ -1,18 +1,20 @@
 'use client'
 import type { FC } from 'react'
 import React, { useState } from 'react'
-import { useTranslation } from 'react-i18next'
-import { RiDeleteBinLine } from '@remixicon/react'
+import {
+  RiDeleteBinLine,
+  RiEditLine,
+} from '@remixicon/react'
 import SettingsModal from '../settings-modal'
 import type { DataSet } from '@/models/datasets'
 import { DataSourceType } from '@/models/datasets'
-import { formatNumber } from '@/utils/format'
 import FileIcon from '@/app/components/base/file-icon'
-import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
 import { Folder } from '@/app/components/base/icons/src/vender/solid/files'
 import { Globe06 } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
 import Drawer from '@/app/components/base/drawer'
 import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
+import Badge from '@/app/components/base/badge'
+import { useKnowledge } from '@/hooks/use-knowledge'
 
 type ItemProps = {
   className?: string
@@ -27,12 +29,10 @@ const Item: FC<ItemProps> = ({
   onSave,
   onRemove,
 }) => {
-  const { t } = useTranslation()
-
   const media = useBreakpoints()
   const isMobile = media === MediaType.mobile
-
   const [showSettingsModal, setShowSettingsModal] = useState(false)
+  const { formatIndexingTechniqueAndMethod } = useKnowledge()
 
   const handleSave = (newDataset: DataSet) => {
     onSave(newDataset)
@@ -65,22 +65,17 @@ const Item: FC<ItemProps> = ({
       <div className='grow'>
         <div className='flex items-center h-[18px]'>
           <div className='grow text-[13px] font-medium text-gray-800 truncate' title={config.name}>{config.name}</div>
-          <div className='shrink-0 text-xs text-gray-500'>
-            {formatNumber(config.word_count)} {t('appDebug.feature.dataSet.words')} · {formatNumber(config.document_count)} {t('appDebug.feature.dataSet.textBlocks')}
-          </div>
+          <Badge
+            text={formatIndexingTechniqueAndMethod(config.indexing_technique, config.retrieval_model_dict?.search_method)}
+          />
         </div>
-        {/* {
-          config.description && (
-            <div className='text-xs text-gray-500'>{config.description}</div>
-          )
-        } */}
       </div>
       <div className='hidden rounded-lg group-hover:flex items-center justify-end absolute right-0 top-0 bottom-0 pr-2 w-[124px] bg-gradient-to-r from-white/50 to-white to-50%'>
         <div
           className='flex items-center justify-center mr-1 w-6 h-6 hover:bg-black/5 rounded-md cursor-pointer'
           onClick={() => setShowSettingsModal(true)}
         >
-          <Settings01 className='w-4 h-4 text-gray-500' />
+          <RiEditLine className='w-4 h-4 text-gray-500' />
         </div>
         <div
           className='group/action flex items-center justify-center w-6 h-6 hover:bg-[#FEE4E2] rounded-md cursor-pointer'

+ 3 - 2
web/app/components/app/configuration/dataset-config/index.tsx

@@ -44,7 +44,8 @@ const DatasetConfig: FC = () => {
   const handleSave = (newDataset: DataSet) => {
     const index = dataSet.findIndex(item => item.id === newDataset.id)
 
-    setDataSet([...dataSet.slice(0, index), newDataset, ...dataSet.slice(index + 1)])
+    const newDatasets = [...dataSet.slice(0, index), newDataset, ...dataSet.slice(index + 1)]
+    setDataSet(newDatasets)
     formattingChangedDispatcher()
   }
 
@@ -74,7 +75,7 @@ const DatasetConfig: FC = () => {
       title={t('appDebug.feature.dataSet.title')}
       headerRight={
         <div className='flex items-center gap-1'>
-          {!isAgent && <ParamsConfig />}
+          {!isAgent && <ParamsConfig disabled={!hasData} selectedDatasets={dataSet} />}
           <OperationBtn type="add" onClick={showSelectDataSet} />
         </div>
       }

+ 217 - 37
web/app/components/app/configuration/dataset-config/params-config/config-content.tsx

@@ -1,10 +1,12 @@
 'use client'
-import React from 'react'
+
+import { memo, useMemo } from 'react'
 import type { FC } from 'react'
 import { useTranslation } from 'react-i18next'
 import {
   RiQuestionLine,
 } from '@remixicon/react'
+import WeightedScore from './weighted-score'
 import TopKItem from '@/app/components/base/param-item/top-k-item'
 import ScoreThresholdItem from '@/app/components/base/param-item/score-threshold-item'
 import RadioCard from '@/app/components/base/radio-card/simple'
@@ -16,13 +18,20 @@ import {
 import type {
   DatasetConfigs,
 } from '@/models/debug'
-
 import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
 import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
 import type { ModelConfig } from '@/app/components/workflow/types'
 import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
 import TooltipPlus from '@/app/components/base/tooltip-plus'
 import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
+import type {
+  DataSet,
+  WeightedScoreEnum,
+} from '@/models/datasets'
+import { RerankingModeEnum } from '@/models/datasets'
+import cn from '@/utils/classnames'
+import { useSelectedDatasetsMode } from '@/app/components/workflow/nodes/knowledge-retrieval/hooks'
+import Switch from '@/app/components/base/switch'
 
 type Props = {
   datasetConfigs: DatasetConfigs
@@ -31,6 +40,7 @@ type Props = {
   singleRetrievalModelConfig?: ModelConfig
   onSingleRetrievalModelChange?: (config: ModelConfig) => void
   onSingleRetrievalModelParamsChange?: (config: ModelConfig) => void
+  selectedDatasets?: DataSet[]
 }
 
 const ConfigContent: FC<Props> = ({
@@ -40,8 +50,10 @@ const ConfigContent: FC<Props> = ({
   singleRetrievalModelConfig: singleRetrievalConfig = {} as ModelConfig,
   onSingleRetrievalModelChange = () => { },
   onSingleRetrievalModelParamsChange = () => { },
+  selectedDatasets = [],
 }) => {
   const { t } = useTranslation()
+  const selectedDatasetsMode = useSelectedDatasetsMode(selectedDatasets)
   const type = datasetConfigs.retrieval_model
   const setType = (value: RETRIEVE_TYPE) => {
     onChange({
@@ -54,7 +66,7 @@ const ConfigContent: FC<Props> = ({
     defaultModel: rerankDefaultModel,
   } = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank)
   const rerankModel = (() => {
-    if (datasetConfigs.reranking_model) {
+    if (datasetConfigs.reranking_model?.reranking_provider_name) {
       return {
         provider_name: datasetConfigs.reranking_model.reranking_provider_name,
         model_name: datasetConfigs.reranking_model.reranking_model_name,
@@ -93,14 +105,73 @@ const ConfigContent: FC<Props> = ({
     })
   }
 
+  const handleWeightedScoreChange = (value: { type: WeightedScoreEnum; value: number[] }) => {
+    const configs = {
+      ...datasetConfigs,
+      weights: {
+        ...datasetConfigs.weights!,
+        weight_type: value.type,
+        vector_setting: {
+          ...datasetConfigs.weights!.vector_setting!,
+          vector_weight: value.value[0],
+        },
+        keyword_setting: {
+          keyword_weight: value.value[1],
+        },
+      },
+    }
+    onChange(configs)
+  }
+
+  const handleRerankModeChange = (mode: RerankingModeEnum) => {
+    onChange({
+      ...datasetConfigs,
+      reranking_mode: mode,
+    })
+  }
+
   const model = singleRetrievalConfig
 
+  const rerankingModeOptions = [
+    {
+      value: RerankingModeEnum.WeightedScore,
+      label: t('dataset.weightedScore.title'),
+      tips: t('dataset.weightedScore.description'),
+    },
+    {
+      value: RerankingModeEnum.RerankingModel,
+      label: t('common.modelProvider.rerankModel.key'),
+      tips: t('common.modelProvider.rerankModel.tip'),
+    },
+  ]
+
+  const showWeightedScore = selectedDatasetsMode.allHighQuality
+    && !selectedDatasetsMode.inconsistentEmbeddingModel
+
+  const showWeightedScorePanel = showWeightedScore && datasetConfigs.reranking_mode === RerankingModeEnum.WeightedScore && datasetConfigs.weights
+  const selectedRerankMode = datasetConfigs.reranking_mode || RerankingModeEnum.RerankingModel
+
+  const showRerankModel = useMemo(() => {
+    if (datasetConfigs.reranking_enable === false && selectedDatasetsMode.allEconomic)
+      return false
+
+    return true
+  }, [datasetConfigs.reranking_enable, selectedDatasetsMode.allEconomic])
+
   return (
     <div>
+      <div className='system-xl-semibold text-text-primary'>{t('dataset.retrievalSettings')}</div>
       <div className='mt-2 space-y-3'>
         <RadioCard
           icon={<NTo1Retrieval className='shrink-0 mr-3 w-9 h-9 rounded-lg' />}
-          title={t('appDebug.datasetConfig.retrieveOneWay.title')}
+          title={(
+            <div className='flex items-center'>
+              {t('appDebug.datasetConfig.retrieveOneWay.title')}
+              <TooltipPlus popupContent={<div className='w-[320px]'>{t('dataset.nTo1RetrievalLegacy')}</div>}>
+                <div className='ml-1 flex items-center px-[5px] h-[18px] rounded-[5px] border border-text-accent-secondary system-2xs-medium-uppercase text-text-accent-secondary'>legacy</div>
+              </TooltipPlus>
+            </div>
+          )}
           description={t('appDebug.datasetConfig.retrieveOneWay.description')}
           isChosen={type === RETRIEVE_TYPE.oneWay}
           onChosen={() => { setType(RETRIEVE_TYPE.oneWay) }}
@@ -115,43 +186,152 @@ const ConfigContent: FC<Props> = ({
       </div>
       {type === RETRIEVE_TYPE.multiWay && (
         <>
-          <div className='mt-6'>
-            <div className='leading-[32px] text-[13px] font-medium text-gray-900'>{t('common.modelProvider.rerankModel.key')}</div>
-            <div>
-              <ModelSelector
-                defaultModel={rerankModel && { provider: rerankModel?.provider_name, model: rerankModel?.model_name }}
-                onSelect={(v) => {
-                  onChange({
-                    ...datasetConfigs,
-                    reranking_model: {
-                      reranking_provider_name: v.provider,
-                      reranking_model_name: v.model,
-                    },
-                  })
-                }}
-                modelList={rerankModelList}
-              />
-            </div>
-          </div>
-          <div className='mt-4 space-y-4'>
-            <TopKItem
-              value={datasetConfigs.top_k}
-              onChange={handleParamChange}
-              enable={true}
-            />
-            <ScoreThresholdItem
-              value={datasetConfigs.score_threshold as number}
-              onChange={handleParamChange}
-              enable={datasetConfigs.score_threshold_enabled}
-              hasSwitch={true}
-              onSwitchChange={handleSwitch}
-            />
+          <div className='mb-2 mt-4 h-[1px] bg-divider-subtle'></div>
+          <div
+            className='flex items-center mb-2 h-6 system-md-semibold text-text-secondary'
+          >
+            {t('dataset.rerankSettings')}
           </div>
+          {
+            selectedDatasetsMode.inconsistentEmbeddingModel
+            && (
+              <div className='mt-4 system-xs-regular text-text-warning'>
+                {t('dataset.inconsistentEmbeddingModelTip')}
+              </div>
+            )
+          }
+          {
+            selectedDatasetsMode.mixtureHighQualityAndEconomic
+            && (
+              <div className='mt-4 system-xs-regular text-text-warning'>
+                {t('dataset.mixtureHighQualityAndEconomicTip')}
+              </div>
+            )
+          }
+          {
+            showWeightedScore && (
+              <div className='flex items-center justify-between'>
+                {
+                  rerankingModeOptions.map(option => (
+                    <div
+                      key={option.value}
+                      className={cn(
+                        'flex items-center justify-center w-[calc((100%-8px)/2)] h-8 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg cursor-pointer system-sm-medium text-text-secondary',
+                        selectedRerankMode === option.value && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary',
+                      )}
+                      onClick={() => handleRerankModeChange(option.value)}
+                    >
+                      <div className='truncate'>{option.label}</div>
+                      <TooltipPlus
+                        popupContent={<div className='w-[200px]'>{option.tips}</div>}
+                        hideArrow
+                      >
+                        <RiQuestionLine className='ml-0.5 w-3.5 h-4.5 text-text-quaternary' />
+                      </TooltipPlus>
+                    </div>
+                  ))
+                }
+              </div>
+            )
+          }
+          {
+            !showWeightedScorePanel && (
+              <div className='mt-2'>
+                <div className='flex items-center'>
+                  {
+                    selectedDatasetsMode.allEconomic && (
+                      <Switch
+                        size='md'
+                        defaultValue={showRerankModel}
+                        onChange={(v) => {
+                          onChange({
+                            ...datasetConfigs,
+                            reranking_enable: v,
+                          })
+                        }}
+                      />
+                    )
+                  }
+                  <div className='ml-2 leading-[32px] text-[13px] font-medium text-gray-900'>{t('common.modelProvider.rerankModel.key')}</div>
+                  <TooltipPlus popupContent={<div className="w-[200px]">{t('common.modelProvider.rerankModel.tip')}</div>}>
+                    <RiQuestionLine className='ml-0.5 w-[14px] h-[14px] text-gray-400' />
+                  </TooltipPlus>
+                </div>
+                <div>
+                  <ModelSelector
+                    defaultModel={rerankModel && { provider: rerankModel?.provider_name, model: rerankModel?.model_name }}
+                    onSelect={(v) => {
+                      onChange({
+                        ...datasetConfigs,
+                        reranking_model: {
+                          reranking_provider_name: v.provider,
+                          reranking_model_name: v.model,
+                        },
+                      })
+                    }}
+                    modelList={rerankModelList}
+                  />
+                </div>
+              </div>
+            )
+          }
+          {
+            showWeightedScorePanel
+            && (
+              <div className='mt-2 space-y-4'>
+                <WeightedScore
+                  value={{
+                    type: datasetConfigs.weights!.weight_type,
+                    value: [
+                      datasetConfigs.weights!.vector_setting.vector_weight,
+                      datasetConfigs.weights!.keyword_setting.keyword_weight,
+                    ],
+                  }}
+                  onChange={handleWeightedScoreChange}
+                />
+                <TopKItem
+                  value={datasetConfigs.top_k}
+                  onChange={handleParamChange}
+                  enable={true}
+                />
+                <ScoreThresholdItem
+                  value={datasetConfigs.score_threshold as number}
+                  onChange={handleParamChange}
+                  enable={datasetConfigs.score_threshold_enabled}
+                  hasSwitch={true}
+                  onSwitchChange={handleSwitch}
+                />
+              </div>
+            )
+          }
+          {
+            !showWeightedScorePanel
+            && (
+              <div className='mt-4 space-y-4'>
+                <TopKItem
+                  value={datasetConfigs.top_k}
+                  onChange={handleParamChange}
+                  enable={true}
+                />
+                {
+                  showRerankModel && (
+                    <ScoreThresholdItem
+                      value={datasetConfigs.score_threshold as number}
+                      onChange={handleParamChange}
+                      enable={datasetConfigs.score_threshold_enabled}
+                      hasSwitch={true}
+                      onSwitchChange={handleSwitch}
+                    />
+                  )
+                }
+              </div>
+            )
+          }
         </>
       )}
 
       {isInWorkflow && type === RETRIEVE_TYPE.oneWay && (
-        <div className='mt-6'>
+        <div className='mt-4'>
           <div className='flex items-center space-x-0.5'>
             <div className='leading-[32px] text-[13px] font-medium text-gray-900'>{t('common.modelProvider.systemReasoningModel.key')}</div>
             <TooltipPlus
@@ -180,4 +360,4 @@ const ConfigContent: FC<Props> = ({
     </div >
   )
 }
-export default React.memo(ConfigContent)
+export default memo(ConfigContent)

+ 93 - 26
web/app/components/app/configuration/dataset-config/params-config/index.tsx

@@ -1,29 +1,73 @@
 'use client'
-import type { FC } from 'react'
-import { memo, useState } from 'react'
+import { memo, useEffect, useState } from 'react'
 import { useTranslation } from 'react-i18next'
 import { useContext } from 'use-context-selector'
+import { RiEqualizer2Line } from '@remixicon/react'
 import ConfigContent from './config-content'
 import cn from '@/utils/classnames'
-import { Settings04 } from '@/app/components/base/icons/src/vender/line/general'
 import ConfigContext from '@/context/debug-configuration'
 import Modal from '@/app/components/base/modal'
 import Button from '@/app/components/base/button'
 import { RETRIEVE_TYPE } from '@/types/app'
 import Toast from '@/app/components/base/toast'
-import { DATASET_DEFAULT } from '@/config'
 import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
 import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
+import type { DataSet } from '@/models/datasets'
+import type { DatasetConfigs } from '@/models/debug'
+import {
+  getMultipleRetrievalConfig,
+  getSelectedDatasetsMode,
+} from '@/app/components/workflow/nodes/knowledge-retrieval/utils'
 
-const ParamsConfig: FC = () => {
+type ParamsConfigProps = {
+  disabled?: boolean
+  selectedDatasets: DataSet[]
+}
+const ParamsConfig = ({
+  disabled,
+  selectedDatasets,
+}: ParamsConfigProps) => {
   const { t } = useTranslation()
-  const [open, setOpen] = useState(false)
   const {
     datasetConfigs,
     setDatasetConfigs,
+    rerankSettingModalOpen,
+    setRerankSettingModalOpen,
   } = useContext(ConfigContext)
   const [tempDataSetConfigs, setTempDataSetConfigs] = useState(datasetConfigs)
 
+  useEffect(() => {
+    const {
+      allEconomic,
+    } = getSelectedDatasetsMode(selectedDatasets)
+    const { datasets, retrieval_model, score_threshold_enabled, ...restConfigs } = datasetConfigs
+    let rerankEnable = restConfigs.reranking_enable
+
+    if (allEconomic && !restConfigs.reranking_model?.reranking_provider_name && rerankEnable === undefined)
+      rerankEnable = false
+
+    setTempDataSetConfigs({
+      ...getMultipleRetrievalConfig({
+        top_k: restConfigs.top_k,
+        score_threshold: restConfigs.score_threshold,
+        reranking_model: restConfigs.reranking_model && {
+          provider: restConfigs.reranking_model.reranking_provider_name,
+          model: restConfigs.reranking_model.reranking_model_name,
+        },
+        reranking_mode: restConfigs.reranking_mode,
+        weights: restConfigs.weights,
+        reranking_enable: rerankEnable,
+      }, selectedDatasets),
+      reranking_model: restConfigs.reranking_model && {
+        reranking_provider_name: restConfigs.reranking_model.reranking_provider_name,
+        reranking_model_name: restConfigs.reranking_model.reranking_model_name,
+      },
+      retrieval_model,
+      score_threshold_enabled,
+      datasets,
+    })
+  }, [selectedDatasets, datasetConfigs])
+
   const {
     defaultModel: rerankDefaultModel,
     currentModel: isRerankDefaultModelVaild,
@@ -55,45 +99,68 @@ const ParamsConfig: FC = () => {
       } as any
     }
     setDatasetConfigs(config)
-    setOpen(false)
+    setRerankSettingModalOpen(false)
+  }
+
+  const handleSetTempDataSetConfigs = (newDatasetConfigs: DatasetConfigs) => {
+    const { datasets, retrieval_model, score_threshold_enabled, ...restConfigs } = newDatasetConfigs
+
+    const retrievalConfig = getMultipleRetrievalConfig({
+      top_k: restConfigs.top_k,
+      score_threshold: restConfigs.score_threshold,
+      reranking_model: restConfigs.reranking_model && {
+        provider: restConfigs.reranking_model.reranking_provider_name,
+        model: restConfigs.reranking_model.reranking_model_name,
+      },
+      reranking_mode: restConfigs.reranking_mode,
+      weights: restConfigs.weights,
+      reranking_enable: restConfigs.reranking_enable,
+    }, selectedDatasets)
+
+    setTempDataSetConfigs({
+      ...retrievalConfig,
+      reranking_model: restConfigs.reranking_model && {
+        reranking_provider_name: restConfigs.reranking_model.reranking_provider_name,
+        reranking_model_name: restConfigs.reranking_model.reranking_model_name,
+      },
+      retrieval_model,
+      score_threshold_enabled,
+      datasets,
+    })
   }
 
   return (
     <div>
-      <div
-        className={cn('flex items-center rounded-md h-7 px-3 space-x-1 text-gray-700 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}
+      <Button
+        variant='ghost'
+        size='small'
+        className={cn('h-7', rerankSettingModalOpen && 'bg-components-button-ghost-bg-hover')}
         onClick={() => {
-          setTempDataSetConfigs({
-            ...datasetConfigs,
-            top_k: datasetConfigs.top_k || DATASET_DEFAULT.top_k,
-            score_threshold: datasetConfigs.score_threshold || DATASET_DEFAULT.score_threshold,
-          })
-          setOpen(true)
+          setRerankSettingModalOpen(true)
         }}
+        disabled={disabled}
       >
-        <Settings04 className="w-[14px] h-[14px]" />
-        <div className='text-xs font-medium'>
-          {t('appDebug.datasetConfig.params')}
-        </div>
-      </div>
+        <RiEqualizer2Line className='mr-1 w-3.5 h-3.5' />
+        {t('dataset.retrievalSettings')}
+      </Button>
       {
-        open && (
+        rerankSettingModalOpen && (
           <Modal
-            isShow={open}
+            isShow={rerankSettingModalOpen}
             onClose={() => {
-              setOpen(false)
+              setRerankSettingModalOpen(false)
             }}
             className='sm:min-w-[528px]'
-            title={t('appDebug.datasetConfig.settingTitle')}
           >
             <ConfigContent
               datasetConfigs={tempDataSetConfigs}
-              onChange={setTempDataSetConfigs}
+              onChange={handleSetTempDataSetConfigs}
+              selectedDatasets={selectedDatasets}
             />
 
             <div className='mt-6 flex justify-end'>
               <Button className='mr-2 flex-shrink-0' onClick={() => {
-                setOpen(false)
+                setRerankSettingModalOpen(false)
               }}>{t('common.operation.cancel')}</Button>
               <Button variant='primary' className='flex-shrink-0' onClick={handleSave} >{t('common.operation.save')}</Button>
             </div>

+ 112 - 0
web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx

@@ -0,0 +1,112 @@
+import { memo, useCallback } from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+  DEFAULT_WEIGHTED_SCORE,
+  WeightedScoreEnum,
+} from '@/models/datasets'
+import Slider from '@/app/components/base/slider'
+import cn from '@/utils/classnames'
+
+const formatNumber = (value: number) => {
+  if (value > 0 && value < 1)
+    return `0.${value * 10}`
+  else if (value === 1)
+    return '1.0'
+
+  return value
+}
+
+type Value = {
+  type: WeightedScoreEnum
+  value: number[]
+}
+
+type WeightedScoreProps = {
+  value: Value
+  onChange: (value: Value) => void
+}
+const WeightedScore = ({
+  value,
+  onChange = () => {},
+}: WeightedScoreProps) => {
+  const { t } = useTranslation()
+  const options = [
+    {
+      value: WeightedScoreEnum.SemanticFirst,
+      label: t('dataset.weightedScore.semanticFirst'),
+    },
+    {
+      value: WeightedScoreEnum.KeywordFirst,
+      label: t('dataset.weightedScore.keywordFirst'),
+    },
+    {
+      value: WeightedScoreEnum.Customized,
+      label: t('dataset.weightedScore.customized'),
+    },
+  ]
+
+  const disabled = value.type !== WeightedScoreEnum.Customized
+
+  const handleTypeChange = useCallback((type: WeightedScoreEnum) => {
+    const result = { ...value, type }
+
+    if (type === WeightedScoreEnum.SemanticFirst)
+      result.value = [DEFAULT_WEIGHTED_SCORE.semanticFirst.semantic, DEFAULT_WEIGHTED_SCORE.semanticFirst.keyword]
+
+    if (type === WeightedScoreEnum.KeywordFirst)
+      result.value = [DEFAULT_WEIGHTED_SCORE.keywordFirst.semantic, DEFAULT_WEIGHTED_SCORE.keywordFirst.keyword]
+
+    onChange(result)
+  }, [value, onChange])
+
+  return (
+    <div>
+      <div className='flex items-center mb-1 space-x-4'>
+        {
+          options.map(option => (
+            <div
+              key={option.value}
+              className='flex py-1.5 max-w-[calc((100%-32px)/3)] system-sm-regular text-text-secondary cursor-pointer'
+              onClick={() => handleTypeChange(option.value)}
+            >
+              <div
+                className={cn(
+                  'shrink-0 mr-2 w-4 h-4 bg-components-radio-bg border border-components-radio-border rounded-full shadow-xs',
+                  value.type === option.value && 'border-[5px] border-components-radio-border-checked',
+                )}
+              ></div>
+              <div className='truncate' title={option.label}>{option.label}</div>
+            </div>
+          ))
+        }
+      </div>
+      <div className='flex items-center px-3 h-9 space-x-3 rounded-lg border border-components-panel-border'>
+        <div className='shrink-0 flex items-center w-[90px] system-xs-semibold-uppercase text-util-colors-blue-blue-500'>
+          <div className='mr-1 truncate uppercase' title={t('dataset.weightedScore.semantic') || ''}>
+            {t('dataset.weightedScore.semantic')}
+          </div>
+          {formatNumber(value.value[0])}
+        </div>
+        <Slider
+          className={cn('grow h-0.5 bg-gradient-to-r from-[#53B1FD] to-[#2ED3B7]', disabled && 'cursor-not-allowed')}
+          max={1.0}
+          min={0}
+          step={0.1}
+          value={value.value[0]}
+          onChange={v => onChange({ type: value.type, value: [v, (10 - v * 10) / 10] })}
+          disabled={disabled}
+          thumbClassName={cn(disabled && '!cursor-not-allowed')}
+          trackClassName='!bg-transparent'
+        />
+        <div className='shrink-0 flex items-center justify-end w-[90px] system-xs-semibold-uppercase text-util-colors-cyan-cyan-500'>
+          {formatNumber(value.value[1])}
+          <div className='ml-1 truncate uppercase' title={t('dataset.weightedScore.keyword') || ''}>
+            {t('dataset.weightedScore.keyword')}
+          </div>
+        </div>
+      </div>
+    </div>
+  )
+}
+
+export default memo(WeightedScore)

+ 11 - 10
web/app/components/app/configuration/dataset-config/select-dataset/index.tsx

@@ -13,7 +13,8 @@ import type { DataSet } from '@/models/datasets'
 import Button from '@/app/components/base/button'
 import { fetchDatasets } from '@/service/datasets'
 import Loading from '@/app/components/base/loading'
-import { formatNumber } from '@/utils/format'
+import Badge from '@/app/components/base/badge'
+import { useKnowledge } from '@/hooks/use-knowledge'
 
 export type ISelectDataSetProps = {
   isShow: boolean
@@ -38,6 +39,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
   const listRef = useRef<HTMLDivElement>(null)
   const [page, setPage, getPage] = useGetState(1)
   const [isNoMore, setIsNoMore] = useState(false)
+  const { formatIndexingTechniqueAndMethod } = useKnowledge()
 
   useInfiniteScroll(
     async () => {
@@ -45,7 +47,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
         const { data, has_more } = await fetchDatasets({ url: '/datasets', params: { page } })
         setPage(getPage() + 1)
         setIsNoMore(!has_more)
-        const newList = [...(datasets || []), ...data]
+        const newList = [...(datasets || []), ...data.filter(item => item.indexing_technique)]
         setDataSets(newList)
         setLoaded(true)
         if (!selected.find(item => !item.name))
@@ -136,14 +138,13 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
                     <span className='ml-1 shrink-0 px-1 border boder-gray-200 rounded-md text-gray-500 text-xs font-normal leading-[18px]'>{t('dataset.unavailable')}</span>
                   )}
                 </div>
-
-                <div className={cn('shrink-0 flex text-xs text-gray-500 overflow-hidden whitespace-nowrap', !item.embedding_available && 'opacity-50')}>
-                  <span className='max-w-[100px] overflow-hidden text-ellipsis whitespace-nowrap'>{formatNumber(item.word_count)}</span>
-                  {t('appDebug.feature.dataSet.words')}
-                  <span className='px-0.5'>·</span>
-                  <span className='max-w-[100px] min-w-[8px] overflow-hidden text-ellipsis whitespace-nowrap'>{formatNumber(item.document_count)} </span>
-                  {t('appDebug.feature.dataSet.textBlocks')}
-                </div>
+                {
+                  item.indexing_technique && (
+                    <Badge
+                      text={formatIndexingTechniqueAndMethod(item.indexing_technique, item.retrieval_model_dict?.search_method)}
+                    />
+                  )
+                }
               </div>
             ))}
           </div>

+ 2 - 2
web/app/components/app/configuration/dataset-config/settings-modal/index.tsx

@@ -259,7 +259,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
 
         {/* Retrieval Method Config */}
         <div className={rowClass}>
-          <div className={labelClass}>
+          <div className={cn(labelClass, 'w-auto min-w-[168px]')}>
             <div>
               <div>{t('datasetSettings.form.retrievalSetting.title')}</div>
               <div className='leading-[18px] text-xs font-normal text-gray-500'>
@@ -268,7 +268,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
               </div>
             </div>
           </div>
-          <div className='w-[480px]'>
+          <div>
             {indexMethod === 'high_quality'
               ? (
                 <RetrievalMethodConfig

+ 47 - 5
web/app/components/app/configuration/index.tsx

@@ -46,7 +46,7 @@ import { fetchDatasets } from '@/service/datasets'
 import { useProviderContext } from '@/context/provider-context'
 import { AgentStrategy, AppType, ModelModeType, RETRIEVE_TYPE, Resolution, TransferMethod } from '@/types/app'
 import { PromptMode } from '@/models/debug'
-import { ANNOTATION_DEFAULT, DEFAULT_AGENT_SETTING, DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
+import { ANNOTATION_DEFAULT, DATASET_DEFAULT, DEFAULT_AGENT_SETTING, DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
 import SelectDataSet from '@/app/components/app/configuration/dataset-config/select-dataset'
 import { useModalContext } from '@/context/modal-context'
 import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
@@ -57,6 +57,10 @@ import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/comp
 import { fetchCollectionList } from '@/service/tools'
 import { type Collection } from '@/app/components/tools/types'
 import { useStore as useAppStore } from '@/app/components/app/store'
+import {
+  getMultipleRetrievalConfig,
+  getSelectedDatasetsMode,
+} from '@/app/components/workflow/nodes/knowledge-retrieval/utils'
 
 type PublishConfig = {
   modelConfig: ModelConfig
@@ -174,14 +178,14 @@ const Configuration: FC = () => {
 
   }, [])
   const [datasetConfigs, setDatasetConfigs] = useState<DatasetConfigs>({
-    retrieval_model: RETRIEVE_TYPE.oneWay,
+    retrieval_model: RETRIEVE_TYPE.multiWay,
     reranking_model: {
       reranking_provider_name: '',
       reranking_model_name: '',
     },
-    top_k: 2,
+    top_k: DATASET_DEFAULT.top_k,
     score_threshold_enabled: false,
-    score_threshold: 0.7,
+    score_threshold: DATASET_DEFAULT.score_threshold,
     datasets: {
       datasets: [],
     },
@@ -202,6 +206,7 @@ const Configuration: FC = () => {
   const hasSetContextVar = !!contextVar
   const [isShowSelectDataSet, { setTrue: showSelectDataSet, setFalse: hideSelectDataSet }] = useBoolean(false)
   const selectedIds = dataSets.map(item => item.id)
+  const [rerankSettingModalOpen, setRerankSettingModalOpen] = useState(false)
   const handleSelect = (data: DataSet[]) => {
     if (isEqual(data.map(item => item.id), dataSets.map(item => item.id))) {
       hideSelectDataSet()
@@ -209,6 +214,7 @@ const Configuration: FC = () => {
     }
 
     formattingChangedDispatcher()
+    let newDatasets = data
     if (data.find(item => !item.name)) { // has not loaded selected dataset
       const newSelected = produce(data, (draft: any) => {
         data.forEach((item, index) => {
@@ -220,11 +226,45 @@ const Configuration: FC = () => {
         })
       })
       setDataSets(newSelected)
+      newDatasets = newSelected
     }
     else {
       setDataSets(data)
     }
     hideSelectDataSet()
+    const {
+      allEconomic,
+      mixtureHighQualityAndEconomic,
+      inconsistentEmbeddingModel,
+    } = getSelectedDatasetsMode(newDatasets)
+
+    if (allEconomic || mixtureHighQualityAndEconomic || inconsistentEmbeddingModel)
+      setRerankSettingModalOpen(true)
+
+    const { datasets, retrieval_model, score_threshold_enabled, ...restConfigs } = datasetConfigs
+
+    const retrievalConfig = getMultipleRetrievalConfig({
+      top_k: restConfigs.top_k,
+      score_threshold: restConfigs.score_threshold,
+      reranking_model: restConfigs.reranking_model && {
+        provider: restConfigs.reranking_model.reranking_provider_name,
+        model: restConfigs.reranking_model.reranking_model_name,
+      },
+      reranking_mode: restConfigs.reranking_mode,
+      weights: restConfigs.weights,
+      reranking_enable: restConfigs.reranking_enable,
+    }, newDatasets)
+
+    setDatasetConfigs({
+      ...retrievalConfig,
+      reranking_model: restConfigs.reranking_model && {
+        reranking_provider_name: restConfigs.reranking_model.reranking_provider_name,
+        reranking_model_name: restConfigs.reranking_model.reranking_model_name,
+      },
+      retrieval_model,
+      score_threshold_enabled,
+      datasets,
+    })
   }
 
   const [isShowHistoryModal, { setTrue: showHistoryModal, setFalse: hideHistoryModal }] = useBoolean(false)
@@ -509,7 +549,7 @@ const Configuration: FC = () => {
         syncToPublishedConfig(config)
         setPublishedConfig(config)
         setDatasetConfigs({
-          retrieval_model: RETRIEVE_TYPE.oneWay,
+          retrieval_model: RETRIEVE_TYPE.multiWay,
           ...modelConfig.dataset_configs,
         })
         setHasFetchedDetail(true)
@@ -744,6 +784,8 @@ const Configuration: FC = () => {
       isShowVisionConfig,
       visionConfig,
       setVisionConfig: handleSetVisionConfig,
+      rerankSettingModalOpen,
+      setRerankSettingModalOpen,
     }}
     >
       <>

+ 25 - 0
web/app/components/base/badge.tsx

@@ -0,0 +1,25 @@
+import { memo } from 'react'
+import cn from '@/utils/classnames'
+
+type BadgeProps = {
+  className?: string
+  text: string
+}
+
+const Badge = ({
+  className,
+  text,
+}: BadgeProps) => {
+  return (
+    <div
+      className={cn(
+        'inline-flex items-center px-[5px] h-5 rounded-[5px] border border-divider-deep system-2xs-medium-uppercase leading-3 text-text-tertiary',
+        className,
+      )}
+    >
+      {text}
+    </div>
+  )
+}
+
+export default memo(Badge)

+ 152 - 13
web/app/components/base/button/index.css

@@ -2,46 +2,185 @@
 
 @layer components {
   .btn {
-    @apply inline-flex justify-center items-center border-[0.5px] font-medium cursor-pointer whitespace-nowrap shadow;
+    @apply inline-flex justify-center items-center cursor-pointer whitespace-nowrap;
   }
 
   .btn-disabled {
-    @apply opacity-60 cursor-not-allowed;
+    @apply cursor-not-allowed;
   }
 
   .btn-small {
-    @apply px-2 h-6 rounded-md text-xs
+    @apply px-2 h-6 rounded-md text-xs font-medium;
   }
 
   .btn-medium {
-    @apply px-3.5 h-8 rounded-lg text-[13px]
+    @apply px-3.5 h-8 rounded-lg text-[13px] leading-4 font-medium;
   }
 
   .btn-large {
-    @apply px-4 h-9 rounded-[10px] text-sm font-semibold
+    @apply px-4 h-9 rounded-[10px] text-sm font-semibold;
+  }
+
+  .btn-primary {
+    @apply 
+    shadow
+    bg-components-button-primary-bg
+    border-components-button-primary-border
+    hover:bg-components-button-primary-bg-hover
+    hover:border-components-button-primary-border-hover
+    text-components-button-primary-text;
+  }
+
+  .btn-primary.btn-destructive {
+    @apply 
+    bg-components-button-destructive-primary-bg
+    border-components-button-destructive-primary-border
+    hover:bg-components-button-destructive-primary-bg-hover
+    hover:border-components-button-destructive-primary-border-hover
+    text-components-button-destructive-primary-text;
+  }
+
+  .btn-primary.btn-disabled {
+    @apply 
+    shadow-none
+    bg-components-button-primary-bg-disabled
+    border-components-button-primary-border-disabled
+    text-components-button-primary-text-disabled;
+  }
+
+  .btn-primary.btn-destructive.btn-disabled {
+    @apply 
+    shadow-none
+    bg-components-button-destructive-primary-bg-disabled
+    border-components-button-destructive-primary-border-disabled
+    text-components-button-destructive-primary-text-disabled;
   }
 
   .btn-secondary {
-    @apply bg-white hover:bg-white/80 border-gray-200 hover:border-gray-300 text-gray-700;
+    @apply 
+    border-[0.5px]
+    shadow-xs
+    bg-components-button-secondary-bg 
+    border-components-button-secondary-border 
+    hover:bg-components-button-secondary-bg-hover 
+    hover:border-components-button-secondary-border-hover 
+    text-components-button-secondary-text;
   }
 
+  .btn-secondary.btn-disabled {
+    @apply 
+    bg-components-button-secondary-bg-disabled 
+    border-components-button-secondary-border-disabled 
+    text-components-button-secondary-text-disabled;
+  }
+
+  .btn-secondary.btn-destructive {
+    @apply 
+    bg-components-button-destructive-secondary-bg 
+    border-components-button-destructive-secondary-border 
+    hover:bg-components-button-destructive-secondary-bg-hover 
+    hover:border-components-button-destructive-secondary-border-hover 
+    text-components-button-destructive-secondary-text;
+  }
+
+  .btn-secondary.btn-destructive.btn-disabled {
+    @apply 
+    bg-components-button-destructive-secondary-bg-disabled 
+    border-components-button-destructive-secondary-border-disabled 
+    text-components-button-destructive-secondary-text-disabled;
+  }
+
+  
   .btn-secondary-accent {
-    @apply bg-white hover:bg-white/80 border-gray-200 hover:border-gray-300 text-primary-600;
+    @apply 
+    border-[0.5px]
+    shadow-xs
+    bg-components-button-secondary-bg 
+    border-components-button-secondary-border 
+    hover:bg-components-button-secondary-bg-hover 
+    hover:border-components-button-secondary-border-hover 
+    text-components-button-secondary-accent-text;
   }
 
-  .btn-primary {
-    @apply bg-primary-600 hover:bg-primary-700 text-white;
+  .btn-secondary-accent.btn-disabled {
+    @apply 
+    bg-components-button-secondary-bg-disabled 
+    border-components-button-secondary-border-disabled 
+    text-components-button-secondary-accent-text-disabled;
   }
 
   .btn-warning {
-    @apply bg-red-600 hover:bg-red-700 text-white;
+    @apply 
+    bg-components-button-destructive-primary-bg 
+    border-components-button-destructive-primary-border 
+    hover:bg-components-button-destructive-primary-bg-hover 
+    hover:border-components-button-destructive-primary-border-hover 
+    text-components-button-destructive-primary-text;
   }
 
-  .btn-ghost {
-    @apply bg-transparent hover:bg-gray-200 border-transparent shadow-none text-gray-700;
+  .btn-warning.btn-disabled {
+    @apply 
+    bg-components-button-destructive-primary-bg-disabled 
+    border-components-button-destructive-primary-border-disabled 
+    text-components-button-destructive-primary-text-disabled;
   }
 
   .btn-tertiary {
-    @apply bg-[#F2F4F7] hover:bg-[#E9EBF0] border-transparent shadow-none text-gray-700;
+    @apply 
+    bg-components-button-tertiary-bg 
+    hover:bg-components-button-tertiary-bg-hover 
+    text-components-button-tertiary-text;
+  }
+
+  .btn-tertiary.btn-disabled {
+    @apply 
+    bg-components-button-tertiary-bg-disabled 
+    text-components-button-tertiary-text-disabled;
+  }
+
+  .btn-tertiary.btn-destructive {
+    @apply 
+    bg-components-button-destructive-tertiary-bg 
+    hover:bg-components-button-destructive-tertiary-bg-hover 
+    text-components-button-destructive-tertiary-text;
+  }
+
+  .btn-tertiary.btn-destructive.btn-disabled {
+    @apply 
+    bg-components-button-destructive-tertiary-bg-disabled 
+    text-components-button-destructive-tertiary-text-disabled;
+  }
+
+  .btn-ghost {
+    @apply 
+    hover:bg-components-button-ghost-bg-hover 
+    text-components-button-ghost-text;
+  }
+
+  .btn-ghost.btn-disabled {
+    @apply 
+    text-components-button-ghost-text-disabled;
+  }
+
+  .btn-ghost.btn-destructive {
+    @apply 
+    hover:bg-components-button-destructive-ghost-bg-hover 
+    text-components-button-destructive-ghost-text;
+  }
+
+  .btn-ghost.btn-destructive.btn-disabled {
+    @apply 
+    text-components-button-destructive-ghost-text-disabled;
+  }
+
+  .btn-ghost-accent {
+    @apply 
+    hover:bg-state-accent-hover
+    text-components-button-secondary-accent-text;
+  }
+
+  .btn-ghost-accent.btn-disabled {
+    @apply 
+    text-components-button-secondary-accent-text-disabled;
   }
 }

+ 7 - 2
web/app/components/base/button/index.tsx

@@ -14,6 +14,7 @@ const buttonVariants = cva(
         'secondary': 'btn-secondary',
         'secondary-accent': 'btn-secondary-accent',
         'ghost': 'btn-ghost',
+        'ghost-accent': 'btn-ghost-accent',
         'tertiary': 'btn-tertiary',
       },
       size: {
@@ -30,16 +31,20 @@ const buttonVariants = cva(
 )
 
 export type ButtonProps = {
+  destructive?: boolean
   loading?: boolean
   styleCss?: CSSProperties
 } & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>
 
 const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
-  ({ className, variant, size, loading, styleCss, children, ...props }, ref) => {
+  ({ className, variant, size, destructive, loading, styleCss, children, ...props }, ref) => {
     return (
       <button
         type='button'
-        className={classNames(buttonVariants({ variant, size, className }))}
+        className={classNames(
+          buttonVariants({ variant, size, className }),
+          destructive && 'btn-destructive',
+        )}
         ref={ref}
         style={styleCss}
         {...props}

+ 11 - 4
web/app/components/base/radio-card/index.tsx

@@ -1,7 +1,6 @@
 'use client'
 import type { FC } from 'react'
 import React from 'react'
-import s from './style.module.css'
 import cn from '@/utils/classnames'
 
 type Props = {
@@ -29,7 +28,12 @@ const RadioCard: FC<Props> = ({
   chosenConfigWrapClassName,
 }) => {
   return (
-    <div className={cn(s.item, isChosen && s.active)}>
+    <div
+      className={cn(
+        'border border-components-option-card-option-border bg-components-option-card-option-bg rounded-xl hover:shadow-xs cursor-pointer',
+        isChosen && 'bg-components-option-card-option-selected-bg border-components-panel-border shadow-xs',
+      )}
+    >
       <div className='flex py-3 pl-3 pr-4' onClick={onChosen}>
         <div className={cn(iconBgClassName, 'mr-3 shrink-0 flex w-8 justify-center h-8 items-center rounded-lg')}>
           {icon}
@@ -40,12 +44,15 @@ const RadioCard: FC<Props> = ({
         </div>
         {!noRadio && (
           <div className='shrink-0 flex items-center h-8'>
-            <div className={s.radio}></div>
+            <div className={cn(
+              'w-4 h-4 border border-components-radio-border bg-components-radio-bg shadow-xs rounded-full',
+              isChosen && 'border-[5px] border-components-radio-border-checked',
+            )}></div>
           </div>
         )}
       </div>
       {((isChosen && chosenConfig) || noRadio) && (
-        <div className={cn(chosenConfigWrapClassName, 'pt-2 px-14 pb-6 border-t border-gray-200')}>
+        <div className={cn(chosenConfigWrapClassName, 'p-3 border-t border-gray-200')}>
           {chosenConfig}
         </div>
       )}

+ 1 - 1
web/app/components/base/radio-card/simple/index.tsx

@@ -6,7 +6,7 @@ import cn from '@/utils/classnames'
 
 type Props = {
   className?: string
-  title: string
+  title: string | JSX.Element | null
   description: string
   isChosen: boolean
   onChosen: () => void

+ 0 - 25
web/app/components/base/radio-card/style.module.css

@@ -1,25 +0,0 @@
-.item {
-  @apply relative rounded-xl border border-gray-100 cursor-pointer;
-  background-color: #fcfcfd;
-}
-
-.item.active {
-  border-width: 1.5px;
-  border-color: #528BFF;
-  box-shadow: 0px 1px 3px rgba(16, 24, 40, 0.1), 0px 1px 2px rgba(16, 24, 40, 0.06);
-}
-
-.item:hover {
-  background-color: #ffffff;
-  border-color: #B2CCFF;
-  box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
-}
-
-.radio {
-  @apply w-4 h-4 border-[2px] border-gray-200 rounded-full;
-}
-
-.item.active .radio {
-  border-width: 5px;
-  border-color: #155EEF;
-}

+ 16 - 4
web/app/components/base/slider/index.tsx

@@ -4,6 +4,8 @@ import './style.css'
 
 type ISliderProps = {
   className?: string
+  thumbClassName?: string
+  trackClassName?: string
   value: number
   max?: number
   min?: number
@@ -12,16 +14,26 @@ type ISliderProps = {
   onChange: (value: number) => void
 }
 
-const Slider: React.FC<ISliderProps> = ({ className, max, min, step, value, disabled, onChange }) => {
+const Slider: React.FC<ISliderProps> = ({
+  className,
+  thumbClassName,
+  trackClassName,
+  max,
+  min,
+  step,
+  value,
+  disabled,
+  onChange,
+}) => {
   return <ReactSlider
     disabled={disabled}
     value={isNaN(value) ? 0 : value}
     min={min || 0}
     max={max || 100}
     step={step || 1}
-    className={cn(className, 'slider')}
-    thumbClassName="slider-thumb"
-    trackClassName="slider-track"
+    className={cn('slider', className)}
+    thumbClassName={cn('slider-thumb', thumbClassName)}
+    trackClassName={cn('slider-track', trackClassName)}
     onChange={onChange}
   />
 }

+ 2 - 1
web/app/components/datasets/common/check-rerank-model.ts

@@ -3,6 +3,7 @@ import type {
   DefaultModelResponse,
   Model,
 } from '@/app/components/header/account-setting/model-provider-page/declarations'
+import { RerankingModeEnum } from '@/models/datasets'
 
 export const isReRankModelSelected = ({
   rerankDefaultModel,
@@ -32,7 +33,7 @@ export const isReRankModelSelected = ({
 
   if (
     indexMethod === 'high_quality'
-    && (retrievalConfig.reranking_enable || retrievalConfig.search_method === RETRIEVE_METHOD.hybrid)
+    && (retrievalConfig.search_method === RETRIEVE_METHOD.hybrid && retrievalConfig.reranking_mode !== RerankingModeEnum.WeightedScore)
     && !rerankModelSelected
   )
     return false

+ 193 - 37
web/app/components/datasets/common/retrieval-param-config/index.tsx

@@ -15,6 +15,11 @@ import type { RetrievalConfig } from '@/types/app'
 import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
 import { useModelListAndDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
 import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
+import {
+  RerankingModeEnum,
+  WeightedScoreEnum,
+} from '@/models/datasets'
+import WeightedScore from '@/app/components/app/configuration/dataset-config/params-config/weighted-score'
 
 type Props = {
   type: RETRIEVE_METHOD
@@ -34,6 +39,7 @@ const RetrievalParamConfig: FC<Props> = ({
     defaultModel: rerankDefaultModel,
     modelList: rerankModelList,
   } = useModelListAndDefaultModel(ModelTypeEnum.rerank)
+  const isHybridSearch = type === RETRIEVE_METHOD.hybrid
 
   const rerankModel = (() => {
     if (value.reranking_model) {
@@ -50,9 +56,47 @@ const RetrievalParamConfig: FC<Props> = ({
     }
   })()
 
+  const handleChangeRerankMode = (v: RerankingModeEnum) => {
+    if (v === value.reranking_mode)
+      return
+
+    const result = {
+      ...value,
+      reranking_mode: v,
+    }
+
+    if (!result.weights && v === RerankingModeEnum.WeightedScore) {
+      result.weights = {
+        weight_type: WeightedScoreEnum.Customized,
+        vector_setting: {
+          vector_weight: 0.5,
+          embedding_provider_name: '',
+          embedding_model_name: '',
+        },
+        keyword_setting: {
+          keyword_weight: 0.5,
+        },
+      }
+    }
+    onChange(result)
+  }
+
+  const rerankingModeOptions = [
+    {
+      value: RerankingModeEnum.WeightedScore,
+      label: t('dataset.weightedScore.title'),
+      tips: t('dataset.weightedScore.description'),
+    },
+    {
+      value: RerankingModeEnum.RerankingModel,
+      label: t('common.modelProvider.rerankModel.key'),
+      tips: t('common.modelProvider.rerankModel.tip'),
+    },
+  ]
+
   return (
     <div>
-      {!isEconomical && (
+      {!isEconomical && !isHybridSearch && (
         <div>
           <div className='flex h-8 items-center text-[13px] font-medium text-gray-900 space-x-2'>
             {canToggleRerankModalEnable && (
@@ -75,10 +119,10 @@ const RetrievalParamConfig: FC<Props> = ({
             </div>
           </div>
           <ModelSelector
-            triggerClassName={`${!value.reranking_enable && type !== RETRIEVE_METHOD.hybrid && '!opacity-60 !cursor-not-allowed'}`}
+            triggerClassName={`${!value.reranking_enable && '!opacity-60 !cursor-not-allowed'}`}
             defaultModel={rerankModel && { provider: rerankModel.provider_name, model: rerankModel.model_name }}
             modelList={rerankModelList}
-            readonly={!value.reranking_enable && type !== RETRIEVE_METHOD.hybrid}
+            readonly={!value.reranking_enable}
             onSelect={(v) => {
               onChange({
                 ...value,
@@ -91,40 +135,152 @@ const RetrievalParamConfig: FC<Props> = ({
           />
         </div>
       )}
-
-      <div className={cn(!isEconomical && 'mt-4', 'flex space-between space-x-6')}>
-        <TopKItem
-          className='grow'
-          value={value.top_k}
-          onChange={(_key, v) => {
-            onChange({
-              ...value,
-              top_k: v,
-            })
-          }}
-          enable={true}
-        />
-        {(!isEconomical && !(value.search_method === RETRIEVE_METHOD.fullText && !value.reranking_enable)) && (
-          <ScoreThresholdItem
-            className='grow'
-            value={value.score_threshold}
-            onChange={(_key, v) => {
-              onChange({
-                ...value,
-                score_threshold: v,
-              })
-            }}
-            enable={value.score_threshold_enabled}
-            hasSwitch={true}
-            onSwitchChange={(_key, v) => {
-              onChange({
-                ...value,
-                score_threshold_enabled: v,
-              })
-            }}
-          />
-        )}
-      </div>
+      {
+        !isHybridSearch && (
+          <div className={cn(!isEconomical && 'mt-4', 'flex space-between space-x-6')}>
+            <TopKItem
+              className='grow'
+              value={value.top_k}
+              onChange={(_key, v) => {
+                onChange({
+                  ...value,
+                  top_k: v,
+                })
+              }}
+              enable={true}
+            />
+            {(!isEconomical && !(value.search_method === RETRIEVE_METHOD.fullText && !value.reranking_enable)) && (
+              <ScoreThresholdItem
+                className='grow'
+                value={value.score_threshold}
+                onChange={(_key, v) => {
+                  onChange({
+                    ...value,
+                    score_threshold: v,
+                  })
+                }}
+                enable={value.score_threshold_enabled}
+                hasSwitch={true}
+                onSwitchChange={(_key, v) => {
+                  onChange({
+                    ...value,
+                    score_threshold_enabled: v,
+                  })
+                }}
+              />
+            )}
+          </div>
+        )
+      }
+      {
+        isHybridSearch && (
+          <>
+            <div className='flex items-center justify-between'>
+              {
+                rerankingModeOptions.map(option => (
+                  <div
+                    key={option.value}
+                    className={cn(
+                      'flex items-center justify-center mb-4 w-[calc((100%-8px)/2)] h-8 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg cursor-pointer system-sm-medium text-text-secondary',
+                      value.reranking_mode === RerankingModeEnum.WeightedScore && option.value === RerankingModeEnum.WeightedScore && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary',
+                      value.reranking_mode !== RerankingModeEnum.WeightedScore && option.value !== RerankingModeEnum.WeightedScore && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary',
+                    )}
+                    onClick={() => handleChangeRerankMode(option.value)}
+                  >
+                    <div className='truncate'>{option.label}</div>
+                    <Tooltip
+                      popupContent={<div className='w-[200px]'>{option.tips}</div>}
+                      hideArrow
+                    >
+                      <RiQuestionLine className='ml-0.5 w-3.5 h-4.5 text-text-quaternary' />
+                    </Tooltip>
+                  </div>
+                ))
+              }
+            </div>
+            {
+              value.reranking_mode === RerankingModeEnum.WeightedScore && (
+                <WeightedScore
+                  value={{
+                    type: value.weights!.weight_type,
+                    value: [
+                      value.weights!.vector_setting.vector_weight,
+                      value.weights!.keyword_setting.keyword_weight,
+                    ],
+                  }}
+                  onChange={(v) => {
+                    onChange({
+                      ...value,
+                      weights: {
+                        ...value.weights!,
+                        weight_type: v.type,
+                        vector_setting: {
+                          ...value.weights!.vector_setting,
+                          vector_weight: v.value[0],
+                        },
+                        keyword_setting: {
+                          ...value.weights!.keyword_setting,
+                          keyword_weight: v.value[1],
+                        },
+                      },
+                    })
+                  }}
+                />
+              )
+            }
+            {
+              value.reranking_mode !== RerankingModeEnum.WeightedScore && (
+                <ModelSelector
+                  triggerClassName={`${!value.reranking_enable && '!opacity-60 !cursor-not-allowed'}`}
+                  defaultModel={rerankModel && { provider: rerankModel.provider_name, model: rerankModel.model_name }}
+                  modelList={rerankModelList}
+                  readonly={!value.reranking_enable}
+                  onSelect={(v) => {
+                    onChange({
+                      ...value,
+                      reranking_model: {
+                        reranking_provider_name: v.provider,
+                        reranking_model_name: v.model,
+                      },
+                    })
+                  }}
+                />
+              )
+            }
+            <div className={cn(!isEconomical && 'mt-4', 'flex space-between space-x-6')}>
+              <TopKItem
+                className='grow'
+                value={value.top_k}
+                onChange={(_key, v) => {
+                  onChange({
+                    ...value,
+                    top_k: v,
+                  })
+                }}
+                enable={true}
+              />
+              <ScoreThresholdItem
+                className='grow'
+                value={value.score_threshold}
+                onChange={(_key, v) => {
+                  onChange({
+                    ...value,
+                    score_threshold: v,
+                  })
+                }}
+                enable={value.score_threshold_enabled}
+                hasSwitch={true}
+                onSwitchChange={(_key, v) => {
+                  onChange({
+                    ...value,
+                    score_threshold_enabled: v,
+                  })
+                }}
+              />
+            </div>
+          </>
+        )
+      }
     </div>
   )
 }

+ 4 - 0
web/app/components/datasets/settings/form/index.tsx

@@ -113,6 +113,10 @@ const Form = () => {
       retrievalConfig,
       indexMethod,
     })
+    if (postRetrievalConfig.weights) {
+      postRetrievalConfig.weights.vector_setting.embedding_provider_name = currentDataset?.embedding_model_provider || ''
+      postRetrievalConfig.weights.vector_setting.embedding_model_name = currentDataset?.embedding_model || ''
+    }
     try {
       setLoading(true)
       const requestParams = {

+ 0 - 1
web/app/components/tools/add-tool-modal/category.tsx

@@ -22,7 +22,6 @@ const Icon = ({ svgString, active }: { svgString: string; active: boolean }) =>
       return null
     const parser = new DOMParser()
     const doc = parser.parseFromString(svg, 'image/svg+xml')
-    console.log(doc.documentElement)
     return doc.documentElement
   }
   useMount(() => {

+ 9 - 2
web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx

@@ -4,15 +4,17 @@ import React, { useCallback } from 'react'
 import { useBoolean } from 'ahooks'
 import {
   RiDeleteBinLine,
+  RiEditLine,
 } from '@remixicon/react'
 import type { DataSet } from '@/models/datasets'
 import { DataSourceType } from '@/models/datasets'
-import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
 import FileIcon from '@/app/components/base/file-icon'
 import { Folder } from '@/app/components/base/icons/src/vender/solid/files'
 import SettingsModal from '@/app/components/app/configuration/dataset-config/settings-modal'
 import Drawer from '@/app/components/base/drawer'
 import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
+import Badge from '@/app/components/base/badge'
+import { useKnowledge } from '@/hooks/use-knowledge'
 
 type Props = {
   payload: DataSet
@@ -29,6 +31,7 @@ const DatasetItem: FC<Props> = ({
 }) => {
   const media = useBreakpoints()
   const isMobile = media === MediaType.mobile
+  const { formatIndexingTechniqueAndMethod } = useKnowledge()
 
   const [isShowSettingsModal, {
     setTrue: showSettingsModal,
@@ -62,7 +65,7 @@ const DatasetItem: FC<Props> = ({
             className='flex items-center justify-center w-6 h-6 hover:bg-black/5 rounded-md cursor-pointer'
             onClick={showSettingsModal}
           >
-            <Settings01 className='w-4 h-4 text-gray-500' />
+            <RiEditLine className='w-4 h-4 text-gray-500' />
           </div>
           <div
             className='flex items-center justify-center w-6 h-6 hover:bg-black/5 rounded-md cursor-pointer'
@@ -72,6 +75,10 @@ const DatasetItem: FC<Props> = ({
           </div>
         </div>
       )}
+      <Badge
+        className='group-hover/dataset-item:hidden shrink-0'
+        text={formatIndexingTechniqueAndMethod(payload.indexing_technique, payload.retrieval_model_dict?.search_method)}
+      />
 
       {isShowSettingsModal && (
         <Drawer isOpen={isShowSettingsModal} onClose={hideSettingsModal} footer={null} mask={isMobile} panelClassname='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[640px] rounded-xl'>

+ 41 - 20
web/app/components/workflow/nodes/knowledge-retrieval/components/retrieval-config.tsx

@@ -1,8 +1,8 @@
 'use client'
 import type { FC } from 'react'
 import React, { useCallback, useState } from 'react'
+import { RiEqualizer2Line } from '@remixicon/react'
 import { useTranslation } from 'react-i18next'
-import { RiArrowDownSLine } from '@remixicon/react'
 import type { MultipleRetrievalConfig, SingleRetrievalConfig } from '../types'
 import type { ModelConfig } from '../../../types'
 import cn from '@/utils/classnames'
@@ -16,10 +16,9 @@ import { RETRIEVE_TYPE } from '@/types/app'
 import { DATASET_DEFAULT } from '@/config'
 import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
 import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
-
-import type {
-  DatasetConfigs,
-} from '@/models/debug'
+import Button from '@/app/components/base/button'
+import type { DatasetConfigs } from '@/models/debug'
+import type { DataSet } from '@/models/datasets'
 
 type Props = {
   payload: {
@@ -33,6 +32,9 @@ type Props = {
   onSingleRetrievalModelChange?: (config: ModelConfig) => void
   onSingleRetrievalModelParamsChange?: (config: ModelConfig) => void
   readonly?: boolean
+  openFromProps?: boolean
+  onOpenFromPropsChange?: (openFromProps: boolean) => void
+  selectedDatasets: DataSet[]
 }
 
 const RetrievalConfig: FC<Props> = ({
@@ -43,10 +45,18 @@ const RetrievalConfig: FC<Props> = ({
   onSingleRetrievalModelChange,
   onSingleRetrievalModelParamsChange,
   readonly,
+  openFromProps,
+  onOpenFromPropsChange,
+  selectedDatasets,
 }) => {
   const { t } = useTranslation()
-
   const [open, setOpen] = useState(false)
+  const mergedOpen = openFromProps !== undefined ? openFromProps : open
+
+  const handleOpen = useCallback((newOpen: boolean) => {
+    setOpen(newOpen)
+    onOpenFromPropsChange?.(newOpen)
+  }, [onOpenFromPropsChange])
 
   const {
     defaultModel: rerankDefaultModel,
@@ -72,16 +82,18 @@ const RetrievalConfig: FC<Props> = ({
             provider: configs.reranking_model?.reranking_provider_name,
             model: configs.reranking_model?.reranking_model_name,
           }),
+      reranking_mode: configs.reranking_mode,
+      weights: configs.weights as any,
+      reranking_enable: configs.reranking_enable,
     })
   }, [onMultipleRetrievalConfigChange, payload.retrieval_mode, rerankDefaultModel?.provider?.provider, rerankDefaultModel?.model, onRetrievalModeChange])
 
   return (
     <PortalToFollowElem
-      open={open}
-      onOpenChange={setOpen}
+      open={mergedOpen}
+      onOpenChange={handleOpen}
       placement='bottom-end'
       offset={{
-        // mainAxis: 12,
         crossAxis: -2,
       }}
     >
@@ -89,13 +101,18 @@ const RetrievalConfig: FC<Props> = ({
         onClick={() => {
           if (readonly)
             return
-          setOpen(v => !v)
+          handleOpen(!mergedOpen)
         }}
       >
-        <div className={cn(!readonly && 'cursor-pointer', open && 'bg-gray-100', 'flex items-center h-6  px-2 rounded-md hover:bg-gray-100 group  select-none')}>
-          <div className={cn(open ? 'text-gray-700' : 'text-gray-500', 'leading-[18px] text-xs font-medium group-hover:bg-gray-100')}>{payload.retrieval_mode === RETRIEVE_TYPE.oneWay ? t('appDebug.datasetConfig.retrieveOneWay.title') : t('appDebug.datasetConfig.retrieveMultiWay.title')}</div>
-          {!readonly && <RiArrowDownSLine className='w-3 h-3 ml-1' />}
-        </div>
+        <Button
+          variant='ghost'
+          size='small'
+          disabled={readonly}
+          className={cn(open && 'bg-components-button-ghost-bg-hover')}
+        >
+          <RiEqualizer2Line className='mr-1 w-3.5 h-3.5' />
+          {t('dataset.retrievalSettings')}
+        </Button>
       </PortalToFollowElemTrigger>
       <PortalToFollowElemContent style={{ zIndex: 1001 }}>
         <div className='w-[404px] pt-3 pb-4 px-4 shadow-xl  rounded-2xl border border-gray-200  bg-white'>
@@ -103,21 +120,24 @@ const RetrievalConfig: FC<Props> = ({
             datasetConfigs={
               {
                 retrieval_model: payload.retrieval_mode,
-                reranking_model: !multiple_retrieval_config?.reranking_model?.provider
+                reranking_model: multiple_retrieval_config?.reranking_model?.provider
                   ? {
-                    reranking_provider_name: rerankDefaultModel?.provider?.provider || '',
-                    reranking_model_name: rerankDefaultModel?.model || '',
+                    reranking_provider_name: multiple_retrieval_config.reranking_model?.provider,
+                    reranking_model_name: multiple_retrieval_config.reranking_model?.model,
                   }
                   : {
-                    reranking_provider_name: multiple_retrieval_config?.reranking_model?.provider || '',
-                    reranking_model_name: multiple_retrieval_config?.reranking_model?.model || '',
+                    reranking_provider_name: '',
+                    reranking_model_name: '',
                   },
                 top_k: multiple_retrieval_config?.top_k || DATASET_DEFAULT.top_k,
-                score_threshold_enabled: !(multiple_retrieval_config?.score_threshold === undefined || multiple_retrieval_config?.score_threshold === null),
+                score_threshold_enabled: !(multiple_retrieval_config?.score_threshold === undefined || multiple_retrieval_config.score_threshold === null),
                 score_threshold: multiple_retrieval_config?.score_threshold,
                 datasets: {
                   datasets: [],
                 },
+                reranking_mode: multiple_retrieval_config?.reranking_mode,
+                weights: multiple_retrieval_config?.weights,
+                reranking_enable: multiple_retrieval_config?.reranking_enable,
               }
             }
             onChange={handleChange}
@@ -125,6 +145,7 @@ const RetrievalConfig: FC<Props> = ({
             singleRetrievalModelConfig={singleRetrievalModelConfig}
             onSingleRetrievalModelChange={onSingleRetrievalModelChange}
             onSingleRetrievalModelParamsChange={onSingleRetrievalModelParamsChange}
+            selectedDatasets={selectedDatasets}
           />
         </div>
       </PortalToFollowElemContent>

+ 7 - 2
web/app/components/workflow/nodes/knowledge-retrieval/default.ts

@@ -2,7 +2,7 @@ import { BlockEnum } from '../../types'
 import type { NodeDefault } from '../../types'
 import type { KnowledgeRetrievalNodeType } from './types'
 import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
-
+import { DATASET_DEFAULT } from '@/config'
 import { RETRIEVE_TYPE } from '@/types/app'
 const i18nPrefix = 'workflow'
 
@@ -10,7 +10,12 @@ const nodeDefault: NodeDefault<KnowledgeRetrievalNodeType> = {
   defaultValue: {
     query_variable_selector: [],
     dataset_ids: [],
-    retrieval_mode: RETRIEVE_TYPE.oneWay,
+    retrieval_mode: RETRIEVE_TYPE.multiWay,
+    multiple_retrieval_config: {
+      top_k: DATASET_DEFAULT.top_k,
+      score_threshold: undefined,
+      reranking_enable: false,
+    },
   },
   getAvailablePrevNodes(isChatMode: boolean) {
     const nodes = isChatMode

+ 14 - 0
web/app/components/workflow/nodes/knowledge-retrieval/hooks.ts

@@ -0,0 +1,14 @@
+import { useMemo } from 'react'
+import { getSelectedDatasetsMode } from './utils'
+import type {
+  DataSet,
+  SelectedDatasetsMode,
+} from '@/models/datasets'
+
+export const useSelectedDatasetsMode = (datasets: DataSet[]) => {
+  const selectedDatasetsMode: SelectedDatasetsMode = useMemo(() => {
+    return getSelectedDatasetsMode(datasets)
+  }, [datasets])
+
+  return selectedDatasetsMode
+}

+ 15 - 3
web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx

@@ -1,5 +1,8 @@
 import type { FC } from 'react'
-import React from 'react'
+import {
+  memo,
+  useCallback,
+} from 'react'
 import { useTranslation } from 'react-i18next'
 import VarReferencePicker from '../_base/components/variable/var-reference-picker'
 import useConfig from './use-config'
@@ -41,8 +44,14 @@ const Panel: FC<NodePanelProps<KnowledgeRetrievalNodeType>> = ({
     query,
     setQuery,
     runResult,
+    rerankModelOpen,
+    setRerankModelOpen,
   } = useConfig(id, data)
 
+  const handleOpenFromPropsChange = useCallback((openFromProps: boolean) => {
+    setRerankModelOpen(openFromProps)
+  }, [setRerankModelOpen])
+
   return (
     <div className='mt-2'>
       <div className='px-4 pb-4 space-y-4'>
@@ -75,7 +84,10 @@ const Panel: FC<NodePanelProps<KnowledgeRetrievalNodeType>> = ({
                 singleRetrievalModelConfig={inputs.single_retrieval_config?.model}
                 onSingleRetrievalModelChange={handleModelChanged as any}
                 onSingleRetrievalModelParamsChange={handleCompletionParamsChange}
-                readonly={readOnly}
+                readonly={readOnly || !selectedDatasets.length}
+                openFromProps={rerankModelOpen}
+                onOpenFromPropsChange={handleOpenFromPropsChange}
+                selectedDatasets={selectedDatasets}
               />
               {!readOnly && (<div className='w-px h-3 bg-gray-200'></div>)}
               {!readOnly && (
@@ -162,4 +174,4 @@ const Panel: FC<NodePanelProps<KnowledgeRetrievalNodeType>> = ({
   )
 }
 
-export default React.memo(Panel)
+export default memo(Panel)

+ 17 - 0
web/app/components/workflow/nodes/knowledge-retrieval/types.ts

@@ -1,5 +1,9 @@
 import type { CommonNodeType, ModelConfig, ValueSelector } from '@/app/components/workflow/types'
 import type { RETRIEVE_TYPE } from '@/types/app'
+import type {
+  RerankingModeEnum,
+  WeightedScoreEnum,
+} from '@/models/datasets'
 
 export type MultipleRetrievalConfig = {
   top_k: number
@@ -8,6 +12,19 @@ export type MultipleRetrievalConfig = {
     provider: string
     model: string
   }
+  reranking_mode?: RerankingModeEnum
+  weights?: {
+    weight_type: WeightedScoreEnum
+    vector_setting: {
+      vector_weight: number
+      embedding_provider_name: string
+      embedding_model_name: string
+    }
+    keyword_setting: {
+      keyword_weight: number
+    }
+  }
+  reranking_enable?: boolean
 }
 
 export type SingleRetrievalConfig = {

+ 34 - 25
web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts

@@ -1,4 +1,9 @@
-import { useCallback, useEffect, useRef, useState } from 'react'
+import {
+  useCallback,
+  useEffect,
+  useRef,
+  useState,
+} from 'react'
 import produce from 'immer'
 import { isEqual } from 'lodash-es'
 import type { ValueSelector, Var } from '../../types'
@@ -8,6 +13,10 @@ import {
   useWorkflow,
 } from '../../hooks'
 import type { KnowledgeRetrievalNodeType, MultipleRetrievalConfig } from './types'
+import {
+  getMultipleRetrievalConfig,
+  getSelectedDatasetsMode,
+} from './utils'
 import { RETRIEVE_TYPE } from '@/types/app'
 import { DATASET_DEFAULT } from '@/config'
 import type { DataSet } from '@/models/datasets'
@@ -126,34 +135,20 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
       draft.multiple_retrieval_config = {
         top_k: multipleRetrievalConfig?.top_k || DATASET_DEFAULT.top_k,
         score_threshold: multipleRetrievalConfig?.score_threshold,
-        reranking_model: payload.retrieval_mode === RETRIEVE_TYPE.oneWay
-          ? undefined
-          : (!multipleRetrievalConfig?.reranking_model?.provider
-            ? {
-              provider: rerankDefaultModel?.provider?.provider || '',
-              model: rerankDefaultModel?.model || '',
-            }
-            : multipleRetrievalConfig?.reranking_model),
+        reranking_model: multipleRetrievalConfig?.reranking_model,
       }
     })
     setInputs(newInput)
   // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [currentProvider?.provider, currentModel, rerankDefaultModel])
-
+  const [selectedDatasets, setSelectedDatasets] = useState<DataSet[]>([])
+  const [rerankModelOpen, setRerankModelOpen] = useState(false)
   const handleRetrievalModeChange = useCallback((newMode: RETRIEVE_TYPE) => {
     const newInputs = produce(inputs, (draft) => {
       draft.retrieval_mode = newMode
       if (newMode === RETRIEVE_TYPE.multiWay) {
-        draft.multiple_retrieval_config = {
-          top_k: draft.multiple_retrieval_config?.top_k || DATASET_DEFAULT.top_k,
-          score_threshold: draft.multiple_retrieval_config?.score_threshold,
-          reranking_model: !draft.multiple_retrieval_config?.reranking_model?.provider
-            ? {
-              provider: rerankDefaultModel?.provider?.provider || '',
-              model: rerankDefaultModel?.model || '',
-            }
-            : draft.multiple_retrieval_config?.reranking_model,
-        }
+        const multipleRetrievalConfig = draft.multiple_retrieval_config
+        draft.multiple_retrieval_config = getMultipleRetrievalConfig(multipleRetrievalConfig!, selectedDatasets)
       }
       else {
         const hasSetModel = draft.single_retrieval_config?.model?.provider
@@ -170,17 +165,16 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
       }
     })
     setInputs(newInputs)
-  }, [currentModel?.model, currentModel?.model_properties?.mode, currentProvider?.provider, inputs, rerankDefaultModel?.model, rerankDefaultModel?.provider?.provider, setInputs])
+  }, [currentModel?.model, currentModel?.model_properties?.mode, currentProvider?.provider, inputs, setInputs, selectedDatasets])
 
   const handleMultipleRetrievalConfigChange = useCallback((newConfig: MultipleRetrievalConfig) => {
     const newInputs = produce(inputs, (draft) => {
-      draft.multiple_retrieval_config = newConfig
+      draft.multiple_retrieval_config = getMultipleRetrievalConfig(newConfig!, selectedDatasets)
     })
     setInputs(newInputs)
-  }, [inputs, setInputs])
+  }, [inputs, setInputs, selectedDatasets])
 
   // datasets
-  const [selectedDatasets, setSelectedDatasets] = useState<DataSet[]>([])
   useEffect(() => {
     (async () => {
       const inputs = inputRef.current
@@ -210,12 +204,25 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
   }, [])
 
   const handleOnDatasetsChange = useCallback((newDatasets: DataSet[]) => {
+    const {
+      allEconomic,
+      mixtureHighQualityAndEconomic,
+      inconsistentEmbeddingModel,
+    } = getSelectedDatasetsMode(newDatasets)
     const newInputs = produce(inputs, (draft) => {
       draft.dataset_ids = newDatasets.map(d => d.id)
+
+      if (payload.retrieval_mode === RETRIEVE_TYPE.multiWay && newDatasets.length > 0) {
+        const multipleRetrievalConfig = draft.multiple_retrieval_config
+        draft.multiple_retrieval_config = getMultipleRetrievalConfig(multipleRetrievalConfig!, newDatasets)
+      }
     })
     setInputs(newInputs)
     setSelectedDatasets(newDatasets)
-  }, [inputs, setInputs])
+
+    if (allEconomic || mixtureHighQualityAndEconomic || inconsistentEmbeddingModel)
+      setRerankModelOpen(true)
+  }, [inputs, setInputs, payload.retrieval_mode])
 
   const filterVar = useCallback((varPayload: Var) => {
     return varPayload.type === VarType.string
@@ -266,6 +273,8 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
     query,
     setQuery,
     runResult,
+    rerankModelOpen,
+    setRerankModelOpen,
   }
 }
 

+ 121 - 2
web/app/components/workflow/nodes/knowledge-retrieval/utils.ts

@@ -1,5 +1,124 @@
-import type { KnowledgeRetrievalNodeType } from './types'
+import { uniq } from 'lodash-es'
+import type { MultipleRetrievalConfig } from './types'
+import type {
+  DataSet,
+  SelectedDatasetsMode,
+} from '@/models/datasets'
+import {
+  DEFAULT_WEIGHTED_SCORE,
+  RerankingModeEnum,
+  WeightedScoreEnum,
+} from '@/models/datasets'
+import { RETRIEVE_METHOD } from '@/types/app'
+import { DATASET_DEFAULT } from '@/config'
 
-export const checkNodeValid = (payload: KnowledgeRetrievalNodeType) => {
+export const checkNodeValid = () => {
   return true
 }
+
+export const getSelectedDatasetsMode = (datasets: DataSet[]) => {
+  let allHighQuality = true
+  let allHighQualityVectorSearch = true
+  let allHighQualityFullTextSearch = true
+  let allEconomic = true
+  let mixtureHighQualityAndEconomic = true
+  let inconsistentEmbeddingModel = false
+  if (!datasets.length) {
+    allHighQuality = false
+    allHighQualityVectorSearch = false
+    allHighQualityFullTextSearch = false
+    allEconomic = false
+    mixtureHighQualityAndEconomic = false
+    inconsistentEmbeddingModel = false
+  }
+  datasets.forEach((dataset) => {
+    if (dataset.indexing_technique === 'economy') {
+      allHighQuality = false
+      allHighQualityVectorSearch = false
+      allHighQualityFullTextSearch = false
+    }
+    if (dataset.indexing_technique === 'high_quality') {
+      allEconomic = false
+
+      if (dataset.retrieval_model_dict.search_method !== RETRIEVE_METHOD.semantic)
+        allHighQualityVectorSearch = false
+
+      if (dataset.retrieval_model_dict.search_method !== RETRIEVE_METHOD.fullText)
+        allHighQualityFullTextSearch = false
+    }
+  })
+
+  if (allHighQuality || allEconomic)
+    mixtureHighQualityAndEconomic = false
+
+  if (allHighQuality)
+    inconsistentEmbeddingModel = uniq(datasets.map(item => item.embedding_model)).length > 1
+
+  return {
+    allHighQuality,
+    allHighQualityVectorSearch,
+    allHighQualityFullTextSearch,
+    allEconomic,
+    mixtureHighQualityAndEconomic,
+    inconsistentEmbeddingModel,
+  } as SelectedDatasetsMode
+}
+
+export const getMultipleRetrievalConfig = (multipleRetrievalConfig: MultipleRetrievalConfig, selectedDatasets: DataSet[]) => {
+  const {
+    allHighQuality,
+    allHighQualityVectorSearch,
+    allHighQualityFullTextSearch,
+    allEconomic,
+    mixtureHighQualityAndEconomic,
+    inconsistentEmbeddingModel,
+  } = getSelectedDatasetsMode(selectedDatasets)
+
+  const {
+    top_k = DATASET_DEFAULT.top_k,
+    score_threshold,
+    reranking_mode,
+    reranking_model,
+    weights,
+    reranking_enable,
+  } = multipleRetrievalConfig || { top_k: DATASET_DEFAULT.top_k }
+
+  const result = {
+    top_k,
+    score_threshold,
+    reranking_mode,
+    reranking_model,
+    weights,
+    reranking_enable,
+  }
+
+  if (allEconomic || mixtureHighQualityAndEconomic || inconsistentEmbeddingModel)
+    result.reranking_mode = RerankingModeEnum.RerankingModel
+
+  if (allHighQuality && !inconsistentEmbeddingModel && reranking_mode === undefined)
+    result.reranking_mode = RerankingModeEnum.WeightedScore
+
+  if (allHighQuality && !inconsistentEmbeddingModel && (reranking_mode === RerankingModeEnum.WeightedScore || reranking_mode === undefined) && !weights) {
+    result.weights = {
+      weight_type: WeightedScoreEnum.Customized,
+      vector_setting: {
+        vector_weight: allHighQualityVectorSearch
+          ? DEFAULT_WEIGHTED_SCORE.allHighQualityVectorSearch.semantic
+          : allHighQualityFullTextSearch
+            ? DEFAULT_WEIGHTED_SCORE.allHighQualityFullTextSearch.semantic
+            : DEFAULT_WEIGHTED_SCORE.other.semantic,
+        embedding_provider_name: selectedDatasets[0].embedding_model_provider,
+        embedding_model_name: selectedDatasets[0].embedding_model,
+      },
+      keyword_setting: {
+        keyword_weight: allHighQualityVectorSearch
+          ? DEFAULT_WEIGHTED_SCORE.allHighQualityVectorSearch.keyword
+          : allHighQualityFullTextSearch
+            ? DEFAULT_WEIGHTED_SCORE.allHighQualityFullTextSearch.keyword
+            : DEFAULT_WEIGHTED_SCORE.other.keyword,
+      },
+    }
+  }
+
+  return result
+}

+ 2 - 2
web/config/index.ts

@@ -138,8 +138,8 @@ export const appDefaultIconBackground = '#D5F5F6'
 export const NEED_REFRESH_APP_LIST_KEY = 'needRefreshAppList'
 
 export const DATASET_DEFAULT = {
-  top_k: 2,
-  score_threshold: 0.5,
+  top_k: 4,
+  score_threshold: 0.8,
 }
 
 export const APP_PAGE_LIMIT = 10

+ 5 - 1
web/context/debug-configuration.ts

@@ -97,6 +97,8 @@ type IDebugConfiguration = {
   isShowVisionConfig: boolean
   visionConfig: VisionSettings
   setVisionConfig: (visionConfig: VisionSettings, noNotice?: boolean) => void
+  rerankSettingModalOpen: boolean
+  setRerankSettingModalOpen: (rerankSettingModalOpen: boolean) => void
 }
 
 const DebugConfigurationContext = createContext<IDebugConfiguration>({
@@ -217,7 +219,7 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({
   showSelectDataSet: () => { },
   setDataSets: () => { },
   datasetConfigs: {
-    retrieval_model: RETRIEVE_TYPE.oneWay,
+    retrieval_model: RETRIEVE_TYPE.multiWay,
     reranking_model: {
       reranking_provider_name: '',
       reranking_model_name: '',
@@ -239,6 +241,8 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({
     transfer_methods: [TransferMethod.remote_url],
   },
   setVisionConfig: () => { },
+  rerankSettingModalOpen: false,
+  setRerankSettingModalOpen: () => { },
 })
 
 export const useDebugConfigurationContext = () => useContext(DebugConfigurationContext)

+ 29 - 0
web/hooks/use-knowledge.ts

@@ -0,0 +1,29 @@
+import { useCallback } from 'react'
+import { useTranslation } from 'react-i18next'
+
+export const useKnowledge = () => {
+  const { t } = useTranslation()
+
+  const formatIndexingTechnique = useCallback((indexingTechnique: string) => {
+    return t(`dataset.indexingTechnique.${indexingTechnique}`)
+  }, [t])
+
+  const formatIndexingMethod = useCallback((indexingMethod: string) => {
+    return t(`dataset.indexingMethod.${indexingMethod}`)
+  }, [t])
+
+  const formatIndexingTechniqueAndMethod = useCallback((indexingTechnique: string, indexingMethod: string) => {
+    let result = formatIndexingTechnique(indexingTechnique)
+
+    if (indexingMethod)
+      result += ` · ${formatIndexingMethod(indexingMethod)}`
+
+    return result
+  }, [formatIndexingTechnique, formatIndexingMethod])
+
+  return {
+    formatIndexingTechnique,
+    formatIndexingMethod,
+    formatIndexingTechniqueAndMethod,
+  }
+}

+ 23 - 0
web/i18n/en-US/dataset.ts

@@ -45,6 +45,29 @@ const translation = {
   },
   docsFailedNotice: 'documents failed to be indexed',
   retry: 'Retry',
+  indexingTechnique: {
+    high_quality: 'HQ',
+    economy: 'ECO',
+  },
+  indexingMethod: {
+    semantic_search: 'VECTOR',
+    full_text_search: 'FULL TEXT',
+    hybrid_search: 'HYBRID',
+  },
+  mixtureHighQualityAndEconomicTip: 'The Rerank model is required for mixture of high quality and economical knowledge bases.',
+  inconsistentEmbeddingModelTip: 'The Rerank model is required if the Embedding models of the selected knowledge bases are inconsistent.',
+  retrievalSettings: 'Retrieval Setting',
+  rerankSettings: 'Rerank Setting',
+  weightedScore: {
+    title: 'Weighted Score',
+    description: 'By adjusting the weights assigned, this rerank strategy determines whether to prioritize semantic or keyword matching.',
+    semanticFirst: 'Semantic first',
+    keywordFirst: 'Keyword first',
+    customized: 'Customized',
+    semantic: 'Semantic',
+    keyword: 'Keyword',
+  },
+  nTo1RetrievalLegacy: 'According to product planning, N-to-1 retrieval will be officially deprecated in September. Until then you can still use it normally.',
 }
 
 export default translation

+ 23 - 0
web/i18n/zh-Hans/dataset.ts

@@ -45,6 +45,29 @@ const translation = {
   },
   docsFailedNotice: '文档无法被索引',
   retry: '重试',
+  indexingTechnique: {
+    high_quality: '高质量',
+    economy: '经济',
+  },
+  indexingMethod: {
+    semantic_search: '向量检索',
+    full_text_search: '全文检索',
+    hybrid_search: '混合检索',
+  },
+  mixtureHighQualityAndEconomicTip: '混合使用高质量和经济型知识库需要配置 Rerank 模型。',
+  inconsistentEmbeddingModelTip: '当所选知识库配置的 Embedding 模型不一致时,需要配置 Rerank 模型。',
+  retrievalSettings: '召回设置',
+  rerankSettings: 'Rerank 设置',
+  weightedScore: {
+    title: '权重设置',
+    description: '通过调整分配的权重,重新排序策略确定是优先进行语义匹配还是关键字匹配。',
+    semanticFirst: '语义优先',
+    keywordFirst: '关键词优先',
+    customized: '自定义',
+    semantic: '语义',
+    keyword: '关键词',
+  },
+  nTo1RetrievalLegacy: '根据产品规划,N 选 1 召回将于 9 月正式弃用。在那之前,您仍然可以正常使用它。',
 }
 
 export default translation

+ 43 - 0
web/models/datasets.ts

@@ -449,3 +449,46 @@ export type ErrorDocsResponse = {
   data: IndexingStatusResponse[]
   total: number
 }
+
+export type SelectedDatasetsMode = {
+  allHighQuality: boolean
+  allHighQualityVectorSearch: boolean
+  allHighQualityFullTextSearch: boolean
+  allEconomic: boolean
+  mixtureHighQualityAndEconomic: boolean
+  inconsistentEmbeddingModel: boolean
+}
+
+export enum WeightedScoreEnum {
+  SemanticFirst = 'semantic_first',
+  KeywordFirst = 'keyword_first',
+  Customized = 'customized',
+}
+
+export enum RerankingModeEnum {
+  RerankingModel = 'reranking_model',
+  WeightedScore = 'weighted_score',
+}
+
+export const DEFAULT_WEIGHTED_SCORE = {
+  allHighQualityVectorSearch: {
+    semantic: 1.0,
+    keyword: 0,
+  },
+  allHighQualityFullTextSearch: {
+    semantic: 0,
+    keyword: 1.0,
+  },
+  semanticFirst: {
+    semantic: 0.7,
+    keyword: 0.3,
+  },
+  keywordFirst: {
+    semantic: 0.3,
+    keyword: 0.7,
+  },
+  other: {
+    semantic: 0.7,
+    keyword: 0.3,
+  },
+}

+ 18 - 1
web/models/debug.ts

@@ -1,4 +1,8 @@
 import type { AgentStrategy, ModelModeType, RETRIEVE_TYPE, ToolItem, TtsAutoPlay } from '@/types/app'
+import type {
+  RerankingModeEnum,
+  WeightedScoreEnum,
+} from '@/models/datasets'
 export type Inputs = Record<string, string | number | object>
 
 export enum PromptMode {
@@ -144,13 +148,26 @@ export type DatasetConfigs = {
   }
   top_k: number
   score_threshold_enabled: boolean
-  score_threshold?: number | null
+  score_threshold: number | null | undefined
   datasets: {
     datasets: {
       enabled: boolean
       id: string
     }[]
   }
+  reranking_mode?: RerankingModeEnum
+  weights?: {
+    weight_type: WeightedScoreEnum
+    vector_setting: {
+      vector_weight: number
+      embedding_provider_name: string
+      embedding_model_name: string
+    }
+    keyword_setting: {
+      keyword_weight: number
+    }
+  }
+  reranking_enable?: boolean
 }
 
 export type DebugRequestBody = {

+ 16 - 0
web/types/app.ts

@@ -2,6 +2,10 @@ import type { AnnotationReplyConfig, ChatPromptConfig, CompletionPromptConfig, D
 import type { CollectionType } from '@/app/components/tools/types'
 import type { LanguagesSupported } from '@/i18n/language'
 import type { Tag } from '@/app/components/base/tag-management/constant'
+import type {
+  RerankingModeEnum,
+  WeightedScoreEnum,
+} from '@/models/datasets'
 
 export enum Theme {
   light = 'light',
@@ -403,4 +407,16 @@ export type RetrievalConfig = {
   top_k: number
   score_threshold_enabled: boolean
   score_threshold: number
+  reranking_mode?: RerankingModeEnum
+  weights?: {
+    weight_type: WeightedScoreEnum
+    vector_setting: {
+      vector_weight: number
+      embedding_provider_name: string
+      embedding_model_name: string
+    }
+    keyword_setting: {
+      keyword_weight: number
+    }
+  }
 }