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:
@@ -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 = {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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] : '';
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user