[8.14] [Search] [Playground] Improve UX + fix vector field issue (#182342) (#182383)

# Backport

This will backport the following commits from `main` to `8.14`:
- [[Search] [Playground] Improve UX + fix vector field issue
(#182342)](https://github.com/elastic/kibana/pull/182342)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Joe
McElroy","email":"joseph.mcelroy@elastic.co"},"sourceCommit":{"committedDate":"2024-05-02T14:45:07Z","message":"[Search]
[Playground] Improve UX + fix vector field issue (#182342)\n\nImproves
the experience for Edit context and view query.\r\n\r\nNow select boxes
for context
fields\r\n\r\n\r\n![image](6b9c9ef4-2cb1-4bec-9047-9736dbffc34d)\r\n\r\nand
now view query is checkboxes and a label for when fields are
hidden\r\n\r\n\r\n![image](6ae4a2a2-41c2-4cbf-8ff9-954910272c4e)\r\n\r\nAlso
fixes an issue where if an index has a dense vector field but\r\ndoesn't
have an inference processor setup. we now skip over these type\r\nof
fields.","sha":"8cc8be50d75ac64ebe05985c0916aa248a78b61d","branchLabelMapping":{"^v8.15.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:EnterpriseSearch","v8.14.0","v8.15.0"],"title":"[Search]
[Playground] Improve UX + fix vector field
issue","number":182342,"url":"https://github.com/elastic/kibana/pull/182342","mergeCommit":{"message":"[Search]
[Playground] Improve UX + fix vector field issue (#182342)\n\nImproves
the experience for Edit context and view query.\r\n\r\nNow select boxes
for context
fields\r\n\r\n\r\n![image](6b9c9ef4-2cb1-4bec-9047-9736dbffc34d)\r\n\r\nand
now view query is checkboxes and a label for when fields are
hidden\r\n\r\n\r\n![image](6ae4a2a2-41c2-4cbf-8ff9-954910272c4e)\r\n\r\nAlso
fixes an issue where if an index has a dense vector field but\r\ndoesn't
have an inference processor setup. we now skip over these type\r\nof
fields.","sha":"8cc8be50d75ac64ebe05985c0916aa248a78b61d"}},"sourceBranch":"main","suggestedTargetBranches":["8.14"],"targetPullRequestStates":[{"branch":"8.14","label":"v8.14.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.15.0","branchLabelMappingKey":"^v8.15.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/182342","number":182342,"mergeCommit":{"message":"[Search]
[Playground] Improve UX + fix vector field issue (#182342)\n\nImproves
the experience for Edit context and view query.\r\n\r\nNow select boxes
for context
fields\r\n\r\n\r\n![image](6b9c9ef4-2cb1-4bec-9047-9736dbffc34d)\r\n\r\nand
now view query is checkboxes and a label for when fields are
hidden\r\n\r\n\r\n![image](6ae4a2a2-41c2-4cbf-8ff9-954910272c4e)\r\n\r\nAlso
fixes an issue where if an index has a dense vector field but\r\ndoesn't
have an inference processor setup. we now skip over these type\r\nof
fields.","sha":"8cc8be50d75ac64ebe05985c0916aa248a78b61d"}}]}]
BACKPORT-->

Co-authored-by: Joe McElroy <joseph.mcelroy@elastic.co>
This commit is contained in:
Kibana Machine 2024-05-08 07:30:49 -04:00 committed by GitHub
parent 6785bb1f8d
commit 9e1d156c72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 244 additions and 107 deletions

View file

@ -19,6 +19,7 @@ export interface QuerySourceFields {
dense_vector_query_fields: ModelFields[];
bm25_query_fields: string[];
source_fields: string[];
skipped_fields: number;
}
export enum APIRoutes {

View file

@ -20,6 +20,12 @@ jest.mock('../../hooks/use_indices_fields', () => ({
bm25_query_fields: ['field1', 'field2'],
source_fields: ['context_field1', 'context_field2'],
},
index2: {
elser_query_fields: [],
dense_vector_query_fields: [],
bm25_query_fields: ['field1', 'field2'],
source_fields: ['context_field1', 'context_field2'],
},
},
}),
}));
@ -36,6 +42,11 @@ const MockFormProvider = ({ children }: { children: React.ReactElement }) => {
const methods = useForm({
values: {
indices: ['index1'],
docSize: 1,
sourceFields: {
index1: ['context_field1'],
index2: ['context_field2'],
},
},
});
return <FormProvider {...methods}>{children}</FormProvider>;
@ -55,13 +66,14 @@ describe('EditContextFlyout component tests', () => {
});
it('calls onClose when the close button is clicked', () => {
fireEvent.click(screen.getByTestId('euiFlyoutCloseButton'));
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
expect(onCloseMock).toHaveBeenCalledTimes(1);
});
it('should see the context fields', async () => {
expect(screen.getByTestId('contextFieldsSelectable')).toBeInTheDocument();
expect(screen.getByTestId('contextFieldsSelectable')).toHaveTextContent(`context_field2`);
expect(screen.getByTestId('contextFieldsSelectable')).toHaveTextContent(`context_field1`);
expect(screen.getByTestId('contextFieldsSelectable_index1')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('contextFieldsSelectable_index1'));
expect(screen.getByRole('option', { name: 'context_field1' })).toBeInTheDocument();
expect(screen.getByRole('option', { name: 'context_field2' })).toBeInTheDocument();
});
});

View file

@ -18,11 +18,9 @@ import {
EuiLink,
EuiSpacer,
EuiText,
EuiPanel,
EuiAccordion,
EuiSelectable,
EuiSelect,
EuiSelectableOption,
EuiSuperSelect,
EuiFormRow,
} from '@elastic/eui';
import { useController, useFormContext } from 'react-hook-form';
import { i18n } from '@kbn/i18n';
@ -62,10 +60,10 @@ export const EditContextFlyout: React.FC<EditContextFlyoutProps> = ({ onClose })
const [tempSourceFields, setTempSourceFields] = useState(sourceFields);
const toggleSourceField = (index: string, f: EuiSelectableOption[]) => {
const updateSourceField = (index: string, field: string) => {
setTempSourceFields({
...tempSourceFields,
[index]: f.filter(({ checked }) => checked === 'on').map(({ label }) => label),
[index]: [field],
});
usageTracker?.click(AnalyticsEvents.editContextFieldToggled);
};
@ -76,6 +74,7 @@ export const EditContextFlyout: React.FC<EditContextFlyoutProps> = ({ onClose })
onChangeSize(docSize);
onClose();
};
const handleDocSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
usageTracker?.click(AnalyticsEvents.editContextDocSizeChanged);
setDocSize(Number(e.target.value));
@ -119,71 +118,69 @@ export const EditContextFlyout: React.FC<EditContextFlyoutProps> = ({ onClose })
<EuiFlyoutBody>
<EuiFlexGroup>
<EuiFlexItem grow={3}>
<EuiFlexGroup direction="column">
<EuiFlexGroup direction="column" gutterSize="l">
<EuiFlexItem grow={false}>
<EuiSelect
prepend={i18n.translate(
<EuiFormRow
label={i18n.translate(
'xpack.searchPlayground.editContext.flyout.docsRetrievedCount',
{
defaultMessage: 'Retrieved documents',
defaultMessage: 'Number of documents to retrieve',
}
)}
options={[
{
value: 3,
text: '3',
},
{
value: 5,
text: '5',
},
{
value: 10,
text: '10',
},
]}
value={docSize}
onChange={handleDocSizeChange}
/>
</EuiFlexItem>
<EuiText>
<h5>
<FormattedMessage
id="xpack.searchPlayground.editContext.flyout.table.title"
defaultMessage="Selected fields"
>
<EuiSelect
options={[
{
value: 1,
text: '1',
},
{
value: 3,
text: '3',
},
{
value: 5,
text: '5',
},
{
value: 10,
text: '10',
},
]}
value={docSize}
onChange={handleDocSizeChange}
/>
</h5>
</EuiText>
{Object.entries(fields).map(([index, group], i) => (
<EuiFlexItem grow={false} key={index}>
<EuiPanel grow={false} hasShadow={false} hasBorder>
<EuiAccordion
initialIsOpen={i === 0}
id={index}
buttonContent={
<EuiText>
<h5>{index}</h5>
</EuiText>
}
>
<EuiSpacer size="s" />
<EuiSelectable
aria-label="Select context fields"
data-test-subj="contextFieldsSelectable"
options={group.source_fields.map((field) => ({
label: field,
checked: tempSourceFields[index]?.includes(field) ? 'on' : undefined,
}))}
onChange={(newOptions) => toggleSourceField(index, newOptions)}
listProps={{ bordered: false }}
singleSelection="always"
>
{(list) => list}
</EuiSelectable>
</EuiAccordion>
</EuiPanel>
</EuiFlexItem>
))}
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<EuiText>
<h5>
<FormattedMessage
id="xpack.searchPlayground.editContext.flyout.table.title"
defaultMessage="Select fields"
/>
</h5>
</EuiText>
</EuiFlexItem>
{Object.entries(fields).map(([index, group]) => (
<EuiFlexItem grow={false} key={index}>
<EuiFormRow label={index}>
<EuiSuperSelect
data-test-subj={`contextFieldsSelectable_${index}`}
options={group.source_fields.map((field) => ({
value: field,
inputDisplay: field,
}))}
valueOfSelected={tempSourceFields[index]?.[0]}
onChange={(value) => updateSourceField(index, value)}
/>
</EuiFormRow>
</EuiFlexItem>
))}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -18,6 +18,13 @@ jest.mock('../../hooks/use_indices_fields', () => ({
elser_query_fields: [],
dense_vector_query_fields: [],
bm25_query_fields: ['field1', 'field2'],
skipped_fields: 1,
},
index2: {
elser_query_fields: [],
dense_vector_query_fields: [],
bm25_query_fields: ['field1', 'field2'],
skipped_fields: 0,
},
},
}),
@ -34,7 +41,7 @@ jest.mock('../../hooks/use_usage_tracker', () => ({
const MockFormProvider = ({ children }: { children: React.ReactElement }) => {
const methods = useForm({
values: {
indices: ['index1'],
indices: ['index1', 'index2'],
},
});
return <FormProvider {...methods}>{children}</FormProvider>;
@ -61,7 +68,19 @@ describe('ViewQueryFlyout component tests', () => {
it('should see the view elasticsearch query', async () => {
expect(screen.getByTestId('ViewElasticsearchQueryResult')).toBeInTheDocument();
expect(screen.getByTestId('ViewElasticsearchQueryResult')).toHaveTextContent(
`{ "retriever": { "standard": { "query": { "multi_match": { "query": "{query}", "fields": [ "field1" ] } } } } }`
`{ "retriever": { "rrf": { "retrievers": [ { "standard": { "query": { "multi_match": { "query": "{query}", "fields": [ "field1" ] } } } }, { "standard": { "query": { "multi_match": { "query": "{query}", "fields": [ "field1" ] } } } } ] } } }`
);
});
it('displays query fields and indicates hidden fields', () => {
expect(screen.getByTestId('queryFieldsSelectable_index1')).toBeInTheDocument();
expect(screen.getByTestId('queryFieldsSelectable_index2')).toBeInTheDocument();
// Check if hidden fields indicator is shown
expect(screen.getByTestId('skipped_fields_index1')).toBeInTheDocument();
expect(screen.getByTestId('skipped_fields_index1')).toHaveTextContent('1 fields are hidden.');
// Check if hidden fields indicator is shown
expect(screen.queryByTestId('skipped_fields_index2')).not.toBeInTheDocument();
});
});

View file

@ -22,12 +22,16 @@ import {
EuiSelectableOption,
EuiText,
EuiTitle,
EuiCheckbox,
EuiLink,
EuiIcon,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { useEffect, useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { useController } from 'react-hook-form';
import { AnalyticsEvents } from '../../analytics/constants';
import { docLinks } from '../../../common/doc_links';
import { useIndicesFields } from '../../hooks/use_indices_fields';
import { useUsageTracker } from '../../hooks/use_usage_tracker';
import { ChatForm, ChatFormFields, IndicesQuerySourceFields } from '../../types';
@ -157,7 +161,7 @@ export const ViewQueryFlyout: React.FC<ViewQueryFlyoutProps> = ({ onClose }) =>
</EuiCodeBlock>
</EuiFlexItem>
<EuiFlexItem grow={3}>
<EuiFlexGroup direction="column">
<EuiFlexGroup direction="column" gutterSize="s">
<EuiText>
<h5>
<FormattedMessage
@ -166,12 +170,12 @@ export const ViewQueryFlyout: React.FC<ViewQueryFlyoutProps> = ({ onClose }) =>
/>
</h5>
</EuiText>
{Object.entries(fields).map(([index, group], i) => (
{Object.entries(fields).map(([index, group]) => (
<EuiFlexItem grow={false} key={index}>
<EuiPanel grow={false} hasShadow={false} hasBorder>
<EuiAccordion
id={index}
initialIsOpen={i === 0}
initialIsOpen
buttonContent={
<EuiText>
<h5>{index}</h5>
@ -181,24 +185,70 @@ export const ViewQueryFlyout: React.FC<ViewQueryFlyoutProps> = ({ onClose }) =>
<EuiSpacer size="s" />
<EuiSelectable
aria-label="Select query fields"
data-test-subj={`queryFieldsSelectable_${index}`}
options={[
...group.elser_query_fields,
...group.dense_vector_query_fields,
...group.bm25_query_fields,
].map((field) => ({
label: typeof field === 'string' ? field : field.field,
checked: isQueryFieldSelected(
].map((field, idx) => {
const checked = isQueryFieldSelected(
index,
typeof field === 'string' ? field : field.field
)
? 'on'
: undefined,
}))}
);
return {
label: typeof field === 'string' ? field : field.field,
prepend: (
<EuiCheckbox
id={`checkbox_${idx}`}
checked={checked}
onChange={() => {}}
/>
),
checked: checked ? 'on' : undefined,
};
})}
listProps={{
bordered: false,
showIcons: false,
}}
onChange={(newOptions) => updateFields(index, newOptions)}
listProps={{ bordered: false }}
>
{(list) => list}
</EuiSelectable>
{group.skipped_fields > 0 && (
<>
<EuiSpacer size="m" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiText
size="s"
color="subdued"
data-test-subj={`skipped_fields_${index}`}
>
<EuiIcon type="eyeClosed" />
{` `}
<FormattedMessage
id="xpack.searchPlayground.viewQuery.flyout.hiddenFields"
defaultMessage="{skippedFields} fields are hidden."
values={{ skippedFields: group.skipped_fields }}
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink
href={docLinks.chatPlayground}
target="_blank"
data-test-subj="context-optimization-documentation-link"
>
<FormattedMessage
id="xpack.searchPlayground.viewQuery.flyout.learnMoreLink"
defaultMessage="Learn more."
/>
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
</EuiAccordion>
</EuiPanel>
</EuiFlexItem>

View file

@ -43,6 +43,7 @@ describe.skip('useSourceIndicesFields Hook', () => {
dense_vector_query_fields: [],
bm25_query_fields: [],
source_fields: ['field1'],
skipped_fields: 0,
},
};
@ -144,6 +145,7 @@ describe.skip('useSourceIndicesFields Hook', () => {
dense_vector_query_fields: [],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
},
};
@ -196,6 +198,7 @@ describe.skip('useSourceIndicesFields Hook', () => {
dense_vector_query_fields: [],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
},
};

View file

@ -23,7 +23,7 @@ export const PlaygroundProvider: React.FC<PlaygroundProviderProps> = ({
const form = useForm<ChatForm>({
defaultValues: {
prompt: 'You are an assistant for question-answering tasks.',
doc_size: 5,
doc_size: 3,
source_fields: [],
indices: defaultValues?.indices || [],
},

View file

@ -23,6 +23,7 @@ describe('create_query', () => {
dense_vector_query_fields: [],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
},
};
@ -55,6 +56,7 @@ describe('create_query', () => {
],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
},
};
@ -92,6 +94,7 @@ describe('create_query', () => {
dense_vector_query_fields: [],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
},
index2: {
elser_query_fields: [
@ -100,6 +103,7 @@ describe('create_query', () => {
dense_vector_query_fields: [],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
},
};
@ -133,6 +137,7 @@ describe('create_query', () => {
dense_vector_query_fields: [],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
},
index2: {
elser_query_fields: [
@ -141,6 +146,7 @@ describe('create_query', () => {
dense_vector_query_fields: [],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
},
};
@ -196,6 +202,7 @@ describe('create_query', () => {
],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
},
};
@ -228,6 +235,7 @@ describe('create_query', () => {
dense_vector_query_fields: [],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
},
};
@ -257,6 +265,7 @@ describe('create_query', () => {
dense_vector_query_fields: [],
bm25_query_fields: ['content', 'title'],
source_fields: [],
skipped_fields: 0,
},
index2: {
elser_query_fields: [
@ -265,6 +274,7 @@ describe('create_query', () => {
dense_vector_query_fields: [],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
},
};
@ -321,6 +331,7 @@ describe('create_query', () => {
dense_vector_query_fields: [],
bm25_query_fields: ['content', 'title'],
source_fields: [],
skipped_fields: 0,
},
index2: {
elser_query_fields: [
@ -329,6 +340,7 @@ describe('create_query', () => {
dense_vector_query_fields: [],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
},
};
@ -390,6 +402,7 @@ describe('create_query', () => {
],
bm25_query_fields: ['content', 'title'],
source_fields: [],
skipped_fields: 0,
},
index2: {
elser_query_fields: [
@ -398,6 +411,7 @@ describe('create_query', () => {
dense_vector_query_fields: [],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
},
};
@ -435,6 +449,7 @@ describe('create_query', () => {
],
bm25_query_fields: ['content', 'title'],
source_fields: [],
skipped_fields: 0,
},
};
@ -487,6 +502,7 @@ describe('create_query', () => {
],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
},
};
@ -509,6 +525,7 @@ describe('create_query', () => {
],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
},
index2: {
elser_query_fields: [
@ -524,6 +541,7 @@ describe('create_query', () => {
],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
},
};
@ -549,6 +567,7 @@ describe('create_query', () => {
],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
},
index2: {
elser_query_fields: [
@ -564,6 +583,7 @@ describe('create_query', () => {
],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
},
};
@ -582,6 +602,7 @@ describe('create_query', () => {
],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
},
};
@ -595,6 +616,7 @@ describe('create_query', () => {
dense_vector_query_fields: [],
bm25_query_fields: ['title', 'text', 'content'],
source_fields: [],
skipped_fields: 0,
},
};
@ -610,6 +632,7 @@ describe('create_query', () => {
dense_vector_query_fields: [],
bm25_query_fields: ['unknown1', 'unknown2'],
source_fields: [],
skipped_fields: 0,
},
};
@ -659,6 +682,7 @@ describe('create_query', () => {
'url_path_dir2',
'url_path_dir1',
],
skipped_fields: 0,
},
};
@ -674,6 +698,7 @@ describe('create_query', () => {
dense_vector_query_fields: [],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
},
};
@ -691,6 +716,7 @@ describe('create_query', () => {
dense_vector_query_fields: [],
bm25_query_fields: [],
source_fields: ['non_suggested_field'],
skipped_fields: 0,
},
};

View file

@ -48,6 +48,7 @@ describe('fetch_query_source_fields', () => {
indices: ['workplace_index'],
},
],
skipped_fields: 8,
source_fields: ['metadata.summary', 'metadata.rolePermissions', 'text', 'metadata.name'],
},
workplace_index2: {
@ -58,6 +59,7 @@ describe('fetch_query_source_fields', () => {
'metadata.name',
],
dense_vector_query_fields: [],
skipped_fields: 8,
elser_query_fields: [
{
field: 'content_vector.tokens',
@ -110,6 +112,7 @@ describe('fetch_query_source_fields', () => {
},
],
elser_query_fields: [],
skipped_fields: 30,
source_fields: [
'page_content_key',
'title',
@ -150,11 +153,13 @@ describe('fetch_query_source_fields', () => {
},
],
source_fields: ['body_content', 'headings', 'title'],
skipped_fields: 4,
},
});
});
it('should return the correct fields for a document first index', () => {
// Skips the nested dense vector field.
expect(
parseFieldsCapabilities(DENSE_VECTOR_DOCUMENT_FIRST_FIELD_CAPS, [
{
@ -174,14 +179,7 @@ describe('fetch_query_source_fields', () => {
'metadata.summary',
'metadata.content',
],
dense_vector_query_fields: [
{
field: 'passages.vector.predicted_value',
model_id: '.multilingual-e5-small',
nested: true,
indices: ['workplace_index_nested'],
},
],
dense_vector_query_fields: [],
elser_query_fields: [],
source_fields: [
'metadata.category',
@ -193,6 +191,7 @@ describe('fetch_query_source_fields', () => {
'metadata.summary',
'metadata.content',
],
skipped_fields: 18,
},
});
});

View file

@ -7,7 +7,7 @@
import { FieldCapsResponse, SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
import { get } from 'lodash';
import { get, has } from 'lodash';
import { IndicesQuerySourceFields } from '../types';
export const fetchFields = async (
@ -45,10 +45,23 @@ export const fetchFields = async (
return parseFieldsCapabilities(fieldCapabilities, indexDocs);
};
const INFERENCE_MODEL_FIELD_REGEXP = /\.predicted_value|\.tokens/;
const hasModelField = (field: string, indexDoc: any, nestedField: string | false) => {
if (field.match(INFERENCE_MODEL_FIELD_REGEXP)) {
const path = nestedField ? field.replace(`${nestedField}.`, `${nestedField}[0].`) : field;
return has(
indexDoc.doc,
`_source.${[path.replace(INFERENCE_MODEL_FIELD_REGEXP, '.model_id')]}`
);
}
return false;
};
const getModelField = (field: string, indexDoc: any, nestedField: string | false) => {
// If the field is nested, we need to get the first occurrence as its an array
const path = nestedField ? field.replace(`${nestedField}.`, `${nestedField}[0].`) : field;
return get(indexDoc.doc, `_source.${[path.replace(/\.predicted_value|\.tokens/, '.model_id')]}`);
return get(indexDoc.doc, `_source.${[path.replace(INFERENCE_MODEL_FIELD_REGEXP, '.model_id')]}`);
};
const isFieldNested = (field: string, fieldCapsResponse: FieldCapsResponse) => {
@ -83,6 +96,7 @@ export const parseFieldsCapabilities = (
dense_vector_query_fields: [],
bm25_query_fields: [],
source_fields: [],
skipped_fields: 0,
};
return acc;
}, {});
@ -106,25 +120,41 @@ export const parseFieldsCapabilities = (
if ('rank_features' in field || 'sparse_vector' in field) {
const nestedField = isFieldNested(fieldKey, fieldCapsResponse);
const elserModelField = {
field: fieldKey,
model_id: getModelField(fieldKey, indexDoc, nestedField),
nested: !!isFieldNested(fieldKey, fieldCapsResponse),
indices: indicesPresentIn,
};
acc[index].elser_query_fields.push(elserModelField);
if (!nestedField) {
const elserModelField = {
field: fieldKey,
model_id: getModelField(fieldKey, indexDoc, nestedField),
nested: !!isFieldNested(fieldKey, fieldCapsResponse),
indices: indicesPresentIn,
};
acc[index].elser_query_fields.push(elserModelField);
} else {
acc[index].skipped_fields++;
}
} else if ('dense_vector' in field) {
const nestedField = isFieldNested(fieldKey, fieldCapsResponse);
const denseVectorField = {
field: fieldKey,
model_id: getModelField(fieldKey, indexDoc, nestedField),
nested: !!nestedField,
indices: indicesPresentIn,
};
acc[index].dense_vector_query_fields.push(denseVectorField);
// Check if the dense vector field has a model_id associated with it
// skip this field if has no model associated with it
// and the vectors were embedded outside of stack
if (hasModelField(fieldKey, indexDoc, nestedField) && !nestedField) {
const denseVectorField = {
field: fieldKey,
model_id: getModelField(fieldKey, indexDoc, nestedField),
nested: !!nestedField,
indices: indicesPresentIn,
};
acc[index].dense_vector_query_fields.push(denseVectorField);
} else {
acc[index].skipped_fields++;
}
} else if ('text' in field && field.text.searchable && shouldIgnoreField(fieldKey)) {
acc[index].bm25_query_fields.push(fieldKey);
acc[index].source_fields.push(fieldKey);
} else {
if (fieldKey !== '_id' && fieldKey !== '_index' && fieldKey !== '_type') {
acc[index].skipped_fields++;
}
}
}