diff --git a/pkg/services/searchV2/bluge.go b/pkg/services/searchV2/bluge.go index c65624c7f16..93658636fb0 100644 --- a/pkg/services/searchV2/bluge.go +++ b/pkg/services/searchV2/bluge.go @@ -429,7 +429,8 @@ func doSearchQuery( hasConstraints = true } - if q.Query == "*" || q.Query == "" { + isMatchAllQuery := q.Query == "*" || q.Query == "" + if isMatchAllQuery { if !hasConstraints { fullQuery.AddShould(bluge.NewMatchAllQuery()) } @@ -600,7 +601,11 @@ func doSearchQuery( } if q.Explain { - fScore.Append(match.Score) + if isMatchAllQuery { + fScore.Append(float64(fieldLen + q.From)) + } else { + fScore.Append(match.Score) + } if match.Explanation != nil { js, _ := json.Marshal(&match.Explanation) jsb := json.RawMessage(js) diff --git a/public/app/features/search/page/components/ExplainScorePopup.tsx b/public/app/features/search/page/components/ExplainScorePopup.tsx new file mode 100644 index 00000000000..5f14e7f7947 --- /dev/null +++ b/public/app/features/search/page/components/ExplainScorePopup.tsx @@ -0,0 +1,57 @@ +import React, { useState } from 'react'; + +import { DataFrame } from '@grafana/data'; +import { CodeEditor, Modal, ModalTabsHeader, TabContent } from '@grafana/ui'; +import { DataHoverView } from 'app/plugins/panel/geomap/components/DataHoverView'; + +export interface Props { + name: string; + explain: {}; + frame: DataFrame; + row: number; +} + +const tabs = [ + { label: 'Score', value: 'score' }, + { label: 'Fields', value: 'fields' }, +]; + +export function ExplainScorePopup({ name, explain, frame, row }: Props) { + const [isOpen, setOpen] = useState<boolean>(true); + const [activeTab, setActiveTab] = useState('score'); + + const modalHeader = ( + <ModalTabsHeader + title={name} + icon={'info'} + tabs={tabs} + activeTab={activeTab} + onChangeTab={(t) => { + setActiveTab(t.value); + }} + /> + ); + + return ( + <Modal title={modalHeader} isOpen={isOpen} onDismiss={() => setOpen(false)} closeOnBackdropClick closeOnEscape> + <TabContent> + {activeTab === tabs[0].value && ( + <CodeEditor + width="100%" + height="70vh" + language="json" + showLineNumbers={false} + showMiniMap={true} + value={JSON.stringify(explain, null, 2)} + readOnly={false} + /> + )} + {activeTab === tabs[1].value && ( + <div> + <DataHoverView data={frame} rowIndex={row} /> + </div> + )} + </TabContent> + </Modal> + ); +} diff --git a/public/app/features/search/page/components/SearchResultsTable.tsx b/public/app/features/search/page/components/SearchResultsTable.tsx index eb88361da35..8f30e9c1671 100644 --- a/public/app/features/search/page/components/SearchResultsTable.tsx +++ b/public/app/features/search/page/components/SearchResultsTable.tsx @@ -282,6 +282,11 @@ const getColumnStyles = (theme: GrafanaTheme2) => { text-align: right; padding: ${theme.spacing(1)} ${theme.spacing(3)} ${theme.spacing(1)} ${theme.spacing(1)}; `, + explainItem: css` + text-align: right; + padding: ${theme.spacing(1)} ${theme.spacing(3)} ${theme.spacing(1)} ${theme.spacing(1)}; + cursor: pointer; + `, locationCellStyle: css` padding-top: ${theme.spacing(1)}; padding-right: ${theme.spacing(1)}; diff --git a/public/app/features/search/page/components/SearchView.tsx b/public/app/features/search/page/components/SearchView.tsx index ce7377a15b2..adddc05973b 100644 --- a/public/app/features/search/page/components/SearchView.tsx +++ b/public/app/features/search/page/components/SearchView.tsx @@ -64,6 +64,7 @@ export const SearchView = ({ ds_uid: query.datasource as string, location: folderDTO?.uid, // This will scope all results to the prefix sort: query.sort?.value, + explain: query.explain, }; // Only dashboards have additional properties diff --git a/public/app/features/search/page/components/columns.tsx b/public/app/features/search/page/components/columns.tsx index 12219de34ab..9381df6b15b 100644 --- a/public/app/features/search/page/components/columns.tsx +++ b/public/app/features/search/page/components/columns.tsx @@ -5,11 +5,14 @@ import SVG from 'react-inlinesvg'; import { Field, FieldType, formattedValueToString, getDisplayProcessor, getFieldDisplayName } from '@grafana/data'; import { config, getDataSourceSrv } from '@grafana/runtime'; import { Checkbox, Icon, IconButton, IconName, TagList } from '@grafana/ui'; +import appEvents from 'app/core/app_events'; import { PluginIconName } from 'app/features/plugins/admin/types'; +import { ShowModalReactEvent } from 'app/types/events'; import { QueryResponse, SearchResultMeta } from '../../service'; import { SelectionChecker, SelectionToggle } from '../selection'; +import { ExplainScorePopup } from './ExplainScorePopup'; import { TableColumn } from './SearchResultsTable'; const TYPE_COLUMN_WIDTH = 175; @@ -40,6 +43,10 @@ export const generateColumns = ( availableWidth -= sortFieldWith; // pre-allocate the space for the last column } + if (access.explain && access.score) { + availableWidth -= 100; // pre-allocate the space for the last column + } + let width = 50; if (selection && selectionToggle) { width = 30; @@ -107,7 +114,8 @@ export const generateColumns = ( let classNames = cx(styles.nameCellStyle); let name = access.name.values.get(p.row.index); if (!name?.length) { - name = 'Missing title'; // normal for panels + const loading = p.row.index >= response.view.dataFrame.length; + name = loading ? 'Loading...' : 'Missing title'; // normal for panels classNames += ' ' + styles.missingTitleText; } return ( @@ -196,6 +204,37 @@ export const generateColumns = ( }); } + if (access.explain && access.score) { + const vals = access.score.values; + const showExplainPopup = (row: number) => { + appEvents.publish( + new ShowModalReactEvent({ + component: ExplainScorePopup, + props: { + name: access.name.values.get(row), + explain: access.explain.values.get(row), + frame: response.view.dataFrame, + row: row, + }, + }) + ); + }; + + columns.push({ + Header: () => <div className={styles.sortedHeader}>Score</div>, + Cell: (p) => { + return ( + <div {...p.cellProps} className={styles.explainItem} onClick={() => showExplainPopup(p.row.index)}> + {vals.get(p.row.index)} + </div> + ); + }, + id: `column-score-field`, + field: access.score, + width: 100, + }); + } + return columns; }; diff --git a/public/app/features/search/reducers/searchQueryReducer.ts b/public/app/features/search/reducers/searchQueryReducer.ts index 9def0ce0de1..84b42beed07 100644 --- a/public/app/features/search/reducers/searchQueryReducer.ts +++ b/public/app/features/search/reducers/searchQueryReducer.ts @@ -17,9 +17,6 @@ export const defaultQuery: DashboardQuery = { query: '', tag: [], starred: false, - skipRecent: false, - skipStarred: false, - folderIds: [], sort: null, layout: SearchLayout.Folders, prevSort: null, diff --git a/public/app/features/search/service/bluge.ts b/public/app/features/search/service/bluge.ts index 6d7e252fec7..e66f73d45a7 100644 --- a/public/app/features/search/service/bluge.ts +++ b/public/app/features/search/service/bluge.ts @@ -121,15 +121,12 @@ async function doSearchQuery(query: SearchQuery): Promise<QueryResponse> { } } - const view = new DataFrameView<DashboardQueryResult>(first); - return { - totalRows: meta.count ?? first.length, - view, - loadMoreItems: async (startIndex: number, stopIndex: number): Promise<void> => { - console.log('LOAD NEXT PAGE', { startIndex, stopIndex, length: view.dataFrame.length }); + let loadMax = 0; + let pending: Promise<void> | undefined = undefined; + const getNextPage = async () => { + while (loadMax > view.dataFrame.length) { const from = view.dataFrame.length; - const limit = stopIndex - from; - if (limit < 0) { + if (from >= meta.count) { return; } const frame = ( @@ -141,7 +138,7 @@ async function doSearchQuery(query: SearchQuery): Promise<QueryResponse> { search: { ...(target?.search ?? {}), from, - limit: Math.max(limit, nextPageSizes), + limit: nextPageSizes, }, refId: 'Page', facet: undefined, @@ -175,7 +172,20 @@ async function doSearchQuery(query: SearchQuery): Promise<QueryResponse> { meta.locationInfo[key] = value; } } - return; + } + pending = undefined; + }; + + const view = new DataFrameView<DashboardQueryResult>(first); + return { + totalRows: meta.count ?? first.length, + view, + loadMoreItems: async (startIndex: number, stopIndex: number): Promise<void> => { + loadMax = Math.max(loadMax, stopIndex); + if (!pending) { + pending = getNextPage(); + } + return pending; }, isItemLoaded: (index: number): boolean => { return index < view.dataFrame.length; diff --git a/public/app/features/search/service/types.ts b/public/app/features/search/service/types.ts index e0a9eaf6aa9..adea9ba357d 100644 --- a/public/app/features/search/service/types.ts +++ b/public/app/features/search/service/types.ts @@ -33,6 +33,10 @@ export interface DashboardQueryResult { tags: string[]; location: string; // url that can be split ds_uid: string[]; + + // debugging fields + score: number; + explain: {}; } export interface LocationInfo { diff --git a/public/app/features/search/types.ts b/public/app/features/search/types.ts index 5d16446bb9d..47068c37f34 100644 --- a/public/app/features/search/types.ts +++ b/public/app/features/search/types.ts @@ -61,9 +61,7 @@ export interface DashboardQuery { query: string; tag: string[]; starred: boolean; - skipRecent: boolean; - skipStarred: boolean; - folderIds: number[]; + explain?: boolean; // adds debug info datasource?: string; sort: SelectableValue | null; // Save sorting data between layouts