Skip to content

Commit

Permalink
Merge pull request #2841 from Hyperkid123/test-full-text-search
Browse files Browse the repository at this point in the history
Swap async search for local search
  • Loading branch information
Hyperkid123 authored May 28, 2024
2 parents e7a8e07 + 660f391 commit 5b5ce55
Show file tree
Hide file tree
Showing 15 changed files with 584 additions and 150 deletions.
4 changes: 1 addition & 3 deletions cypress/component/AllServicesPage/AllServicesPage.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ describe('<AllServices />', () => {
status: 200,
fixture: 'settings-navigation.json',
});
// cy.intercept('http://localhost:8080/api/chrome-service/v1/static/stable/stage/navigation/*-navigation.json?ts=*', {
// data: [],
// });
cy.intercept('http://localhost:8080/api/chrome-service/v1/static/stable/stage/search/search-index.json', []);
});

it('should filter by service category title', () => {
Expand Down
1 change: 1 addition & 0 deletions cypress/component/DefaultLayout.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ describe('<Default layout />', () => {
navItems: [],
});
cy.intercept('GET', '/api/chrome-service/v1/static/stable/stage/services/services-generated.json', []);
cy.intercept('GET', '/api/chrome-service/v1/static/stable/stage/search/search-index.json', []);
});

it('render correctly with few nav items', () => {
Expand Down
1 change: 1 addition & 0 deletions cypress/component/helptopics/HelpTopicManager.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ describe('HelpTopicManager', () => {
navItems: [],
});
cy.intercept('GET', '/api/chrome-service/v1/static/stable/stage/services/services-generated.json', []);
cy.intercept('http://localhost:8080/api/chrome-service/v1/static/stable/stage/search/search-index.json', []);
});

it.only('should switch help topics drawer content', () => {
Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
"@patternfly/react-core": "^5.3.0",
"@patternfly/react-icons": "^5.3.0",
"@patternfly/react-tokens": "^5.3.0",
"@orama/orama": "^2.0.3",
"@redhat-cloud-services/frontend-components": "^4.2.2",
"@redhat-cloud-services/chrome": "^1.0.9",
"@redhat-cloud-services/entitlements-client": "1.2.0",
Expand Down
6 changes: 4 additions & 2 deletions src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Fragment, useContext, useState } from 'react';
import React, { Fragment, Suspense, useContext, useState } from 'react';
import ReactDOM from 'react-dom';
import Tools from './Tools';
import UnAuthtedHeader from './UnAuthtedHeader';
Expand Down Expand Up @@ -82,7 +82,9 @@ export const Header = ({ breadcrumbsProps }: { breadcrumbsProps?: Breadcrumbspro
)}
</ToolbarGroup>
<ToolbarGroup className="pf-v5-u-flex-grow-1 pf-v5-u-mr-0 pf-v5-u-mr-md-on-2xl" variant="filter-group">
<SearchInput onStateChange={hideAllServices} />
<Suspense fallback={null}>
<SearchInput onStateChange={hideAllServices} />
</Suspense>
</ToolbarGroup>
<ToolbarGroup
className="pf-v5-m-icon-button-group pf-v5-u-ml-auto"
Expand Down
4 changes: 4 additions & 0 deletions src/components/RootApp/ScalprumRoot.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import { Provider as JotaiProvider } from 'jotai';

jest.mock('../Search/SearchInput', () => {
return jest.fn().mockImplementation(() => <div />);
});

jest.mock('../../utils/common', () => {
const utils = jest.requireActual('../../utils/common');
return {
Expand Down
9 changes: 9 additions & 0 deletions src/components/Search/SearchInput.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
@import "~@redhat-cloud-services/frontend-components-utilities/styles/_all";
@import '~@patternfly/patternfly/patternfly-addons.scss';


.chr-c-search-title {
mark {
font-weight: var(--pf-v5-global--FontWeight--bold);
background-color: transparent;
}
}

.chr-c-search {
&__collapsed {
justify-content: flex-end;
Expand Down
116 changes: 15 additions & 101 deletions src/components/Search/SearchInput.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,27 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import debounce from 'lodash/debounce';
import { Bullseye } from '@patternfly/react-core/dist/dynamic/layouts/Bullseye';
import { Menu, MenuContent, MenuGroup, MenuItem, MenuList } from '@patternfly/react-core/dist/dynamic/components/Menu';
import { SearchInput as PFSearchInput, SearchInputProps } from '@patternfly/react-core/dist/dynamic/components/SearchInput';
import { Spinner } from '@patternfly/react-core/dist/dynamic/components/Spinner';
import { Popper } from '@patternfly/react-core/dist/dynamic/helpers/Popper/Popper';

import debounce from 'lodash/debounce';
import uniq from 'lodash/uniq';
import uniqWith from 'lodash/uniqWith';

import './SearchInput.scss';
import { AUTOSUGGEST_TERM_DELIMITER, SearchAutoSuggestionResponseType, SearchAutoSuggestionResultItem, SearchResponseType } from './SearchTypes';

import EmptySearchState from './EmptySearchState';
import { isProd } from '../../utils/common';
import { useSegment } from '../../analytics/useSegment';
import useWindowWidth from '../../hooks/useWindowWidth';
import ChromeLink from '../ChromeLink';
import SearchTitle from './SearchTitle';
import SearchDescription from './SearchDescription';
import { useAtomValue } from 'jotai';
import { asyncLocalOrama } from '../../state/atoms/localSearchAtom';
import { localQuery } from '../../utils/localSearch';

export type SearchInputprops = {
isExpanded?: boolean;
};

const IS_PROD = isProd();
const REPLACE_TAG = 'REPLACE_TAG';
const REPLACE_COUNT_TAG = 'REPLACE_COUNT_TAG';
/**
* The ?q is the search term.
* ------
* The "~" after the search term enables fuzzy search (case sensitivity, similar results for typos).
* For example "inventry" query yields results with Inventory string within it.
* We can use distance ~(0-2) for example: "~2" to narrow restrict/expand the fuzzy search range
*
* Query parsin docs: https://solr.apache.org/guide/7_7/the-standard-query-parser.html#the-standard-query-parser
*
* hl=true enables string "highlight"
* hl.fl=field_name specifies field to be highlighted
*/

const BASE_SEARCH = new URLSearchParams();
BASE_SEARCH.append('q', `alt_titles:${REPLACE_TAG}`); // add query replacement tag and enable fuzzy search with ~ and wildcards
BASE_SEARCH.append('fq', 'documentKind:ModuleDefinition'); // search for ModuleDefinition documents
BASE_SEARCH.append('rows', `${REPLACE_COUNT_TAG}`); // request 10 results

const BASE_URL = new URL(`https://access.${IS_PROD ? '' : 'stage.'}redhat.com/hydra/rest/search/platform/console/`);
// search API stopped receiving encoded search string
BASE_URL.search = decodeURIComponent(BASE_SEARCH.toString());

const SUGGEST_SEARCH = new URLSearchParams();
SUGGEST_SEARCH.append('redhat_client', 'console'); // required client id
SUGGEST_SEARCH.append('q', REPLACE_TAG); // add query replacement tag and enable fuzzy search with ~ and wildcards
SUGGEST_SEARCH.append('suggest.count', '10'); // request 10 results
SUGGEST_SEARCH.append('suggest.dictionary', 'improvedInfixSuggester'); // console new suggest dictionary
SUGGEST_SEARCH.append('suggest.dictionary', 'default');

const SUGGEST_URL = new URL(`https://access.${IS_PROD ? '' : 'stage.'}redhat.com/hydra/proxy/gss-diag/rs/search/autosuggest`);
// search API stopped receiving encoded search string
SUGGEST_URL.search = decodeURIComponent(SUGGEST_SEARCH.toString());
const SUGGEST_SEARCH_QUERY = SUGGEST_URL.toString();

const getMaxMenuHeight = (menuElement?: HTMLDivElement | null) => {
if (!menuElement) {
return 0;
Expand All @@ -82,29 +44,16 @@ type SearchInputListener = {
onStateChange: (isOpen: boolean) => void;
};

function parseSuggestions(suggestions: SearchAutoSuggestionResultItem[] = []) {
return suggestions.map((suggestion) => {
const [allTitle, bundleTitle, abstract] = suggestion.term.split(AUTOSUGGEST_TERM_DELIMITER);
const url = new URL(suggestion.payload);
return {
item: {
title: allTitle,
bundleTitle,
description: abstract,
pathname: url.pathname,
},
allTitle,
};
});
}

const SearchInput = ({ onStateChange }: SearchInputListener) => {
const [isOpen, setIsOpen] = useState(false);
const [searchValue, setSearchValue] = useState('');
const [isFetching, setIsFetching] = useState(false);
const [searchItems, setSearchItems] = useState<SearchItem[]>([]);
const { ready, analytics } = useSegment();
const blockCloseEvent = useRef(false);
const asyncLocalOramaData = useAtomValue(asyncLocalOrama);

const debouncedTrack = useCallback(analytics ? debounce(analytics.track, 1000) : () => null, [analytics]);

const isMounted = useRef(false);
const toggleRef = useRef<HTMLInputElement>(null);
Expand Down Expand Up @@ -183,10 +132,9 @@ const SearchInput = ({ onStateChange }: SearchInputListener) => {
window.removeEventListener('resize', handleWindowResize);
isMounted.current = false;
};
}, []);
}, [isOpen, menuRef, resultCount]);

useEffect(() => {
handleWindowResize();
window.addEventListener('keydown', handleMenuKeys);
window.addEventListener('click', handleClickOutside);
return () => {
Expand All @@ -195,51 +143,17 @@ const SearchInput = ({ onStateChange }: SearchInputListener) => {
};
}, [isOpen, menuRef]);

const handleFetch = async (value = '') => {
const response = (await fetch(SUGGEST_SEARCH_QUERY.replaceAll(REPLACE_TAG, value)).then((r) => r.json())) as SearchAutoSuggestionResponseType;

// parse default suggester
// parse improved suggester
let items: { item: SearchItem; allTitle: string }[] = [];
items = items
.concat(
parseSuggestions(response?.suggest?.default[value]?.suggestions),
parseSuggestions(response?.suggest?.improvedInfixSuggester[value]?.suggestions)
)
.slice(0, 10);
const suggests = uniq(items.map(({ allTitle }) => allTitle.replace(/(<b>|<\/b>)/gm, '').trim()));
let searchItems = items.map(({ item }) => item);
if (items.length < 10) {
const altTitleResults = (await fetch(
BASE_URL.toString()
.replaceAll(REPLACE_TAG, `(${suggests.length > 0 ? suggests.join(' OR ') + ' OR ' : ''}${value})`)
.replaceAll(REPLACE_COUNT_TAG, '10')
).then((r) => r.json())) as { response: SearchResponseType };
searchItems = searchItems.concat(
altTitleResults.response.docs.map((doc) => ({
pathname: doc.relative_uri,
bundleTitle: doc.bundle_title[0],
title: doc.allTitle,
description: doc.abstract,
}))
);
}
searchItems = uniqWith(searchItems, (a, b) => a.title.replace(/(<b>|<\/b>)/gm, '').trim() === b.title.replace(/(<b>|<\/b>)/gm, '').trim());
setSearchItems(searchItems.slice(0, 10));
const handleChange: SearchInputProps['onChange'] = async (_e, value) => {
setSearchValue(value);
setIsFetching(true);
const results = await localQuery(asyncLocalOramaData, value);
setSearchItems(results ?? []);
isMounted.current && setIsFetching(false);
if (ready && analytics) {
analytics.track('chrome.search-query', { query: value });
debouncedTrack('chrome.search-query', { query: value });
}
};

const debouncedFetch = useCallback(debounce(handleFetch, 500), []);

const handleChange: SearchInputProps['onChange'] = (_e, value) => {
setSearchValue(value);
setIsFetching(true);
debouncedFetch(value);
};

const [isExpanded, setIsExpanded] = React.useState(false);

const onToggleExpand = (_event: React.SyntheticEvent<HTMLButtonElement>, isExpanded: boolean) => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Search/SearchTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const SearchTitle = ({ title, bundleTitle }: { title: string; bundleTitle: strin
const showBundleTitle = bundleTitle.replace(/\s/g, '').length > 0;
return (
<TextContent>
<Text component="small" className="pf-v5-u-link-color" dangerouslySetInnerHTML={{ __html: title }}></Text>
<Text component="small" className="pf-v5-u-link-color chr-c-search-title" dangerouslySetInnerHTML={{ __html: title }}></Text>
{showBundleTitle && (
<Text component="small" className="pf-v5-u-link-color">
<span className="pf-v5-u-px-sm">|</span>
Expand Down
42 changes: 0 additions & 42 deletions src/components/Search/SearchTypes.ts

This file was deleted.

Loading

0 comments on commit 5b5ce55

Please sign in to comment.