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

@@ -226,13 +226,16 @@ router.post('documents.search', auth(), pagination(), async ctx => {
ctx.assertPresent(query, 'query is required');
const user = ctx.state.user;
const documents = await Document.searchForUser(user, query, {
const results = await Document.searchForUser(user, query, {
offset,
limit,
});
const data = await Promise.all(
documents.map(async document => await presentDocument(ctx, document))
results.map(async result => {
const document = await presentDocument(ctx, result.document);
return { ...result, document };
})
);
ctx.body = {

View File

@@ -228,7 +228,7 @@ describe('#documents.search', async () => {
expect(res.status).toEqual(200);
expect(body.data.length).toEqual(1);
expect(body.data[0].text).toEqual('# Much guidance');
expect(body.data[0].document.text).toEqual('# Much guidance');
});
it('should require authentication', async () => {

View File

@@ -64,14 +64,14 @@ router.post('hooks.slack', async ctx => {
if (!user) throw new InvalidRequestError('Invalid user');
const documents = await Document.searchForUser(user, text, {
const results = await Document.searchForUser(user, text, {
limit: 5,
});
if (documents.length) {
if (results.length) {
const attachments = [];
for (const document of documents) {
attachments.push(presentSlackAttachment(document));
for (const result of results) {
attachments.push(presentSlackAttachment(result.document));
}
ctx.body = {

View File

@@ -1,6 +1,6 @@
// @flow
import slug from 'slug';
import _ from 'lodash';
import { map, find, compact, uniq } from 'lodash';
import randomstring from 'randomstring';
import MarkdownSerializer from 'slate-md-serializer';
import Plain from 'slate-plain-serializer';
@@ -11,6 +11,7 @@ import { Collection } from '../models';
import { DataTypes, sequelize } from '../sequelize';
import events from '../events';
import parseTitle from '../../shared/utils/parseTitle';
import unescape from '../../shared/utils/unescape';
import Revision from './Revision';
const Op = Sequelize.Op;
@@ -65,7 +66,7 @@ const beforeSave = async doc => {
// add the current user as revision hasn't been generated yet
ids.push(doc.lastModifiedById);
doc.collaboratorIds = _.uniq(ids);
doc.collaboratorIds = uniq(ids);
// increment revision
doc.revisionCount += 1;
@@ -188,44 +189,57 @@ Document.findById = async id => {
}
};
type SearchResult = {
ranking: number,
context: string,
document: Document,
};
Document.searchForUser = async (
user,
query,
options = {}
): Promise<Document[]> => {
): Promise<SearchResult[]> => {
const limit = options.limit || 15;
const offset = options.offset || 0;
const sql = `
SELECT *, ts_rank(documents."searchVector", plainto_tsquery('english', :query)) as "searchRanking" FROM documents
WHERE "searchVector" @@ plainto_tsquery('english', :query) AND
"teamId" = '${user.teamId}'::uuid AND
"deletedAt" IS NULL
ORDER BY "searchRanking" DESC
LIMIT :limit OFFSET :offset;
`;
SELECT
id,
ts_rank(documents."searchVector", plainto_tsquery('english', :query)) as "searchRanking",
ts_headline('english', "text", plainto_tsquery('english', :query), 'MaxFragments=0, MinWords=10, MaxWords=30') as "searchContext"
FROM documents
WHERE "searchVector" @@ plainto_tsquery('english', :query) AND
"teamId" = '${user.teamId}'::uuid AND
"deletedAt" IS NULL
ORDER BY "searchRanking", "updatedAt" DESC
LIMIT :limit
OFFSET :offset;
`;
const results = await sequelize.query(sql, {
type: sequelize.QueryTypes.SELECT,
replacements: {
query,
limit,
offset,
},
model: Document,
});
const ids = results.map(document => document.id);
// Second query to get views for the data
// Second query to get associated document data
const withViewsScope = { method: ['withViews', user.id] };
const documents = await Document.scope(
'defaultScope',
withViewsScope
).findAll({
where: { id: ids },
where: { id: map(results, 'id') },
});
// Order the documents in the same order as the first query
return _.sortBy(documents, doc => ids.indexOf(doc.id));
return map(results, result => ({
ranking: result.searchRanking,
context: unescape(result.searchContext),
document: find(documents, { id: result.id }),
}));
};
// Hooks
@@ -282,7 +296,7 @@ Document.prototype.getTimestamp = function() {
Document.prototype.getSummary = function() {
const value = Markdown.deserialize(this.text);
const plain = Plain.serialize(value);
const lines = _.compact(plain.split('\n'));
const lines = compact(plain.split('\n'));
return lines.length >= 1 ? lines[1] : '';
};