Skip to content

fix: autocomplete for lucene column values #720

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fifty-pugs-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@hyperdx/common-utils": patch
"@hyperdx/app": patch
---

fix: autocomplete for key-values complete for v2 lucene
2 changes: 1 addition & 1 deletion packages/app/src/NamespaceDetailsSidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ export default function NamespaceDetailsSidePanel({

const { data: logServiceNames } = useGetKeyValues(
{
chartConfig: {
chartConfigs: {
from: logSource.from,
where: `${logSource?.resourceAttributesExpression}.k8s.namespace.name:"${namespaceName}"`,
whereLanguage: 'lucene',
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/NodeDetailsSidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ export default function NodeDetailsSidePanel({

const { data: logServiceNames } = useGetKeyValues(
{
chartConfig: {
chartConfigs: {
from: logSource.from,
where: `${logSource?.resourceAttributesExpression}.k8s.node.name:"${nodeName}"`,
whereLanguage: 'lucene',
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/PodDetailsSidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ export default function PodDetailsSidePanel({

const { data: logServiceNames } = useGetKeyValues(
{
chartConfig: {
chartConfigs: {
from: logSource.from,
where: `${logSource?.resourceAttributesExpression}.k8s.pod.name:"${podName}"`,
whereLanguage: 'lucene',
Expand Down
54 changes: 28 additions & 26 deletions packages/app/src/SearchInputV2.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useController, UseControllerProps } from 'react-hook-form';
import { useHotkeys } from 'react-hotkeys-hook';
import { TableConnection } from '@hyperdx/common-utils/dist/metadata';
import { Field, TableConnection } from '@hyperdx/common-utils/dist/metadata';
import { genEnglishExplanation } from '@hyperdx/common-utils/dist/queryParser';

import AutocompleteInput from '@/AutocompleteInput';
import { useAllFields } from '@/hooks/useMetadata';

import {
ILanguageFormatter,
useAutoCompleteOptions,
} from './hooks/useAutoCompleteOptions';

export class LuceneLanguageFormatter implements ILanguageFormatter {
formatFieldValue(f: Field): string {
return f.path.join('.');
Copy link
Member

Choose a reason for hiding this comment

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

no need to fix it for now but I'm curious what happened if the key has '.' in it

}
formatFieldLabel(f: Field): string {
return `${f.path.join('.')} (${f.jsType})`;
}
formatKeyValPair(key: string, value: string): string {
return `${key}:"${value}"`;
}
}

export default function SearchInputV2({
tableConnections,
Expand Down Expand Up @@ -34,31 +50,17 @@ export default function SearchInputV2({
} = useController(props);

const ref = useRef<HTMLInputElement>(null);

const { data: fields } = useAllFields(tableConnections ?? [], {
enabled:
!!tableConnections &&
(Array.isArray(tableConnections) ? tableConnections.length > 0 : true),
});

const autoCompleteOptions = useMemo(() => {
const _columns = (fields ?? []).filter(c => c.jsType !== null);
const baseOptions = _columns.map(c => ({
value: c.path.join('.'),
label: `${c.path.join('.')} (${c.jsType})`,
}));

const suggestionOptions =
additionalSuggestions?.map(column => ({
value: column,
label: column,
})) ?? [];

return [...baseOptions, ...suggestionOptions];
}, [fields, additionalSuggestions]);

const [parsedEnglishQuery, setParsedEnglishQuery] = useState<string>('');

const autoCompleteOptions = useAutoCompleteOptions(
new LuceneLanguageFormatter(),
value,
{
tableConnections,
additionalSuggestions,
},
);

useEffect(() => {
genEnglishExplanation(value).then(q => {
setParsedEnglishQuery(q);
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/DBSearchPageFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ export const DBSearchPageFilters = ({
isLoading: isFacetsLoading,
isFetching: isFacetsFetching,
} = useGetKeyValues({
chartConfig: { ...chartConfig, dateRange },
chartConfigs: { ...chartConfig, dateRange },
keys: datum,
});

Expand Down
6 changes: 3 additions & 3 deletions packages/app/src/components/MetricNameSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,19 @@ function useMetricNames(metricSource: TSource) {
}, [metricSource]);

const { data: gaugeMetrics } = useGetKeyValues({
chartConfig: gaugeConfig,
chartConfigs: gaugeConfig,
keys: ['MetricName'],
limit: MAX_METRIC_NAME_OPTIONS,
disableRowLimit: true,
});
// const { data: histogramMetrics } = useGetKeyValues({
// chartConfig: histogramConfig,
// chartConfigs: histogramConfig,
// keys: ['MetricName'],
// limit: MAX_METRIC_NAME_OPTIONS,
// disableRowLimit: true,
// });
const { data: sumMetrics } = useGetKeyValues({
chartConfig: sumConfig,
chartConfigs: sumConfig,
keys: ['MetricName'],
limit: MAX_METRIC_NAME_OPTIONS,
disableRowLimit: true,
Expand Down
210 changes: 210 additions & 0 deletions packages/app/src/hooks/__tests__/useAutoCompleteOptions.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { JSDataType } from '@hyperdx/common-utils/dist/clickhouse';
import { Field } from '@hyperdx/common-utils/dist/metadata';
import { renderHook } from '@testing-library/react';

import { LuceneLanguageFormatter } from '../../SearchInputV2';
import { useAutoCompleteOptions } from '../useAutoCompleteOptions';
import { useAllFields, useGetKeyValues } from '../useMetadata';

if (!globalThis.structuredClone) {
globalThis.structuredClone = (obj: any) => {
return JSON.parse(JSON.stringify(obj));
};
}

// Mock dependencies
jest.mock('../useMetadata', () => ({
...jest.requireActual('../useMetadata.tsx'),
useAllFields: jest.fn(),
useGetKeyValues: jest.fn(),
}));

const luceneFormatter = new LuceneLanguageFormatter();

const mockFields: Field[] = [
{
path: ['ResourceAttributes'],
jsType: JSDataType.Map,
type: 'map',
},
{
path: ['ResourceAttributes', 'service.name'],
jsType: JSDataType.String,
type: 'string',
},
{
path: ['TraceAttributes', 'trace.id'],
jsType: JSDataType.String,
type: 'string',
},
];

const mockTableConnections = [
{
databaseName: 'test_db',
tableName: 'traces',
connectionId: 'conn1',
},
];

describe('useAutoCompleteOptions', () => {
beforeEach(() => {
// Reset mocks before each test
jest.clearAllMocks();

// Setup default mock implementations
(useAllFields as jest.Mock).mockReturnValue({
data: mockFields,
});

(useGetKeyValues as jest.Mock).mockReturnValue({
data: null,
});
});

it('should return field options with correct lucene formatting', () => {
const { result } = renderHook(() =>
useAutoCompleteOptions(luceneFormatter, 'ResourceAttributes', {
tableConnections: mockTableConnections,
}),
);

expect(result.current).toEqual([
{
value: 'ResourceAttributes',
label: 'ResourceAttributes (map)',
},
{
value: 'ResourceAttributes.service.name',
label: 'ResourceAttributes.service.name (string)',
},
{
value: 'TraceAttributes.trace.id',
label: 'TraceAttributes.trace.id (string)',
},
]);
});

it('should return key value options with correct lucene formatting', () => {
const mockKeyValues = [
{
key: 'ResourceAttributes.service.name',
value: ['frontend', 'backend'],
},
];

(useGetKeyValues as jest.Mock).mockReturnValue({
data: mockKeyValues,
});

const { result } = renderHook(() =>
useAutoCompleteOptions(
luceneFormatter,
'ResourceAttributes.service.name',
{
tableConnections: mockTableConnections,
},
),
);

expect(result.current).toEqual([
{
value: 'ResourceAttributes',
label: 'ResourceAttributes (map)',
},
{
value: 'ResourceAttributes.service.name',
label: 'ResourceAttributes.service.name (string)',
},
{
value: 'TraceAttributes.trace.id',
label: 'TraceAttributes.trace.id (string)',
},
{
value: 'ResourceAttributes.service.name:"frontend"',
label: 'ResourceAttributes.service.name:"frontend"',
},
{
value: 'ResourceAttributes.service.name:"backend"',
label: 'ResourceAttributes.service.name:"backend"',
},
]);
});

// TODO: Does this test case need to be removed after HDX-1548?
it('should handle nested key value options', () => {
const mockKeyValues = [
{
key: 'ResourceAttributes',
value: [
{
'service.name': 'frontend',
'deployment.environment': 'production',
},
],
},
];

(useGetKeyValues as jest.Mock).mockReturnValue({
data: mockKeyValues,
});

const { result } = renderHook(() =>
useAutoCompleteOptions(luceneFormatter, 'ResourceAttributes', {
tableConnections: mockTableConnections,
}),
);

//console.log(result.current);
expect(result.current).toEqual([
{
value: 'ResourceAttributes',
label: 'ResourceAttributes (map)',
},
{
value: 'ResourceAttributes.service.name',
label: 'ResourceAttributes.service.name (string)',
},
{
value: 'TraceAttributes.trace.id',
label: 'TraceAttributes.trace.id (string)',
},
{
value: 'ResourceAttributes.service.name:"frontend"',
label: 'ResourceAttributes.service.name:"frontend"',
},
{
value: 'ResourceAttributes.deployment.environment:"production"',
label: 'ResourceAttributes.deployment.environment:"production"',
},
]);
});

it('should handle additional suggestions', () => {
const { result } = renderHook(() =>
useAutoCompleteOptions(luceneFormatter, 'ResourceAttributes', {
tableConnections: mockTableConnections,
additionalSuggestions: ['custom.field'],
}),
);

expect(result.current).toEqual([
{
value: 'ResourceAttributes',
label: 'ResourceAttributes (map)',
},
{
value: 'ResourceAttributes.service.name',
label: 'ResourceAttributes.service.name (string)',
},
{
value: 'TraceAttributes.trace.id',
label: 'TraceAttributes.trace.id (string)',
},
{
value: 'custom.field',
label: 'custom.field',
},
]);
});
});
Loading