Improves ordering of search results

Modifies documents.search to return a context snippet and search ranking
Displays context snipped on search results screen
This commit is contained in:
Tom Moor
2018-08-04 18:32:56 -07:00
parent 96348ced38
commit e192bcbaee
10 changed files with 121 additions and 63 deletions

View File

@@ -13,6 +13,7 @@ import DocumentMenu from 'menus/DocumentMenu';
type Props = {
document: Document,
highlight?: ?string,
context?: ?string,
showCollection?: boolean,
innerRef?: *,
};
@@ -96,6 +97,15 @@ const Title = styled(Highlight)`
text-overflow: ellipsis;
`;
const ResultContext = styled(Highlight)`
color: ${props => props.theme.slateDark};
font-size: 14px;
margin-top: 0;
margin-bottom: 0.25em;
`;
const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi;
@observer
class DocumentPreview extends React.Component<Props> {
star = (ev: SyntheticEvent<*>) => {
@@ -110,15 +120,26 @@ class DocumentPreview extends React.Component<Props> {
this.props.document.unstar();
};
replaceResultMarks = (tag: string) => {
// don't use SEARCH_RESULT_REGEX here as it causes
// an infinite loop to trigger a regex inside it's own callback
return tag.replace(/<b\b[^>]*>(.*?)<\/b>/gi, '$1');
};
render() {
const {
document,
showCollection,
innerRef,
highlight,
context,
...rest
} = this.props;
const queryIsInTitle =
!!highlight &&
!!document.title.toLowerCase().match(highlight.toLowerCase());
return (
<DocumentLink
to={{
@@ -141,6 +162,13 @@ class DocumentPreview extends React.Component<Props> {
)}
<StyledDocumentMenu document={document} />
</Heading>
{!queryIsInTitle && (
<ResultContext
text={context}
highlight={highlight ? SEARCH_RESULT_REGEX : undefined}
processResult={this.replaceResultMarks}
/>
)}
<PublishingInfo
document={document}
collection={showCollection ? document.collection : undefined}

View File

@@ -4,23 +4,34 @@ import replace from 'string-replace-to-array';
import styled from 'styled-components';
type Props = {
highlight: ?string,
highlight: ?string | RegExp,
processResult?: (tag: string) => string,
text: string,
caseSensitive?: boolean,
};
function Highlight({ highlight, caseSensitive, text, ...rest }: Props) {
function Highlight({
highlight,
processResult,
caseSensitive,
text,
...rest
}: Props) {
let regex;
if (highlight instanceof RegExp) {
regex = highlight;
} else {
regex = new RegExp(
(highlight || '').replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&'),
caseSensitive ? 'g' : 'gi'
);
}
return (
<span {...rest}>
{highlight
? replace(
text,
new RegExp(
(highlight || '').replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&'),
caseSensitive ? 'g' : 'gi'
),
(tag, index) => <Mark key={index}>{tag}</Mark>
)
? replace(text, regex, (tag, index) => (
<Mark key={index}>{processResult ? processResult(tag) : tag}</Mark>
))
: text}
</span>
);

View File

@@ -222,17 +222,12 @@ class DocumentScene extends React.Component<Props> {
};
onSearchLink = async (term: string) => {
const resultIds = await this.props.documents.search(term);
const results = await this.props.documents.search(term);
return resultIds.map((id, index) => {
const document = this.props.documents.getById(id);
if (!document) return {};
return {
title: document.title,
url: document.url,
};
});
return results.map((result, index) => ({
title: result.document.title,
url: result.document.url,
}));
};
onClickLink = (href: string) => {

View File

@@ -5,6 +5,7 @@ import keydown from 'react-keydown';
import Waypoint from 'react-waypoint';
import { observable, action } from 'mobx';
import { observer, inject } from 'mobx-react';
import { SearchResult } from 'types';
import _ from 'lodash';
import DocumentsStore, {
DEFAULT_PAGINATION_LIMIT,
@@ -62,7 +63,7 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)`
class Search extends React.Component<Props> {
firstDocument: HTMLElement;
@observable resultIds: string[] = []; // Document IDs
@observable results: SearchResult[] = [];
@observable query: string = '';
@observable offset: number = 0;
@observable allowLoadMore: boolean = true;
@@ -104,7 +105,7 @@ class Search extends React.Component<Props> {
handleQueryChange = () => {
const query = this.props.match.params.query;
this.query = query ? query : '';
this.resultIds = [];
this.results = [];
this.offset = 0;
this.allowLoadMore = true;
@@ -134,16 +135,14 @@ class Search extends React.Component<Props> {
if (this.query) {
try {
const newResults = await this.props.documents.search(this.query, {
const results = await this.props.documents.search(this.query, {
offset: this.offset,
limit: DEFAULT_PAGINATION_LIMIT,
});
this.resultIds = this.resultIds.concat(newResults);
if (this.resultIds.length > 0) this.pinToTop = true;
if (
newResults.length === 0 ||
newResults.length < DEFAULT_PAGINATION_LIMIT
) {
this.results = this.results.concat(results);
if (this.results.length > 0) this.pinToTop = true;
if (results.length === 0 || results.length < DEFAULT_PAGINATION_LIMIT) {
this.allowLoadMore = false;
} else {
this.offset += DEFAULT_PAGINATION_LIMIT;
@@ -152,7 +151,7 @@ class Search extends React.Component<Props> {
console.error('Something went wrong');
}
} else {
this.resultIds = [];
this.results = [];
this.pinToTop = false;
}
@@ -177,7 +176,7 @@ class Search extends React.Component<Props> {
render() {
const { documents, notFound } = this.props;
const showEmpty =
!this.isFetching && this.query && this.resultIds.length === 0;
!this.isFetching && this.query && this.results.length === 0;
return (
<Container auto>
@@ -201,17 +200,19 @@ class Search extends React.Component<Props> {
mode={ArrowKeyNavigation.mode.VERTICAL}
defaultActiveChildIndex={0}
>
{this.resultIds.map((documentId, index) => {
const document = documents.getById(documentId);
{this.results.map((result, index) => {
const document = documents.getById(result.document.id);
if (!document) return null;
return (
<DocumentPreview
innerRef={ref =>
index === 0 && this.setFirstDocumentRef(ref)
}
key={documentId}
key={document.id}
document={document}
highlight={this.query}
context={result.context}
showCollection
/>
);

View File

@@ -7,7 +7,7 @@ import invariant from 'invariant';
import BaseStore from 'stores/BaseStore';
import Document from 'models/Document';
import UiStore from 'stores/UiStore';
import type { PaginationParams } from 'types';
import type { PaginationParams, SearchResult } from 'types';
export const DEFAULT_PAGINATION_LIMIT = 25;
@@ -156,15 +156,15 @@ class DocumentsStore extends BaseStore {
search = async (
query: string,
options: ?PaginationParams
): Promise<string[]> => {
): Promise<SearchResult[]> => {
const res = await client.get('/documents.search', {
...options,
query,
});
invariant(res && res.data, 'res or res.data missing');
invariant(res && res.data, 'Search API response should be available');
const { data } = res;
data.forEach(documentData => this.add(new Document(documentData)));
return data.map(documentData => documentData.id);
data.forEach(result => this.add(new Document(result.document)));
return data;
};
@action

View File

@@ -78,3 +78,9 @@ export type ApiKey = {
name: string,
secret: string,
};
export type SearchResult = {
ranking: number,
context: string,
document: Document,
};