Basic Full-text Search #393
Replies: 2 comments 1 reply
-
owner @Darmody |
Beta Was this translation helpful? Give feedback.
-
Global ShortcutsFirst, we need to introduce global shortcuts to support invoke search popover by hotkeys. react-hotkeys-hook provides an easy way to define hotkeys via react hooks. We can create a hotkeys hook for the web app.
Search and Filter
To support text searching and conditions filters, the design system should have a Filter types// Filters by kinds
type FilterKind = 'document' | 'media' | 'spreadsheet' | 'command' | 'and so on...' // Filters by the range of creating date
type FilterCreatedAt = [Date, Date] // Filters by the folder name
type FilterFolder = string // Filters by the author id
type FilterAuthor = number Search popover
There are two components function SearchPopover: FC = () => {
const [searchContent, filters] = useSearchingContent()
const [activeIndex] = useActiveIndex()
const searchingResults = useSearchResults(searchingContent, filters)
return <Popover>
<FilterInput searchContent={searchContent} filters={filters} />
<SearchingResults activeIndex={activeIndex} searchingResults={...searchingResults} />
</Popover>
} FilterInput
SearchingResults
interface SearchingResultsProps {
// An array of searching result items
searchResultItems: Array<SearchResultItem>
// Active item index
activeIndex: number | undefined
} Showing default results or recently used results if no search content or filters are offered. function useRecentlyResults(searchResults) {
useEffect(() => {
// Set recently used results to local storage
}, [searchResults])
return useMemo(() => // Get items from local storage, [])
}
function useSearchResults() {
const searchResults = useState()
const recentlyResults = useRecentlyResults(searchResults)
// Handle searching
return useMemo(() => searchResults.length > 0 ? searchResults : recentlyResults, [searchResults, recentlyResults])
} The order of results should base on the The container showing the results array should have a const SearchingResultsContainer = styled('div', {
maxHeight: 'a fixed max height or 100% if popover component can detect the suitable height for the viewport.'
overflow scroll
}) We should add const [index, setIndex] = useActiveIndex()
function onKeydown(event) {
switch (event.key) {
case 'ArrowUp': {
// set the active index
setIndex(Math.max(0, Math.min(index, itemLength - 1))
// 2. Scroll item into view. element.scrollIntoView().
}
}
} Active selection should be reset when search content changed. NOTICE: it is not related to search results but search content, because search results may be changed in an async way. function useActiveIndex(searchContent: string | undefined, filters: Filter[] | undefined) {
const [index, setIndex] = useState<number>(undefined)
useEffect(() => {
setIndex(undefined)
}, [searchContent, filters])
return [index, setIndex]
} Search Result
interface SearchResultItem {
// The represent what kind of the result is.
kind: 'command' | 'document' | 'and more...'
// The title of the search result
title: string
// The description of the search result
description?: string
// The icon of the search result
icon?: string
// The preview of search result
preview?: ReactNode
// The action when the item is selected.
onSelect: Function
// The relevance of the result and search/filter content. It is useful in showing items in the result list.
relevance: number
} interface SearchResultCommandItem extends SearchResultItem {
// The HTML content of the API doc of the command.
APIDoc: string
}
// Import the styles of the API doc, a top-level className, and styling the basic markdown structure like `h1, h2, h3, a, p ...`. interface SearchResultDocumentItem extends SearchResultItem {
// The id of the document
documentId: string
// The selection bookmarks of the matched content inside the document.
selectionBookmarks: SelectionBookMark[]
} interface SearchResultFormulaItem extends SearchResultItem {
// The id of the document
documentId: string
// The id of the formula
formulaId: string
// The selection bookmarks of the matched content inside the document.
selectionBookmarks: SelectionBookMark[]
} interface SearchResultSpreadsheetItem extends SearchResultItem {
// The id of the document
documentId: string
// The id of the spreadsheet
spreadsheetId: string
// The selection bookmarks of the matched content inside the document.
selectionBookmarks: SelectionBookMark[]
} interface SearchResultMediaItem extends SearchResultItem {
// The id of the document
documentId: string
// The content type of media
contentType: string
// The source link of the media
src: string
// The selection bookmark of the matched content inside the document.
selectionBookmark: SelectionBookMark
} Highlight the matched contenthighlight the keywords inside the description.const description = description.replace(keyword, `<mark>${keyword}</mark>`) highlight the keywords inside the previewSee below. How Search WorkDue to various kinds of search results, there must be multiple ways to search. Hence we need a Search Workflow
interface SearchWorkflow {
// Name of the current searching workflow.
name: string
// A function for searching. Return can be a promise.
onSearch: (textContent: string | undefined, filters: Filter[]) => SearchResultItem[] | Promise<SearchResultMediaItem[]>
} function useSearchResults(textContent: string | undefined, filters: Filter[]): SearchResultItem[] {
const [results, setResults] = useState([])
useEffect(() => {
// Should add debounce for searching changing.
debounce(() => {
// Calls every workflow.
workflows.forEach(workflow => {
const results = workflow.onSearch(textContent, filters)
// Async case
if (result is Promise) {
// 1. Awaiting result, and it must be cancelable.
// 2. Push results into the state.
// Sync case
} else {
setResults(currentResults => [...currentResults, ...results])
}
})
})
return () => {
// Clear the effects from the last dependencies. Like async result awaiting.
}
}, [textContent, filters])
return results
} Command SearchCommand search is a sync search workflow. we can add a commands list in the web app and search for commands by comparing command names and search content. const CommandSearchWorkflow: SearchWorkflow = {
name: 'command',
onSearch(textContent) {
return commands.filter(command => command.toLowerCase().includes(textContent?.toLowerCase()))
}
} Full-text SearchFull-text search is an async search workflow. And the highlights of keywords should be marked on the client-side.
const FullTextSearchWorkflow: SearchWorkflow = {
name: 'fullText',
onSearch(textContent, filters) {
const blocks = await querySearchBlocks(textContent, filters)
return blocks.map(block => ({
// Map to SearchResultItem
onSelect: () => // navigate to document page
preview: () => {
// 1. Render the preview document
// 2. Highlight the keyword
doc.descendants(node => {
if (node matches the keyword) {
// Add highlight mark to node
}
})
}
}))
}
} IdeasThe folders in the left sidebar can be right-clicked, and a search option will show up in the context menu. The option will pop up the search popover and filter the search results in that folder by default. |
Beta Was this translation helpful? Give feedback.
-
Problem
Articulate the problem that this piece of work addresses
We need to support basic full text search.
Why is this the right time to address this problem?
We should prioritize this problem to improve the basic usage experience.
Time Budget
1 weeks
Solution
Out of Bounds
Support for searching plain text content only.
Rabbitholes
Beta Was this translation helpful? Give feedback.
All reactions