Merge pull request #2992 from omnivore-app/perf/search-api
perf: limit search item results to 100 a time and fetch labels and recommendations only needed
This commit is contained in:
@ -57,6 +57,7 @@ import { findHighlightsByLibraryItemId } from '../../services/highlights'
|
||||
import {
|
||||
addLabelsToLibraryItem,
|
||||
findLabelsByIds,
|
||||
findLabelsByLibraryItemId,
|
||||
findOrCreateLabels,
|
||||
saveLabelsInLibraryItem,
|
||||
} from '../../services/labels'
|
||||
@ -69,6 +70,7 @@ import {
|
||||
updateLibraryItemReadingProgress,
|
||||
updateLibraryItems,
|
||||
} from '../../services/library_item'
|
||||
import { findRecommendationsByLibraryItemId } from '../../services/recommendation'
|
||||
import { parsedContentToLibraryItem } from '../../services/save_page'
|
||||
import {
|
||||
findUploadFileById,
|
||||
@ -610,7 +612,7 @@ export const searchResolver = authorized<
|
||||
QuerySearchArgs
|
||||
>(async (_obj, params, { log, uid }) => {
|
||||
const startCursor = params.after || ''
|
||||
const first = params.first || 10
|
||||
const first = Math.min(params.first || 10, 100) // limit to 100 items
|
||||
|
||||
// the query size is limited to 255 characters
|
||||
if (params.query && params.query.length > 255) {
|
||||
@ -641,25 +643,7 @@ export const searchResolver = authorized<
|
||||
libraryItems.pop()
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
libraryItems.map(async (libraryItem) => {
|
||||
if (
|
||||
libraryItem.highlightAnnotations &&
|
||||
libraryItem.highlightAnnotations.length > 0
|
||||
) {
|
||||
// fetch highlights for each item
|
||||
libraryItem.highlights = await findHighlightsByLibraryItemId(
|
||||
libraryItem.id,
|
||||
uid
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const edges = libraryItems.map((libraryItem) => {
|
||||
if (libraryItem.siteIcon && !isBase64Image(libraryItem.siteIcon)) {
|
||||
libraryItem.siteIcon = createImageProxyUrl(libraryItem.siteIcon, 128, 128)
|
||||
}
|
||||
if (params.includeContent && libraryItem.readableContent) {
|
||||
// convert html to the requested format
|
||||
const format = params.format || ArticleFormat.Html
|
||||
|
||||
@ -6,15 +6,19 @@
|
||||
import { Subscription } from '../entity/subscription'
|
||||
import {
|
||||
Article,
|
||||
Highlight,
|
||||
Label,
|
||||
PageType,
|
||||
Recommendation,
|
||||
SearchItem,
|
||||
} from '../generated/graphql'
|
||||
import { findHighlightsByLibraryItemId } from '../services/highlights'
|
||||
import { findLabelsByLibraryItemId } from '../services/labels'
|
||||
import { findRecommendationsByLibraryItemId } from '../services/recommendation'
|
||||
import { findUploadFileById } from '../services/upload_file'
|
||||
import {
|
||||
highlightDataToHighlight,
|
||||
isBase64Image,
|
||||
recommandationDataToRecommendation,
|
||||
validatedDate,
|
||||
wordsCount,
|
||||
@ -483,30 +487,64 @@ export const functionResolvers = {
|
||||
if (item.wordCount) return item.wordCount
|
||||
return item.content ? wordsCount(item.content) : undefined
|
||||
},
|
||||
siteIcon(item: { siteIcon?: string }) {
|
||||
if (item.siteIcon && !isBase64Image(item.siteIcon)) {
|
||||
return createImageProxyUrl(item.siteIcon, 128, 128)
|
||||
}
|
||||
|
||||
return item.siteIcon
|
||||
},
|
||||
async highlights(
|
||||
item: {
|
||||
id: string
|
||||
highlights?: Highlight[]
|
||||
highlightAnnotations?: string[] | null
|
||||
},
|
||||
_: unknown,
|
||||
ctx: WithDataSourcesContext
|
||||
) {
|
||||
if (item.highlights) return item.highlights
|
||||
|
||||
if (item.highlightAnnotations && item.highlightAnnotations.length > 0) {
|
||||
const highlights = await findHighlightsByLibraryItemId(item.id, ctx.uid)
|
||||
return highlights.map(highlightDataToHighlight)
|
||||
}
|
||||
|
||||
return []
|
||||
},
|
||||
async labels(
|
||||
item: { id: string; labels?: Label[] },
|
||||
item: { id: string; labels?: Label[]; labelNames?: string[] | null },
|
||||
_: unknown,
|
||||
ctx: WithDataSourcesContext
|
||||
) {
|
||||
if (item.labels) return item.labels
|
||||
|
||||
return findLabelsByLibraryItemId(item.id, ctx.uid)
|
||||
if (item.labelNames && item.labelNames.length > 0) {
|
||||
return findLabelsByLibraryItemId(item.id, ctx.uid)
|
||||
}
|
||||
|
||||
return []
|
||||
},
|
||||
async recommendations(
|
||||
item: {
|
||||
id: string
|
||||
recommendations?: Recommendation[]
|
||||
recommenderNames?: string[] | null
|
||||
},
|
||||
_: unknown,
|
||||
ctx: WithDataSourcesContext
|
||||
) {
|
||||
if (item.recommendations) return item.recommendations
|
||||
|
||||
const recommendations = await findRecommendationsByLibraryItemId(
|
||||
item.id,
|
||||
ctx.uid
|
||||
)
|
||||
return recommendations.map(recommandationDataToRecommendation)
|
||||
if (item.recommenderNames && item.recommenderNames.length > 0) {
|
||||
const recommendations = await findRecommendationsByLibraryItemId(
|
||||
item.id,
|
||||
ctx.uid
|
||||
)
|
||||
return recommendations.map(recommandationDataToRecommendation)
|
||||
}
|
||||
|
||||
return []
|
||||
},
|
||||
},
|
||||
Subscription: {
|
||||
|
||||
@ -280,3 +280,7 @@ export const getGroupsWhereUserCanPost = async (
|
||||
.innerJoinAndSelect('members.user', 'user')
|
||||
.getMany()
|
||||
}
|
||||
|
||||
export const deleteGroup = async (groupId: string) => {
|
||||
return getRepository(Group).delete(groupId)
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import chaiString from 'chai-string'
|
||||
import 'mocha'
|
||||
import sinon from 'sinon'
|
||||
import { DeepPartial } from 'typeorm'
|
||||
import { Group } from '../../src/entity/groups/group'
|
||||
import { Highlight } from '../../src/entity/highlight'
|
||||
import { Label } from '../../src/entity/label'
|
||||
import { LibraryItem, LibraryItemState } from '../../src/entity/library_item'
|
||||
@ -18,6 +19,7 @@ import {
|
||||
UploadFileStatus,
|
||||
} from '../../src/generated/graphql'
|
||||
import { getRepository } from '../../src/repository'
|
||||
import { createGroup, deleteGroup } from '../../src/services/groups'
|
||||
import { createHighlight } from '../../src/services/highlights'
|
||||
import {
|
||||
createLabel,
|
||||
@ -153,6 +155,13 @@ const searchQuery = (keyword = '') => {
|
||||
highlights {
|
||||
id
|
||||
}
|
||||
labels {
|
||||
id
|
||||
name
|
||||
}
|
||||
recommendations {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
@ -1455,6 +1464,64 @@ describe('Article API', () => {
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
context('when recommendedBy:* is in the query', () => {
|
||||
let items: LibraryItem[] = []
|
||||
let group: Group
|
||||
|
||||
before(async () => {
|
||||
keyword = 'recommendedBy:*'
|
||||
|
||||
group = (
|
||||
await createGroup({
|
||||
admin: user,
|
||||
name: 'test group',
|
||||
})
|
||||
)[0]
|
||||
|
||||
// Create some test items
|
||||
items = await createLibraryItems(
|
||||
[
|
||||
{
|
||||
user,
|
||||
title: 'test title 1',
|
||||
readableContent: '<p>test 1</p>',
|
||||
slug: 'test slug 1',
|
||||
originalUrl: `${url}/test1`,
|
||||
recommendations: [
|
||||
{
|
||||
recommender: user,
|
||||
group,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
user,
|
||||
title: 'test title 2',
|
||||
readableContent: '<p>test 2</p>',
|
||||
slug: 'test slug 2',
|
||||
originalUrl: `${url}/test2`,
|
||||
},
|
||||
],
|
||||
user.id
|
||||
)
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await deleteLibraryItems(items, user.id)
|
||||
await deleteGroup(group.id)
|
||||
})
|
||||
|
||||
it('returns recommended items', async () => {
|
||||
const res = await graphqlRequest(query, authToken).expect(200)
|
||||
|
||||
expect(res.body.data.search.pageInfo.totalCount).to.eq(1)
|
||||
expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id)
|
||||
expect(
|
||||
res.body.data.search.edges[0].node.recommendations[0].name
|
||||
).to.eq(group.name)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('TypeaheadSearch API', () => {
|
||||
|
||||
Reference in New Issue
Block a user