diff --git a/packages/api/src/elastic/pages.ts b/packages/api/src/elastic/pages.ts index 49d1bda5e..929737e19 100644 --- a/packages/api/src/elastic/pages.ts +++ b/packages/api/src/elastic/pages.ts @@ -1,13 +1,6 @@ -import { - ArticleSavingRequestStatus, - Page, - PageContext, - PageSearchArgs, - PageType, - ParamSet, - SearchBody, - SearchResponse, -} from './types' +import { ResponseError } from '@elastic/elasticsearch/lib/errors' +import { EntityType } from '../datalayer/pubsub' +import { BulkActionType } from '../generated/graphql' import { DateFilter, FieldFilter, @@ -21,9 +14,16 @@ import { SortOrder, } from '../utils/search' import { client, INDEX_ALIAS } from './index' -import { EntityType } from '../datalayer/pubsub' -import { ResponseError } from '@elastic/elasticsearch/lib/errors' -import { BulkActionType } from '../generated/graphql' +import { + ArticleSavingRequestStatus, + Page, + PageContext, + PageSearchArgs, + PageType, + ParamSet, + SearchBody, + SearchResponse, +} from './types' const appendQuery = (body: SearchBody, query: string): void => { body.query.bool.should.push({ @@ -224,6 +224,17 @@ const appendNoFilters = (body: SearchBody, noFilters: NoFilter[]): void => { }) } +const appendSiteNameFilter = (body: SearchBody, siteName: string): void => { + body.query.bool.should.push({ + multi_match: { + query: siteName, + fields: ['siteName', 'url'], + analyzer: 'simple', + }, + }) + body.query.bool.minimum_should_match = 1 +} + export const createPage = async ( page: Page, ctx: PageContext @@ -404,6 +415,7 @@ export const searchPages = async ( ids, includeContent, noFilters, + siteName, } = args // default order is descending const sortOrder = sort?.order || SortOrder.DESCENDING @@ -499,11 +511,10 @@ export const searchPages = async ( }) } - if (noFilters) { - appendNoFilters(body, noFilters) - } + noFilters && appendNoFilters(body, noFilters) + siteName && appendSiteNameFilter(body, siteName) - console.log('searching pages in elastic', JSON.stringify(body)) + console.debug('searching pages in elastic', JSON.stringify(body)) const response = await client.search, SearchBody>({ index: INDEX_ALIAS, @@ -523,6 +534,10 @@ export const searchPages = async ( response.body.hits.total.value, ] } catch (e) { + if (e instanceof ResponseError) { + console.error('failed to search pages in elastic', e.meta.body.error) + return undefined + } console.error('failed to search pages in elastic', e) return undefined } diff --git a/packages/api/src/elastic/types.ts b/packages/api/src/elastic/types.ts index 1ec528e54..70ee52beb 100644 --- a/packages/api/src/elastic/types.ts +++ b/packages/api/src/elastic/types.ts @@ -1,6 +1,6 @@ // Define the type of the body for the Search request -import { PickTuple } from '../util' import { PubsubClient } from '../datalayer/pubsub' +import { PickTuple } from '../util' import { DateFilter, FieldFilter, @@ -80,13 +80,15 @@ export interface SearchBody { multi_match: { query: string fields: string[] - operator: 'and' | 'or' - type: + operator?: 'and' | 'or' + type?: | 'best_fields' | 'most_fields' | 'cross_fields' | 'phrase' | 'phrase_prefix' + | 'bool_prefix' + analyzer?: string } }[] minimum_should_match?: number @@ -331,4 +333,5 @@ export interface PageSearchArgs { recommendedBy?: string includeContent?: boolean noFilters?: NoFilter[] + siteName?: string } diff --git a/packages/api/src/utils/search.ts b/packages/api/src/utils/search.ts index 15fd1f92c..9b87f7f72 100644 --- a/packages/api/src/utils/search.ts +++ b/packages/api/src/utils/search.ts @@ -37,6 +37,7 @@ export interface SearchFilter { ids: string[] recommendedBy?: string noFilters: NoFilter[] + siteName?: string } export enum LabelFilterType { @@ -335,6 +336,7 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => { 'recommendedBy', 'no', 'mode', + 'site', ], tokenize: true, }) @@ -428,6 +430,9 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => { case 'mode': // mode is ignored and used only by the frontend break + case 'site': + result.siteName = keyword.value + break } } } diff --git a/packages/api/test/resolvers/article.test.ts b/packages/api/test/resolvers/article.test.ts index 30ee73e67..3ab9829cd 100644 --- a/packages/api/test/resolvers/article.test.ts +++ b/packages/api/test/resolvers/article.test.ts @@ -1,31 +1,10 @@ -import { createTestUser, deleteTestUser } from '../db' -import { - createTestElasticPage, - generateFakeUuid, - graphqlRequest, - request, -} from '../util' import * as chai from 'chai' import { expect } from 'chai' -import 'mocha' -import { User } from '../../src/entity/user' import chaiString from 'chai-string' -import { - BulkActionType, - SyncUpdatedItemEdge, - UpdateReason, - UploadFileStatus, -} from '../../src/generated/graphql' -import { - ArticleSavingRequestStatus, - Highlight, - Page, - PageContext, - PageType, -} from '../../src/elastic/types' -import { UploadFile } from '../../src/entity/upload_file' +import 'mocha' import { createPubSubClient } from '../../src/datalayer/pubsub' -import { getRepository } from '../../src/entity/utils' +import { refreshIndex } from '../../src/elastic' +import { addHighlightToPage } from '../../src/elastic/highlights' import { createPage, deletePage, @@ -33,9 +12,30 @@ import { getPageById, updatePage, } from '../../src/elastic/pages' -import { addHighlightToPage } from '../../src/elastic/highlights' -import { refreshIndex } from '../../src/elastic' +import { + ArticleSavingRequestStatus, + Highlight, + Page, + PageContext, + PageType, +} from '../../src/elastic/types' import { SearchHistory } from '../../src/entity/search_history' +import { UploadFile } from '../../src/entity/upload_file' +import { User } from '../../src/entity/user' +import { getRepository } from '../../src/entity/utils' +import { + BulkActionType, + SyncUpdatedItemEdge, + UpdateReason, + UploadFileStatus, +} from '../../src/generated/graphql' +import { createTestUser, deleteTestUser } from '../db' +import { + createTestElasticPage, + generateFakeUuid, + graphqlRequest, + request, +} from '../util' chai.use(chaiString) @@ -847,6 +847,7 @@ describe('Article API', () => { url: url, savedAt: new Date(), state: ArticleSavingRequestStatus.Succeeded, + siteName: 'Example', } page.id = (await createPage(page, ctx))! pages.push(page) @@ -984,6 +985,18 @@ describe('Article API', () => { expect(res.body.data.search.pageInfo.totalCount).to.eq(0) }) }) + + context('when site:${site_name} is in the query', () => { + before(async () => { + keyword = "'search api' site:example" + }) + + it('returns items from the site', async () => { + const res = await graphqlRequest(query, authToken).expect(200) + + expect(res.body.data.search.pageInfo.totalCount).to.eq(5) + }) + }) }) describe('TypeaheadSearch API', () => {