Преглед изворни кода

Add code view for search request (#512)

Signed-off-by: ryjiang <jiangruiyi@gmail.com>
ryjiang пре 1 година
родитељ
комит
2be74e79ee

+ 9 - 9
client/src/components/code/CodeBlock.tsx

@@ -2,29 +2,28 @@ import { makeStyles, Theme } from '@material-ui/core';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import CopyButton from '../advancedSearch/CopyButton';
 import CopyButton from '../advancedSearch/CopyButton';
 import SyntaxHighlighter from 'react-syntax-highlighter';
 import SyntaxHighlighter from 'react-syntax-highlighter';
-import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
+import { githubGist } from 'react-syntax-highlighter/dist/esm/styles/hljs';
 import { FC } from 'react';
 import { FC } from 'react';
 import { CodeBlockProps } from './Types';
 import { CodeBlockProps } from './Types';
 
 
 const getStyles = makeStyles((theme: Theme) => ({
 const getStyles = makeStyles((theme: Theme) => ({
   wrapper: {
   wrapper: {
     position: 'relative',
     position: 'relative',
-    padding: theme.spacing(3),
     backgroundColor: '#fff',
     backgroundColor: '#fff',
     color: '#454545',
     color: '#454545',
   },
   },
-  block: {
-    margin: 0,
-  },
   copy: {
   copy: {
     position: 'absolute',
     position: 'absolute',
-    top: theme.spacing(2),
-    right: theme.spacing(2),
+    top: theme.spacing(1),
+    right: theme.spacing(1),
+    '& svg': {
+      width: 16,
+    },
   },
   },
 }));
 }));
 
 
 const CodeStyle = {
 const CodeStyle = {
-  backgroundColor: '#fff',
+  backgroundColor: '#f5f5f5',
   padding: 0,
   padding: 0,
   margin: 0,
   margin: 0,
   marginRight: 32,
   marginRight: 32,
@@ -50,8 +49,9 @@ const CodeBlock: FC<CodeBlockProps> = ({
       />
       />
       <SyntaxHighlighter
       <SyntaxHighlighter
         language={language}
         language={language}
-        style={docco}
+        style={githubGist}
         customStyle={CodeStyle}
         customStyle={CodeStyle}
+        showLineNumbers={true}
       >
       >
         {code}
         {code}
       </SyntaxHighlighter>
       </SyntaxHighlighter>

+ 1 - 1
client/src/components/code/Types.ts

@@ -13,7 +13,7 @@ export enum CodeLanguageEnum {
 
 
 export interface CodeBlockProps {
 export interface CodeBlockProps {
   code: string;
   code: string;
-  language: CodeLanguageEnum;
+  language: string;
   wrapperClass?: string;
   wrapperClass?: string;
 }
 }
 
 

+ 18 - 0
client/src/components/icons/Icons.tsx

@@ -800,6 +800,7 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
       viewBox="0 0 15 15"
       viewBox="0 0 15 15"
       fill="none"
       fill="none"
       xmlns="http://www.w3.org/2000/svg"
       xmlns="http://www.w3.org/2000/svg"
+      {...props}
     >
     >
       <path
       <path
         d="M13.9 0.499976C13.9 0.279062 13.7209 0.0999756 13.5 0.0999756C13.2791 0.0999756 13.1 0.279062 13.1 0.499976V1.09998H12.5C12.2791 1.09998 12.1 1.27906 12.1 1.49998C12.1 1.72089 12.2791 1.89998 12.5 1.89998H13.1V2.49998C13.1 2.72089 13.2791 2.89998 13.5 2.89998C13.7209 2.89998 13.9 2.72089 13.9 2.49998V1.89998H14.5C14.7209 1.89998 14.9 1.72089 14.9 1.49998C14.9 1.27906 14.7209 1.09998 14.5 1.09998H13.9V0.499976ZM11.8536 3.14642C12.0488 3.34168 12.0488 3.65826 11.8536 3.85353L10.8536 4.85353C10.6583 5.04879 10.3417 5.04879 10.1465 4.85353C9.9512 4.65827 9.9512 4.34169 10.1465 4.14642L11.1464 3.14643C11.3417 2.95116 11.6583 2.95116 11.8536 3.14642ZM9.85357 5.14642C10.0488 5.34168 10.0488 5.65827 9.85357 5.85353L2.85355 12.8535C2.65829 13.0488 2.34171 13.0488 2.14645 12.8535C1.95118 12.6583 1.95118 12.3417 2.14645 12.1464L9.14646 5.14642C9.34172 4.95116 9.65831 4.95116 9.85357 5.14642ZM13.5 5.09998C13.7209 5.09998 13.9 5.27906 13.9 5.49998V6.09998H14.5C14.7209 6.09998 14.9 6.27906 14.9 6.49998C14.9 6.72089 14.7209 6.89998 14.5 6.89998H13.9V7.49998C13.9 7.72089 13.7209 7.89998 13.5 7.89998C13.2791 7.89998 13.1 7.72089 13.1 7.49998V6.89998H12.5C12.2791 6.89998 12.1 6.72089 12.1 6.49998C12.1 6.27906 12.2791 6.09998 12.5 6.09998H13.1V5.49998C13.1 5.27906 13.2791 5.09998 13.5 5.09998ZM8.90002 0.499976C8.90002 0.279062 8.72093 0.0999756 8.50002 0.0999756C8.2791 0.0999756 8.10002 0.279062 8.10002 0.499976V1.09998H7.50002C7.2791 1.09998 7.10002 1.27906 7.10002 1.49998C7.10002 1.72089 7.2791 1.89998 7.50002 1.89998H8.10002V2.49998C8.10002 2.72089 8.2791 2.89998 8.50002 2.89998C8.72093 2.89998 8.90002 2.72089 8.90002 2.49998V1.89998H9.50002C9.72093 1.89998 9.90002 1.72089 9.90002 1.49998C9.90002 1.27906 9.72093 1.09998 9.50002 1.09998H8.90002V0.499976Z"
         d="M13.9 0.499976C13.9 0.279062 13.7209 0.0999756 13.5 0.0999756C13.2791 0.0999756 13.1 0.279062 13.1 0.499976V1.09998H12.5C12.2791 1.09998 12.1 1.27906 12.1 1.49998C12.1 1.72089 12.2791 1.89998 12.5 1.89998H13.1V2.49998C13.1 2.72089 13.2791 2.89998 13.5 2.89998C13.7209 2.89998 13.9 2.72089 13.9 2.49998V1.89998H14.5C14.7209 1.89998 14.9 1.72089 14.9 1.49998C14.9 1.27906 14.7209 1.09998 14.5 1.09998H13.9V0.499976ZM11.8536 3.14642C12.0488 3.34168 12.0488 3.65826 11.8536 3.85353L10.8536 4.85353C10.6583 5.04879 10.3417 5.04879 10.1465 4.85353C9.9512 4.65827 9.9512 4.34169 10.1465 4.14642L11.1464 3.14643C11.3417 2.95116 11.6583 2.95116 11.8536 3.14642ZM9.85357 5.14642C10.0488 5.34168 10.0488 5.65827 9.85357 5.85353L2.85355 12.8535C2.65829 13.0488 2.34171 13.0488 2.14645 12.8535C1.95118 12.6583 1.95118 12.3417 2.14645 12.1464L9.14646 5.14642C9.34172 4.95116 9.65831 4.95116 9.85357 5.14642ZM13.5 5.09998C13.7209 5.09998 13.9 5.27906 13.9 5.49998V6.09998H14.5C14.7209 6.09998 14.9 6.27906 14.9 6.49998C14.9 6.72089 14.7209 6.89998 14.5 6.89998H13.9V7.49998C13.9 7.72089 13.7209 7.89998 13.5 7.89998C13.2791 7.89998 13.1 7.72089 13.1 7.49998V6.89998H12.5C12.2791 6.89998 12.1 6.72089 12.1 6.49998C12.1 6.27906 12.2791 6.09998 12.5 6.09998H13.1V5.49998C13.1 5.27906 13.2791 5.09998 13.5 5.09998ZM8.90002 0.499976C8.90002 0.279062 8.72093 0.0999756 8.50002 0.0999756C8.2791 0.0999756 8.10002 0.279062 8.10002 0.499976V1.09998H7.50002C7.2791 1.09998 7.10002 1.27906 7.10002 1.49998C7.10002 1.72089 7.2791 1.89998 7.50002 1.89998H8.10002V2.49998C8.10002 2.72089 8.2791 2.89998 8.50002 2.89998C8.72093 2.89998 8.90002 2.72089 8.90002 2.49998V1.89998H9.50002C9.72093 1.89998 9.90002 1.72089 9.90002 1.49998C9.90002 1.27906 9.72093 1.09998 9.50002 1.09998H8.90002V0.499976Z"
@@ -809,6 +810,23 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
       ></path>
       ></path>
     </svg>
     </svg>
   ),
   ),
+  code: (props = {}) => (
+    <svg
+      width="15"
+      height="15"
+      viewBox="0 0 15 15"
+      fill="none"
+      xmlns="http://www.w3.org/2000/svg"
+      {...props}
+    >
+      <path
+        d="M9.96424 2.68571C10.0668 2.42931 9.94209 2.13833 9.6857 2.03577C9.4293 1.93322 9.13832 2.05792 9.03576 2.31432L5.03576 12.3143C4.9332 12.5707 5.05791 12.8617 5.3143 12.9642C5.5707 13.0668 5.86168 12.9421 5.96424 12.6857L9.96424 2.68571ZM3.85355 5.14646C4.04882 5.34172 4.04882 5.6583 3.85355 5.85356L2.20711 7.50001L3.85355 9.14646C4.04882 9.34172 4.04882 9.6583 3.85355 9.85356C3.65829 10.0488 3.34171 10.0488 3.14645 9.85356L1.14645 7.85356C0.951184 7.6583 0.951184 7.34172 1.14645 7.14646L3.14645 5.14646C3.34171 4.9512 3.65829 4.9512 3.85355 5.14646ZM11.1464 5.14646C11.3417 4.9512 11.6583 4.9512 11.8536 5.14646L13.8536 7.14646C14.0488 7.34172 14.0488 7.6583 13.8536 7.85356L11.8536 9.85356C11.6583 10.0488 11.3417 10.0488 11.1464 9.85356C10.9512 9.6583 10.9512 9.34172 11.1464 9.14646L12.7929 7.50001L11.1464 5.85356C10.9512 5.6583 10.9512 5.34172 11.1464 5.14646Z"
+        fill="currentColor"
+        fillRule="evenodd"
+        clipRule="evenodd"
+      ></path>
+    </svg>
+  ),
 };
 };
 
 
 export default icons;
 export default icons;

+ 2 - 1
client/src/components/icons/Types.ts

@@ -51,4 +51,5 @@ export type IconsType =
   | 'check'
   | 'check'
   | 'discord'
   | 'discord'
   | 'star'
   | 'star'
-  | 'magic';
+  | 'magic'
+  | 'code';

+ 35 - 43
client/src/pages/databases/collections/search/Search.tsx

@@ -1,4 +1,4 @@
-import { useState, useMemo, ChangeEvent, useCallback } from 'react';
+import { useState, useMemo, ChangeEvent, useCallback, useContext } from 'react';
 import {
 import {
   Typography,
   Typography,
   Accordion,
   Accordion,
@@ -8,6 +8,7 @@ import {
 } from '@material-ui/core';
 } from '@material-ui/core';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { DataService } from '@/http';
 import { DataService } from '@/http';
+import { rootContext } from '@/context';
 import Icons from '@/components/icons/Icons';
 import Icons from '@/components/icons/Icons';
 import AttuGrid from '@/components/grid/Grid';
 import AttuGrid from '@/components/grid/Grid';
 import Filter from '@/components/advancedSearch';
 import Filter from '@/components/advancedSearch';
@@ -23,10 +24,11 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
 import CustomInput from '@/components/customInput/CustomInput';
 import CustomInput from '@/components/customInput/CustomInput';
 import {
 import {
   formatFieldType,
   formatFieldType,
-  VectorStrToObject,
   cloneObj,
   cloneObj,
   generateVectorsByField,
   generateVectorsByField,
   saveCsvAs,
   saveCsvAs,
+  buildSearchParams,
+  buildSearchCode,
 } from '@/utils';
 } from '@/utils';
 import SearchParams from '../../../search/SearchParams';
 import SearchParams from '../../../search/SearchParams';
 import {
 import {
@@ -37,6 +39,7 @@ import {
 import { DYNAMIC_FIELD } from '@/consts';
 import { DYNAMIC_FIELD } from '@/consts';
 import { ColDefinitionsType } from '@/components/grid/Types';
 import { ColDefinitionsType } from '@/components/grid/Types';
 import { CollectionObject, CollectionFullObject } from '@server/types';
 import { CollectionObject, CollectionFullObject } from '@server/types';
+import CodeDialog from '@/pages/dialogs/CodeDialog';
 
 
 export interface CollectionDataProps {
 export interface CollectionDataProps {
   collectionName: string;
   collectionName: string;
@@ -52,6 +55,9 @@ const Search = (props: CollectionDataProps) => {
     i => i.collection_name === collectionName
     i => i.collection_name === collectionName
   ) as CollectionFullObject;
   ) as CollectionFullObject;
 
 
+  // context
+  const { setDialog } = useContext(rootContext);
+
   // UI states
   // UI states
   const [tableLoading, setTableLoading] = useState<boolean>();
   const [tableLoading, setTableLoading] = useState<boolean>();
   const [highlightField, setHighlightField] = useState<string>('');
   const [highlightField, setHighlightField] = useState<string>('');
@@ -161,47 +167,7 @@ const Search = (props: CollectionDataProps) => {
 
 
   // execute search
   // execute search
   const onSearchClicked = useCallback(async () => {
   const onSearchClicked = useCallback(async () => {
-    const data: any = [];
-    const weightedParams: number[] = [];
-
-    searchParams.searchParams.forEach((s, index) => {
-      const formatter =
-        VectorStrToObject[s.field.data_type as keyof typeof VectorStrToObject];
-      if (s.selected) {
-        data.push({
-          anns_field: s.field.name,
-          data: formatter(s.data),
-          params: s.params,
-        });
-        weightedParams.push(
-          searchParams.globalParams.weightedParams.weights[index]
-        );
-      }
-    });
-
-    const params: any = {
-      output_fields: outputFields,
-      limit: searchParams.globalParams.topK,
-      data: data,
-      filter: searchParams.globalParams.filter,
-      consistency_level: searchParams.globalParams.consistency_level,
-    };
-
-    // reranker if exists
-    if (data.length > 1) {
-      if (searchParams.globalParams.rerank === 'rrf') {
-        params.rerank = {
-          strategy: 'rrf',
-          params:  searchParams.globalParams.rrfParams,
-        };
-      }
-      if (searchParams.globalParams.rerank === 'weighted') {
-        params.rerank = {
-          strategy: 'weighted',
-          params: { weights: weightedParams },
-        };
-      }
-    }
+    const params = buildSearchParams(searchParams, outputFields);
 
 
     setTableLoading(true);
     setTableLoading(true);
     try {
     try {
@@ -491,6 +457,32 @@ const Search = (props: CollectionDataProps) => {
                 />
                 />
               </div>
               </div>
               <div className="right">
               <div className="right">
+                <CustomButton
+                  className="btn"
+                  disabled={disableSearch}
+                  onClick={() => {
+                    // open code dialog
+                    setDialog({
+                      open: true,
+                      type: 'custom',
+                      params: {
+                        component: (
+                          <CodeDialog
+                            data={buildSearchCode(
+                              searchParams,
+                              outputFields,
+                              collection
+                            )}
+                          />
+                        ),
+                      },
+                    });
+                  }}
+                  startIcon={<Icons.code classes={{ root: 'icon' }} />}
+                >
+                  {btnTrans('Code')}
+                </CustomButton>
+
                 <CustomButton
                 <CustomButton
                   className="btn"
                   className="btn"
                   disabled={result.length === 0}
                   disabled={result.length === 0}

+ 78 - 0
client/src/pages/dialogs/CodeDialog.tsx

@@ -0,0 +1,78 @@
+import { FC, useContext, useState } from 'react';
+import { makeStyles, Theme } from '@material-ui/core';
+import { useTranslation } from 'react-i18next';
+import { rootContext } from '@/context';
+import DialogTemplate from '@/components/customDialog/DialogTemplate';
+import CodeBlock from '@/components/code/CodeBlock';
+import CustomSelector from '@/components/customSelector/CustomSelector';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  code: {
+    backgroundColor: '#f5f5f5',
+    padding: 8,
+    width: '40vw',
+    minHeight: '40vh',
+    maxHeight: '40vh',
+    overflow: 'auto',
+  },
+}));
+
+type CodeDialogProps = {
+  data: { [key: string]: string };
+};
+
+const CodeDialog: FC<CodeDialogProps> = props => {
+  // props
+  const langauges = Object.keys(props.data);
+
+  // build options
+  const options = langauges.map(lang => ({
+    label: lang,
+    value: lang,
+  }));
+  // styles
+  const classes = useStyles();
+  // states
+  const [langauge, setLanguage] = useState(langauges[0]);
+  const code = props.data[langauge];
+
+  // context
+  const { handleCloseDialog } = useContext(rootContext);
+  // translations
+  const { t: btnTrans } = useTranslation('btn');
+
+  const disabled = false;
+
+  return (
+    <DialogTemplate
+      title={'Code View(beta)'}
+      handleClose={handleCloseDialog}
+      children={
+        <>
+          <CustomSelector
+            label="Language"
+            value={langauge}
+            onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
+              setLanguage(event.target.value as string);
+            }}
+            options={options}
+            variant="filled"
+          />
+          <CodeBlock
+            code={code}
+            language={langauge === 'node.js' ? 'javascript' : langauge}
+            wrapperClass={classes.code}
+          />
+        </>
+      }
+      confirmLabel={btnTrans('Ok')}
+      handleConfirm={() => {
+        handleCloseDialog();
+      }}
+      showCancel={false}
+      confirmDisabled={disabled}
+    />
+  );
+};
+
+export default CodeDialog;

+ 0 - 1
client/src/utils/Common.ts

@@ -1,6 +1,5 @@
 import { saveAs } from 'file-saver';
 import { saveAs } from 'file-saver';
 import { Parser } from '@json2csv/plainjs';
 import { Parser } from '@json2csv/plainjs';
-import { csv } from 'd3';
 
 
 export const copyToCommand = (
 export const copyToCommand = (
   value: string,
   value: string,

+ 155 - 1
client/src/utils/search.ts

@@ -1,5 +1,7 @@
 import { FieldOption } from '../types/SearchTypes';
 import { FieldOption } from '../types/SearchTypes';
-import { FieldObject } from '@server/types';
+import { FieldObject, CollectionFullObject } from '@server/types';
+import { VectorStrToObject } from '@/utils';
+import { SearchParams } from '@/pages/databases/types';
 
 
 export const getVectorFieldOptions = (fields: FieldObject[]): FieldOption[] => {
 export const getVectorFieldOptions = (fields: FieldObject[]): FieldOption[] => {
   const options: FieldOption[] = fields.map(f => {
   const options: FieldOption[] = fields.map(f => {
@@ -12,3 +14,155 @@ export const getVectorFieldOptions = (fields: FieldObject[]): FieldOption[] => {
 
 
   return options;
   return options;
 };
 };
+
+// new search: build search params
+export const buildSearchParams = (
+  searchParams: SearchParams,
+  output_fields: string[]
+) => {
+  const data: any = [];
+  const weightedParams: number[] = [];
+
+  searchParams.searchParams.forEach((s, index) => {
+    const formatter =
+      VectorStrToObject[s.field.data_type as keyof typeof VectorStrToObject];
+    if (s.selected) {
+      data.push({
+        anns_field: s.field.name,
+        data: formatter(s.data),
+        params: s.params,
+      });
+      weightedParams.push(
+        searchParams.globalParams.weightedParams.weights[index]
+      );
+    }
+  });
+
+  const params: any = {
+    output_fields: output_fields,
+    limit: searchParams.globalParams.topK,
+    data: data,
+    filter: searchParams.globalParams.filter,
+    consistency_level: searchParams.globalParams.consistency_level,
+  };
+
+  // reranker if exists
+  if (data.length > 1) {
+    if (searchParams.globalParams.rerank === 'rrf') {
+      params.rerank = {
+        strategy: 'rrf',
+        params: searchParams.globalParams.rrfParams,
+      };
+    }
+    if (searchParams.globalParams.rerank === 'weighted') {
+      params.rerank = {
+        strategy: 'weighted',
+        params: { weights: weightedParams },
+      };
+    }
+  }
+
+  return params;
+};
+
+export const buildSearchCode = (
+  searchParams: SearchParams,
+  output_fields: string[],
+  collection: CollectionFullObject
+) => {
+  const params = buildSearchParams(searchParams, output_fields);
+  const isMultiple = params.data.length > 1;
+
+  return {
+    python: isMultiple
+      ? buildMultipleSearchPythonCode(params, collection)
+      : buildSingleSearchPythonCode(params, collection),
+    ['node.js']: buildNodeSearchCode(params, collection),
+  };
+};
+
+export const buildSingleSearchPythonCode = (
+  params: any,
+  collection: CollectionFullObject
+) => {
+  const code = `# python search code
+res = client.search(
+  collection_name="${collection.collection_name}", # Collection name
+  data=query_vector, # Replace with your query vector
+  search_params={
+    "metric_type": "${collection.schema.vectorFields[0].index.metricType}",
+    "params": ${JSON.stringify(params.data[0].params)}, # Search parameters
+  }, # Search parameters
+  limit=${params.limit}, # Max. number of search results to return
+  output_fields=${JSON.stringify(
+    params.output_fields
+  )}, # Fields to return in the search results
+  consistency_level="${params.consistency_level}"
+)
+`;
+
+  return code;
+};
+
+export const buildMultipleSearchPythonCode = (
+  params: any,
+  collection: CollectionFullObject
+) => {
+  const code = `from pymilvus import AnnSearchRequest, RRFRanker, WeightedRanker`;
+
+  // build request
+  const data = params.data.map((d: any, i: number) => {
+    const req = `search_param_${i} = {
+    "data": query_vector, # Query vector
+    "anns_field": "${d.anns_field}", # Vector field name
+    "param": {
+        "metric_type": "${
+          collection.schema.vectorFields[i].index.metricType
+        }", # This parameter value must be identical to the one used in the collection schema
+        "params": ${JSON.stringify(d.params)}, # Search parameters
+    }
+  }`;
+    return `${req}\nrequest_${i} = AnnSearchRequest(**search_param_${i})`;
+  });
+
+  let reranker = '';
+  // reranks
+  if (params.rerank) {
+    if (params.rerank.strategy === 'rrf') {
+      reranker = `# Rerank by RRF strategy\nrerank = RRFRanker(k=${params.rerank.params.k})`;
+    }
+    if (params.rerank.strategy === 'weighted') {
+      reranker = `# Rerank by Weighted strategy\nrerank = WeightedRanker(${JSON.stringify(
+        params.rerank.params.weights
+      )})`;
+    }
+  }
+
+  return `${code}\n\n${data.join('\n\n')}\n\nreqs = [${Array.from(
+    { length: params.data.length },
+    (_, i) => `request_${i}`
+  ).join(
+    ', '
+  )}]\n${reranker}\n\n# Perform search\nres = client.search(reqs, rerank, limit=${
+    params.limit
+  }, output_fields=${JSON.stringify(
+    params.output_fields
+  )}, consistency_level="${params.consistency_level}")`;
+};
+
+export const buildNodeSearchCode = (
+  params: any,
+  collection: CollectionFullObject
+) => {
+  // remove data.data
+  params.data.forEach((d: any) => {
+    d.data = `YOUR_QUERY_VECTOR`;
+  });
+
+  return `// nodejs search code
+const params = ${JSON.stringify(
+    { collection_name: collection.collection_name, ...params },
+    null,
+    2
+  )};\nawait milvusClient.search(params)`;
+};