Repository: opensearch-project/dashboards-flow-framework
Pull request #683: add callout if knn query does not have knn index
Author: login: kamahuan
Labels: name: backport main
Created: 2025-03-24T06:50:33Z | Merged: 2025-03-31T20:11:19Z

### Description
In the search flow, if the query is Knn query and the source index file is not knn, should pop up a callout box.

### Issues Resolved

[682](https://github.com/opensearch-project/dashboards-flow-framework/issues/682)

### Check List

- [ ] Commits are signed per the DCO using `--signoff`

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check [here](https://github.com/opensearch-project/OpenSearch/blob/main/CONTRIBUTING.md#developer-certificate-of-origin).



Diff:
diff --git a/public/component_types/other/search_request.tsx b/public/component_types/other/search_request.tsx
index c125af2d7..4d432edd5 100644
--- a/public/component_types/other/search_request.tsx
+++ b/public/component_types/other/search_request.tsx
@@ -5,6 +5,7 @@
 
 import { COMPONENT_CLASS } from '../../../common';
 import { BaseComponent } from '../base_component';
+import { isKnnIndex, isKnnQuery } from '../../utils/utils';
 
 /**
  * A basic search request placeholder UI component.
@@ -23,4 +24,41 @@ export class SearchRequest extends BaseComponent {
       },
     ];
   }
+
+  /**
+   * Validates if the index supports Knn if it is Knn query
+   * @param query The current search query
+   * @param indexSettings The settings of the selected index
+   * @returns Object containing validation result and warning message if needed
+   */
+  validateKnnQueryToHaveValidKnnIndex(
+    query: string,
+    indexSettings: string
+  ): {
+    isValid: boolean;
+    warningMessage?: string;
+  } {
+    try {
+      const queryString =
+        typeof query === 'string' ? query : JSON.stringify(query);
+      const hasKnnQuery = isKnnQuery(queryString);
+
+      if (hasKnnQuery) {
+        const isKnnEnabled = isKnnIndex(indexSettings);
+
+        if (!isKnnEnabled) {
+          return {
+            isValid: false,
+            warningMessage:
+              'Warning: The selected index does not have KNN enabled. Please selecte a KNN enabled index.',
+          };
+        }
+      }
+
+      return { isValid: true };
+    } catch (error) {
+      console.error('Error validating KNN query:', error);
+      return { isValid: true };
+    }
+  }
 }
diff --git a/public/pages/workflow_detail/workflow_inputs/search_inputs/configure_search_request.tsx b/public/pages/workflow_detail/workflow_inputs/search_inputs/configure_search_request.tsx
index 0bc8fe633..ce5393f0e 100644
--- a/public/pages/workflow_detail/workflow_inputs/search_inputs/configure_search_request.tsx
+++ b/public/pages/workflow_detail/workflow_inputs/search_inputs/configure_search_request.tsx
@@ -16,10 +16,19 @@ import {
   EuiText,
   EuiCodeBlock,
   EuiSmallButtonEmpty,
+  EuiCallOut,
 } from '@elastic/eui';
 import { WorkflowFormValues } from '../../../../../common';
 import { AppState } from '../../../../store';
 import { EditQueryModal } from './edit_query_modal';
+import { SearchRequest } from '../../../../component_types/other';
+import { useAppDispatch } from '../../../../store';
+import { getDataSourceId } from '../../../../utils/utils';
+import { getIndex } from '../../../../store/reducers/opensearch_reducer';
+interface KnnValidationResult {
+  isValid: boolean;
+  warningMessage?: string;
+}
 
 interface ConfigureSearchRequestProps {}
 
@@ -27,6 +36,8 @@ interface ConfigureSearchRequestProps {}
  * Input component for configuring a search request
  */
 export function ConfigureSearchRequest(props: ConfigureSearchRequestProps) {
+  const dispatch = useAppDispatch();
+  const dataSourceId = getDataSourceId();
   // Form state
   const { values, setFieldValue, setFieldTouched } = useFormikContext<
     WorkflowFormValues
@@ -42,6 +53,15 @@ export function ConfigureSearchRequest(props: ConfigureSearchRequestProps) {
     values?.search?.index?.name
   );
 
+  // Add knn validation state
+  const searchRequest = new SearchRequest();
+  const indexDetailsState = useSelector(
+    (state: AppState) => state.opensearch.indexDetails
+  );
+  const [validationResult, setValidationResult] = useState<KnnValidationResult>(
+    { isValid: true }
+  );
+
   // initial load: set the search index value, if not already set
   useEffect(() => {
     if (values?.ingest?.enabled) {
@@ -49,6 +69,44 @@ export function ConfigureSearchRequest(props: ConfigureSearchRequestProps) {
     }
   }, []);
 
+  useEffect(() => {
+    if (selectedIndex) {
+      dispatch(getIndex({ index: selectedIndex, dataSourceId }));
+    }
+  }, [selectedIndex]);
+
+  // validate Knn query
+  useEffect(() => {
+    if (
+      selectedIndex &&
+      values?.search?.request &&
+      indexDetailsState[selectedIndex]
+    ) {
+      try {
+        const indexSettings = JSON.stringify(
+          indexDetailsState[selectedIndex]?.settings || {}
+        );
+
+        // Make sure your query is properly formatted for the validation
+        const queryString =
+          typeof values.search.request === 'string'
+            ? values.search.request
+            : JSON.stringify(values.search.request);
+
+        // Validate the query
+        const result = searchRequest.validateKnnQueryToHaveValidKnnIndex(
+          queryString,
+          indexSettings
+        );
+
+        setValidationResult(result);
+      } catch (error) {
+        console.error('Error validating KNN query:', error);
+        setValidationResult({ isValid: true });
+      }
+    }
+  }, [selectedIndex, values?.search?.request, indexDetailsState]);
+
   // Edit modal state
   const [isEditModalOpen, setIsEditModalOpen] = useState<boolean>(false);
 
@@ -94,6 +152,18 @@ export function ConfigureSearchRequest(props: ConfigureSearchRequestProps) {
             )}
           </EuiCompressedFormRow>
         </EuiFlexItem>
+        {!validationResult.isValid && validationResult.warningMessage && (
+          <EuiFlexItem>
+            <EuiCallOut
+              title="Vector search might not be working"
+              color="warning"
+              iconType="alert"
+              size="s"
+            >
+              {validationResult.warningMessage}
+            </EuiCallOut>
+          </EuiFlexItem>
+        )}
         <EuiFlexItem>
           <EuiFlexGroup direction="row" justifyContent="spaceBetween">
             <EuiFlexItem grow={false}>
diff --git a/public/utils/utils.tsx b/public/utils/utils.tsx
index d6ccc316e..6afec8c7a 100644
--- a/public/utils/utils.tsx
+++ b/public/utils/utils.tsx
@@ -782,6 +782,25 @@ export function isKnnIndex(existingSettings: string): boolean {
     return false;
   }
 }
+// Check if a query is knn query
+
+export function isKnnQuery(queryString: string): boolean {
+  try {
+    const query =
+      typeof queryString === 'string' ? JSON.parse(queryString) : queryString;
+
+    const hasKnn = getFieldValue(query, 'knn') !== undefined;
+    const hasNeural = getFieldValue(query, 'neural') !== undefined;
+
+    return hasKnn || hasNeural;
+  } catch (error) {
+    console.error('Error in isKnnQuery:', error);
+    // Fallback to string-based detection
+    return (
+      queryString.includes('"knn":{') || queryString.includes('"neural":{')
+    );
+  }
+}
 
 // Update the index settings based on parameters passed.
 // Currently just used for updating the `knn` flag.


Reviews:
user: login: ohltyler
body: Thanks for the enhancement! Can you add a screenshot? Additionally, can you ensure to test all of the scenarios that it works as expected?
1. knn index + knn query
2. knn index + non-knn query
3. non-knn index + knn query
4. non-knn index + non-knn query
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T22:55:17Z
user: login: ohltyler
body: LGTM! Thanks for adding.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-31T19:36:46Z

Review comments:
diff_hunk: @@ -23,4 +24,68 @@ export class SearchRequest extends BaseComponent {
       },
     ];
   }
+
+  /**
+   * Validates if the index supports Knn if it is Knn query
+   * @param query The current search query
+   * @param indexSettings The settings of the selected index
+   * @returns Object containing validation result and warning message if needed
+   */
+  validateKnnQueryToHaveValidKnnIndex(
+    query: string,
+    indexSettings: string
+  ): {
+    isValid: boolean;
+    warningMessage?: string;
+  } {
+    try {
+      const queryString =
+        typeof query === 'string' ? query : JSON.stringify(query);
+      const hasKnnQuery = this.isKnnQuery(queryString);
+
+      if (hasKnnQuery) {
+        const isKnnEnabled = isKnnIndex(indexSettings);
+
+        if (!isKnnEnabled) {
+          return {
+            isValid: false,
+            warningMessage:
+              'Warning: You are using a neural/KNN query, but the selected index does not have KNN enabled (index.knn:true). Please select a KNN-enabled index for proper vector search functionality.',
+          };
+        }
+      }
+
+      return { isValid: true };
+    } catch (error) {
+      console.error('Error validating KNN query:', error);
+      return { isValid: true };
+    }
+  }
+
+  /**
+   * Determines if a query is a KNN query
+   * @param queryString The query string to check
+   * @returns boolean indicating if it's a KNN query
+   */
+  isKnnQuery(queryString: string): boolean {
+    try {
+      const query =
+        typeof queryString === 'string' ? JSON.parse(queryString) : queryString;
+
+      const queryAsString = JSON.stringify(query);
+
+      const hasKnn = queryAsString.includes('"knn":{');
+      const hasNeural = queryAsString.includes('"neural":{');
+
+      console.log('KNN query detection:', { hasKnn, hasNeural, queryAsString });
path: public/component_types/other/search_request.tsx
user: login: ohltyler
body: leftover log statement
created_at: 2025-03-24T22:39:35Z
author_association: MEMBER
diff_hunk: @@ -23,4 +24,68 @@ export class SearchRequest extends BaseComponent {
       },
     ];
   }
+
+  /**
+   * Validates if the index supports Knn if it is Knn query
+   * @param query The current search query
+   * @param indexSettings The settings of the selected index
+   * @returns Object containing validation result and warning message if needed
+   */
+  validateKnnQueryToHaveValidKnnIndex(
+    query: string,
+    indexSettings: string
+  ): {
+    isValid: boolean;
+    warningMessage?: string;
+  } {
+    try {
+      const queryString =
+        typeof query === 'string' ? query : JSON.stringify(query);
+      const hasKnnQuery = this.isKnnQuery(queryString);
+
+      if (hasKnnQuery) {
+        const isKnnEnabled = isKnnIndex(indexSettings);
+
+        if (!isKnnEnabled) {
+          return {
+            isValid: false,
+            warningMessage:
+              'Warning: You are using a neural/KNN query, but the selected index does not have KNN enabled (index.knn:true). Please select a KNN-enabled index for proper vector search functionality.',
+          };
+        }
+      }
+
+      return { isValid: true };
+    } catch (error) {
+      console.error('Error validating KNN query:', error);
+      return { isValid: true };
+    }
+  }
+
+  /**
+   * Determines if a query is a KNN query
+   * @param queryString The query string to check
+   * @returns boolean indicating if it's a KNN query
+   */
+  isKnnQuery(queryString: string): boolean {
path: public/component_types/other/search_request.tsx
user: login: ohltyler
body: nit: suggest to move this next to `isKnnIndex()` in the utils file for discoverability and potential reusability in the future.
created_at: 2025-03-24T22:42:40Z
author_association: MEMBER
diff_hunk: @@ -23,4 +24,68 @@ export class SearchRequest extends BaseComponent {
       },
     ];
   }
+
+  /**
+   * Validates if the index supports Knn if it is Knn query
+   * @param query The current search query
+   * @param indexSettings The settings of the selected index
+   * @returns Object containing validation result and warning message if needed
+   */
+  validateKnnQueryToHaveValidKnnIndex(
+    query: string,
+    indexSettings: string
+  ): {
+    isValid: boolean;
+    warningMessage?: string;
+  } {
+    try {
+      const queryString =
+        typeof query === 'string' ? query : JSON.stringify(query);
+      const hasKnnQuery = this.isKnnQuery(queryString);
+
+      if (hasKnnQuery) {
+        const isKnnEnabled = isKnnIndex(indexSettings);
+
+        if (!isKnnEnabled) {
+          return {
+            isValid: false,
+            warningMessage:
+              'Warning: You are using a neural/KNN query, but the selected index does not have KNN enabled (index.knn:true). Please select a KNN-enabled index for proper vector search functionality.',
+          };
+        }
+      }
+
+      return { isValid: true };
+    } catch (error) {
+      console.error('Error validating KNN query:', error);
+      return { isValid: true };
+    }
+  }
+
+  /**
+   * Determines if a query is a KNN query
+   * @param queryString The query string to check
+   * @returns boolean indicating if it's a KNN query
+   */
+  isKnnQuery(queryString: string): boolean {
+    try {
+      const query =
+        typeof queryString === 'string' ? JSON.parse(queryString) : queryString;
+
+      const queryAsString = JSON.stringify(query);
+
+      const hasKnn = queryAsString.includes('"knn":{');
+      const hasNeural = queryAsString.includes('"neural":{');
path: public/component_types/other/search_request.tsx
user: login: ohltyler
body: How about leveraging the existing util fn `getFieldValue()` on the parsed query to check if the `knn` or `neural` property exists? This would be a cleaner way than depending on a string pattern, and it wouldn't require any string parsing on line 75.
created_at: 2025-03-24T22:47:18Z
author_association: MEMBER
diff_hunk: @@ -23,4 +24,68 @@ export class SearchRequest extends BaseComponent {
       },
     ];
   }
+
+  /**
+   * Validates if the index supports Knn if it is Knn query
+   * @param query The current search query
+   * @param indexSettings The settings of the selected index
+   * @returns Object containing validation result and warning message if needed
+   */
+  validateKnnQueryToHaveValidKnnIndex(
+    query: string,
+    indexSettings: string
+  ): {
+    isValid: boolean;
+    warningMessage?: string;
+  } {
+    try {
+      const queryString =
+        typeof query === 'string' ? query : JSON.stringify(query);
+      const hasKnnQuery = this.isKnnQuery(queryString);
+
+      if (hasKnnQuery) {
+        const isKnnEnabled = isKnnIndex(indexSettings);
+
+        if (!isKnnEnabled) {
+          return {
+            isValid: false,
+            warningMessage:
+              'Warning: You are using a neural/KNN query, but the selected index does not have KNN enabled (index.knn:true). Please select a KNN-enabled index for proper vector search functionality.',
+          };
+        }
+      }
+
+      return { isValid: true };
+    } catch (error) {
+      console.error('Error validating KNN query:', error);
+      return { isValid: true };
+    }
+  }
+
+  /**
+   * Determines if a query is a KNN query
+   * @param queryString The query string to check
+   * @returns boolean indicating if it's a KNN query
+   */
+  isKnnQuery(queryString: string): boolean {
+    try {
+      const query =
+        typeof queryString === 'string' ? JSON.parse(queryString) : queryString;
+
+      const queryAsString = JSON.stringify(query);
+
+      const hasKnn = queryAsString.includes('"knn":{');
+      const hasNeural = queryAsString.includes('"neural":{');
+
+      console.log('KNN query detection:', { hasKnn, hasNeural, queryAsString });
+
+      return hasKnn || hasNeural;
+    } catch (error) {
+      console.error('Error in isKnnQuery:', error);
+      // Fallback to string-based detection
+      return (
+        queryString.includes('"knn":{') || queryString.includes('"neural":{')
path: public/component_types/other/search_request.tsx
user: login: ohltyler
body: same as above comment

created_at: 2025-03-24T22:48:02Z
author_association: MEMBER

PR comments:
user: login: ohltyler
created_at: 2025-03-24T23:01:49Z
body: Also, have you confirmed the wording with UX? Not sure if the callout should just be about the query. The issue could equally be about the index.
author_association: MEMBER
user: login: kamahuan
created_at: 2025-03-27T18:38:18Z
body: > Thanks for the enhancement! Can you add a screenshot? Additionally, can you ensure to test all of the scenarios that it works as expected?
> 
> 1. knn index + knn query
> 2. knn index + non-knn query
> 3. non-knn index + knn query
> 4. non-knn index + non-knn query

Hey Tyler, the below are my screenshots:
Knn index used:`test-index-kama0327`
![Xnip2025-03-27_10-56-52](https://github.com/user-attachments/assets/3c945ad7-6d97-4d77-8f51-dfcbcd673838)
Non Knn index used: `standard-index`

![Xnip2025-03-27_11-14-46](https://github.com/user-attachments/assets/1d47e084-8f8f-4f0e-ba8c-58ae77110201)

case1: Knn index + Knn query

![Xnip2025-03-27_10-58-24](https://github.com/user-attachments/assets/55da4d2f-7442-4dd2-a43d-42c29c9ccbd7)

case2: Knn index + non Knn query
![Xnip2025-03-27_11-07-53](https://github.com/user-attachments/assets/4257ba36-103b-4259-9183-2535d97557f7)

case3: non knn index + knn query: what this PR tests
![Xnip2025-03-27_11-15-39](https://github.com/user-attachments/assets/5775b960-eede-44d0-8ad9-729ec3573084)

case4: non knn index + non knn query


![Xnip2025-03-27_11-17-17](https://github.com/user-attachments/assets/5cd4fdca-348d-4216-a6fe-fafc77977144)

author_association: CONTRIBUTOR
user: login: kamahuan
created_at: 2025-03-27T18:41:07Z
body: > Also, have you confirmed the wording with UX? Not sure if the callout should just be about the query. The issue could equally be about the index.

Yes, confirmed with UX team. The wording should be like this: 

![Xnip2025-03-27_11-39-42](https://github.com/user-attachments/assets/4994fc0d-7362-4590-afec-dc8de43468ce)

author_association: CONTRIBUTOR
user: login: ohltyler
created_at: 2025-03-27T22:54:12Z
body: Got it, thanks for the screenshots, makes sense. I suppose the inverse (non-knn query + knn index) we can't necessarily say is invalid, since query rewrites may eventually lead to a knn query, or the user is simply running a non-knn query which is valid by itself.

Will re-review once there are new commits pushed.
author_association: MEMBER
user: login: kamahuan
created_at: 2025-03-31T20:09:36Z
body: > LGTM! Thanks for adding.

Thank you Tyler. Could you kindly merge and close the PR for me? I do not have permission to merge code
author_association: CONTRIBUTOR