Merge pull request #1819 from omnivore-app/feature/search-filter-with-no-as-keyword

This commit is contained in:
Hongbo Wu 2023-02-21 12:23:22 +08:00 committed by GitHub
commit 65c244edd7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 98 additions and 13 deletions

View file

@ -15,6 +15,7 @@ import {
InFilter,
LabelFilter,
LabelFilterType,
NoFilter,
ReadFilter,
SortBy,
SortOrder,
@ -114,16 +115,15 @@ const appendExcludeLabelFilter = (
body: SearchBody,
filters: LabelFilter[]
): void => {
const labels = filters.map((filter) => filter.labels).flat()
body.query.bool.must_not.push({
nested: {
path: 'labels',
query: filters.map((filter) => {
return {
terms: {
'labels.name': filter.labels,
},
}
}),
query: {
terms: {
'labels.name': labels,
},
},
},
})
}
@ -209,6 +209,21 @@ const appendRecommendedBy = (body: SearchBody, recommendedBy: string): void => {
})
}
const appendNoFilters = (body: SearchBody, noFilters: NoFilter[]): void => {
noFilters.forEach((filter) => {
body.query.bool.must_not.push({
nested: {
path: filter.field,
query: {
exists: {
field: filter.field,
},
},
},
})
})
}
export const createPage = async (
page: Page,
ctx: PageContext
@ -390,6 +405,7 @@ export const searchPages = async (
matchFilters,
ids,
includeContent,
noFilters,
} = args
// default order is descending
const sortOrder = sort?.order || SortOrder.DESCENDING
@ -485,6 +501,10 @@ export const searchPages = async (
})
}
if (noFilters) {
appendNoFilters(body, noFilters)
}
console.log('searching pages in elastic', JSON.stringify(body))
const response = await client.search<SearchResponse<Page>, SearchBody>({

View file

@ -7,6 +7,7 @@ import {
HasFilter,
InFilter,
LabelFilter,
NoFilter,
ReadFilter,
SortParams,
} from '../utils/search'
@ -103,7 +104,17 @@ export interface SearchBody {
terms: {
'labels.name': string[]
}
}[]
}
}
}
| {
nested: {
path: string
query: {
exists: {
field: string
}
}
}
}
)[]
@ -317,4 +328,5 @@ export interface PageSearchArgs {
ids?: string[]
recommendedBy?: string
includeContent?: boolean
noFilters?: NoFilter[]
}

View file

@ -36,6 +36,7 @@ export interface SearchFilter {
matchFilters: FieldFilter[]
ids: string[]
recommendedBy?: string
noFilters: NoFilter[]
}
export enum LabelFilterType {
@ -83,6 +84,10 @@ export interface FieldFilter {
value: string
}
export interface NoFilter {
field: string
}
const parseRecommendedBy = (str?: string): string | undefined => {
if (str === undefined) {
return undefined
@ -263,6 +268,22 @@ const parseIds = (field: string, str?: string): string[] | undefined => {
return str.split(',')
}
const parseNoFilter = (str?: string): NoFilter | undefined => {
if (str === undefined) {
return undefined
}
const strLower = str.toLowerCase()
const accepted = ['highlight', 'label']
if (accepted.includes(strLower)) {
return {
field: `${strLower}s`,
}
}
return undefined
}
export const parseSearchQuery = (query: string | undefined): SearchFilter => {
const searchQuery = query ? query.replace(/\W\s":/g, '') : undefined
const result: SearchFilter = {
@ -275,6 +296,7 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => {
termFilters: [],
matchFilters: [],
ids: [],
noFilters: [],
}
if (!searchQuery) {
@ -288,6 +310,7 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => {
termFilters: [],
matchFilters: [],
ids: [],
noFilters: [],
}
}
@ -310,6 +333,7 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => {
'updated',
'includes',
'recommendedBy',
'no',
],
tokenize: true,
})
@ -395,6 +419,11 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => {
result.recommendedBy = parseRecommendedBy(keyword.value)
break
}
case 'no': {
const noFilter = parseNoFilter(keyword.value)
noFilter && result.noFilters.push(noFilter)
break
}
}
}
}

View file

@ -810,7 +810,7 @@ describe('Article API', () => {
userId: user.id,
pageType: PageType.Article,
title: 'test title',
content: '<p>search page</p>',
content: '<p>test search api</p>',
slug: 'test slug',
createdAt: new Date(),
updatedAt: new Date(),
@ -849,7 +849,7 @@ describe('Article API', () => {
context('when we search for a keyword', () => {
before(() => {
keyword = 'search'
keyword = 'search api'
})
it('saves the term in search history', async () => {
@ -875,7 +875,7 @@ describe('Article API', () => {
context('when type:highlights is not in the query', () => {
before(() => {
keyword = 'search'
keyword = 'search api'
})
it('should return pages in descending order', async () => {
@ -901,7 +901,7 @@ describe('Article API', () => {
context('when type:highlights is in the query', () => {
before(() => {
keyword = 'search type:highlights'
keyword = "'search api' type:highlights"
})
it('should return highlights in descending order', async () => {
@ -918,7 +918,7 @@ describe('Article API', () => {
context('when is:unread is in the query', () => {
before(() => {
keyword = 'search is:unread'
keyword = "'search api' is:unread"
})
it('should return unread articles in descending order', async () => {
@ -932,6 +932,30 @@ describe('Article API', () => {
expect(res.body.data.search.edges[4].node.id).to.eq(pages[0].id)
})
})
context('when no:label is in the query', () => {
before(async () => {
keyword = "'search api' no:label"
})
it('returns non-labeled items in descending order', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(5)
})
})
context('when no:highlight is in the query', () => {
before(async () => {
keyword = "'search api' no:highlight"
})
it('returns non-highlighted items in descending order', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(0)
})
})
})
describe('TypeaheadSearch API', () => {