feat: Record search queries (#1554)
* Record search queries * feat: add totalCount to the search response * feat: add comments to explain why we use setTimeout
This commit is contained in:
@@ -251,10 +251,13 @@ Document.findByPk = async function (id, options = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
type SearchResult = {
|
||||
ranking: number,
|
||||
context: string,
|
||||
document: Document,
|
||||
type SearchResponse = {
|
||||
results: {
|
||||
ranking: number,
|
||||
context: string,
|
||||
document: Document,
|
||||
}[],
|
||||
totalCount: number,
|
||||
};
|
||||
|
||||
type SearchOptions = {
|
||||
@@ -277,7 +280,7 @@ Document.searchForTeam = async (
|
||||
team,
|
||||
query,
|
||||
options: SearchOptions = {}
|
||||
): Promise<SearchResult[]> => {
|
||||
): Promise<SearchResponse> => {
|
||||
const limit = options.limit || 15;
|
||||
const offset = options.offset || 0;
|
||||
const wildcardQuery = `${escape(query)}:*`;
|
||||
@@ -285,21 +288,25 @@ Document.searchForTeam = async (
|
||||
|
||||
// If the team has access no public collections then shortcircuit the rest of this
|
||||
if (!collectionIds.length) {
|
||||
return [];
|
||||
return { results: [], totalCount: 0 };
|
||||
}
|
||||
|
||||
// Build the SQL query to get documentIds, ranking, and search term context
|
||||
const sql = `
|
||||
const whereClause = `
|
||||
"searchVector" @@ to_tsquery('english', :query) AND
|
||||
"teamId" = :teamId AND
|
||||
"collectionId" IN(:collectionIds) AND
|
||||
"deletedAt" IS NULL AND
|
||||
"publishedAt" IS NOT NULL
|
||||
`;
|
||||
|
||||
const selectSql = `
|
||||
SELECT
|
||||
id,
|
||||
ts_rank(documents."searchVector", to_tsquery('english', :query)) as "searchRanking",
|
||||
ts_headline('english', "text", to_tsquery('english', :query), 'MaxFragments=1, MinWords=20, MaxWords=30') as "searchContext"
|
||||
FROM documents
|
||||
WHERE "searchVector" @@ to_tsquery('english', :query) AND
|
||||
"teamId" = :teamId AND
|
||||
"collectionId" IN(:collectionIds) AND
|
||||
"deletedAt" IS NULL AND
|
||||
"publishedAt" IS NOT NULL
|
||||
WHERE ${whereClause}
|
||||
ORDER BY
|
||||
"searchRanking" DESC,
|
||||
"updatedAt" DESC
|
||||
@@ -307,17 +314,34 @@ Document.searchForTeam = async (
|
||||
OFFSET :offset;
|
||||
`;
|
||||
|
||||
const results = await sequelize.query(sql, {
|
||||
const countSql = `
|
||||
SELECT COUNT(id)
|
||||
FROM documents
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
|
||||
const queryReplacements = {
|
||||
teamId: team.id,
|
||||
query: wildcardQuery,
|
||||
collectionIds,
|
||||
};
|
||||
|
||||
const resultsQuery = sequelize.query(selectSql, {
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
replacements: {
|
||||
teamId: team.id,
|
||||
query: wildcardQuery,
|
||||
...queryReplacements,
|
||||
limit,
|
||||
offset,
|
||||
collectionIds,
|
||||
},
|
||||
});
|
||||
|
||||
const countQuery = sequelize.query(countSql, {
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
replacements: queryReplacements,
|
||||
});
|
||||
|
||||
const [results, [{ count }]] = await Promise.all([resultsQuery, countQuery]);
|
||||
|
||||
// Final query to get associated document data
|
||||
const documents = await Document.findAll({
|
||||
where: {
|
||||
@@ -330,20 +354,23 @@ Document.searchForTeam = async (
|
||||
],
|
||||
});
|
||||
|
||||
return map(results, (result) => ({
|
||||
ranking: result.searchRanking,
|
||||
context: removeMarkdown(unescape(result.searchContext), {
|
||||
stripHTML: false,
|
||||
}),
|
||||
document: find(documents, { id: result.id }),
|
||||
}));
|
||||
return {
|
||||
results: map(results, (result) => ({
|
||||
ranking: result.searchRanking,
|
||||
context: removeMarkdown(unescape(result.searchContext), {
|
||||
stripHTML: false,
|
||||
}),
|
||||
document: find(documents, { id: result.id }),
|
||||
})),
|
||||
totalCount: count,
|
||||
};
|
||||
};
|
||||
|
||||
Document.searchForUser = async (
|
||||
user,
|
||||
query,
|
||||
options: SearchOptions = {}
|
||||
): Promise<SearchResult[]> => {
|
||||
): Promise<SearchResponse> => {
|
||||
const limit = options.limit || 15;
|
||||
const offset = options.offset || 0;
|
||||
const wildcardQuery = `${escape(query)}:*`;
|
||||
@@ -360,7 +387,7 @@ Document.searchForUser = async (
|
||||
|
||||
// If the user has access to no collections then shortcircuit the rest of this
|
||||
if (!collectionIds.length) {
|
||||
return [];
|
||||
return { results: [], totalCount: 0 };
|
||||
}
|
||||
|
||||
let dateFilter;
|
||||
@@ -369,13 +396,8 @@ Document.searchForUser = async (
|
||||
}
|
||||
|
||||
// Build the SQL query to get documentIds, ranking, and search term context
|
||||
const sql = `
|
||||
SELECT
|
||||
id,
|
||||
ts_rank(documents."searchVector", to_tsquery('english', :query)) as "searchRanking",
|
||||
ts_headline('english', "text", to_tsquery('english', :query), 'MaxFragments=1, MinWords=20, MaxWords=30') as "searchContext"
|
||||
FROM documents
|
||||
WHERE "searchVector" @@ to_tsquery('english', :query) AND
|
||||
const whereClause = `
|
||||
"searchVector" @@ to_tsquery('english', :query) AND
|
||||
"teamId" = :teamId AND
|
||||
"collectionId" IN(:collectionIds) AND
|
||||
${
|
||||
@@ -393,27 +415,52 @@ Document.searchForUser = async (
|
||||
? '("publishedAt" IS NOT NULL OR "createdById" = :userId)'
|
||||
: '"publishedAt" IS NOT NULL'
|
||||
}
|
||||
`;
|
||||
|
||||
const selectSql = `
|
||||
SELECT
|
||||
id,
|
||||
ts_rank(documents."searchVector", to_tsquery('english', :query)) as "searchRanking",
|
||||
ts_headline('english', "text", to_tsquery('english', :query), 'MaxFragments=1, MinWords=20, MaxWords=30') as "searchContext"
|
||||
FROM documents
|
||||
WHERE ${whereClause}
|
||||
ORDER BY
|
||||
"searchRanking" DESC,
|
||||
"updatedAt" DESC
|
||||
LIMIT :limit
|
||||
OFFSET :offset;
|
||||
`;
|
||||
`;
|
||||
|
||||
const results = await sequelize.query(sql, {
|
||||
const countSql = `
|
||||
SELECT COUNT(id)
|
||||
FROM documents
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
|
||||
const queryReplacements = {
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
collaboratorIds: options.collaboratorIds,
|
||||
query: wildcardQuery,
|
||||
collectionIds,
|
||||
dateFilter,
|
||||
};
|
||||
|
||||
const resultsQuery = sequelize.query(selectSql, {
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
replacements: {
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
collaboratorIds: options.collaboratorIds,
|
||||
query: wildcardQuery,
|
||||
...queryReplacements,
|
||||
limit,
|
||||
offset,
|
||||
collectionIds,
|
||||
dateFilter,
|
||||
},
|
||||
});
|
||||
|
||||
const countQuery = sequelize.query(countSql, {
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
replacements: queryReplacements,
|
||||
});
|
||||
|
||||
const [results, [{ count }]] = await Promise.all([resultsQuery, countQuery]);
|
||||
// Final query to get associated document data
|
||||
const documents = await Document.scope(
|
||||
{
|
||||
@@ -432,13 +479,16 @@ Document.searchForUser = async (
|
||||
],
|
||||
});
|
||||
|
||||
return map(results, (result) => ({
|
||||
ranking: result.searchRanking,
|
||||
context: removeMarkdown(unescape(result.searchContext), {
|
||||
stripHTML: false,
|
||||
}),
|
||||
document: find(documents, { id: result.id }),
|
||||
}));
|
||||
return {
|
||||
results: map(results, (result) => ({
|
||||
ranking: result.searchRanking,
|
||||
context: removeMarkdown(unescape(result.searchContext), {
|
||||
stripHTML: false,
|
||||
}),
|
||||
document: find(documents, { id: result.id }),
|
||||
})),
|
||||
totalCount: count,
|
||||
};
|
||||
};
|
||||
|
||||
// Hooks
|
||||
|
||||
Reference in New Issue
Block a user