Skip to content

Commit

Permalink
feat: Add highlight support for matched words (#880)
Browse files Browse the repository at this point in the history
Setting highlightedFields in Searchkit config plus a customized ResultHit
graphql type and custom resolver one can request ElasticSearch to generate
highlights for matched words and expose it via graphql to the searchkit
clients.

Resolvers for custom ResultHit fields will receive the highlights in the
"highlight" property, but also added a "rawHit" field containing the hit
record as received from ES, which may come handy in other use cases.
  • Loading branch information
beepsoft authored May 22, 2021
1 parent 422a107 commit a7b971e
Show file tree
Hide file tree
Showing 10 changed files with 815 additions and 11 deletions.
2 changes: 1 addition & 1 deletion packages/searchkit-cli/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ export const getSKQuickStartText = ({
mapping
}) => {
const mappingCall = {
properties: mapping
properties: mapping
}

return `
Expand Down
76 changes: 75 additions & 1 deletion packages/searchkit-schema/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,78 @@ Will provide a GraphQL API where you can perform queries like:
}
```

See [Schema Query Guide](https://searchkit.co/docs/guides/graphql-schema-queries-cheatsheet) for more examples.
See [Schema Query Guide](https://searchkit.co/docs/guides/graphql-schema-queries-cheatsheet) for more examples.

### Getting highlights for matches

To receive ElasticSearch highlights for your matches you need to do the followings:

1. Add `highlightedFields` under `hits` for your Searchkit config. Here you may list the name of fields for which you want to get highlights, or an object where you specify the field name and the highlight configuration according to the [ElasticSearch highlighting](https://www.elastic.co/guide/en/elasticsearch/reference/current/highlighting.html) documentation
2. Add a `highlight` field to your `ResultHit` graphql schema type
3. When configuring ApolloServer specify a custom resolver for `highlight`


The following example generates a JSON encoded string version of the highlight objects received from ElasticSearch. You may specify another shape for `highlight` in your `ResultHit` type and a matching transformation of `hit.highlight` in your resolver.

```js
const searchkitConfig = {
host: 'http://localhost:9200/', // elasticsearch instance url
index: 'movies',
hits: {
fields: [ 'title', 'plot', 'poster' ],
highlightedFields: [
'title',
{
field: 'plot',
config: {
pre_tags: ['<b>'],
post_tags: ['</b>']
}
}
]
},
query: new MultiMatchQuery({
fields: [ 'plot','title^4']
}),
facets: [
...
]
}

const { typeDefs, withSearchkitResolvers, context } = SearchkitSchema({
config: searchkitConfig,
typeName: 'ResultSet',
hitTypeName: 'ResultHit',
addToQueryType: true
})

const server = new ApolloServer({
typeDefs: [
gql`
type Query {
root: String
}
type HitFields {
title: String
}
type ResultHit implements SKHit {
id: ID!
fields: HitFields
highlight: String
}
`, ...typeDefs
],
resolvers: withSearchkitResolvers({
ResultHit: {
highlight: (hit: any) => JSON.stringify(hit.highlight)
}
}),
introspection: true,
playground: true,
context: {
...context
}
})
```
13 changes: 13 additions & 0 deletions packages/searchkit-schema/src/core/SearchkitRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,26 @@ export default class SearchkitRequest {
const combinedFilterConfigs = [...(this.config.facets || []), ...(this.config.filters || [])]
const postFilter = filterTransform(this.queryManager, combinedFilterConfigs)

let highlight
this.config.hits.highlightedFields?.forEach((field) => {
if (!highlight) {
highlight = { fields: {} }
}
if (typeof field == 'string') {
highlight.fields[field] = {}
} else {
highlight.fields[field.field] = field.config
}
})

const baseQuery = { size: 0 }

return mergeESQueries(
[
baseQuery,
query && { query },
postFilter && { post_filter: postFilter },
highlight && { highlight },
...(partialQueries as any[])
].filter(Boolean)
)
Expand Down
4 changes: 3 additions & 1 deletion packages/searchkit-schema/src/resolvers/HitsResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ export default async (parent, parameters: HitsParameters, ctx) => {
items: hits.hits.map((hit) => ({
id: hit._id,
fields: hit._source,
type: parent.searchkit.hitType
type: parent.searchkit.hitType,
highlight: hit.highlight,
rawHit: hit
})),
page: {
total: hitsTotal,
Expand Down
6 changes: 6 additions & 0 deletions packages/searchkit-schema/src/resolvers/ResultsResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,18 @@ export interface SortingOption {
defaultOption?: boolean
}

export interface CustomHighlightConfig {
field: string
config: any
}

export interface SearchkitConfig {
host: string
index: string
sortOptions?: SortingOption[]
hits: {
fields: string[]
highlightedFields?: (string | CustomHighlightConfig)[]
}
query?: BaseQuery
facets?: Array<BaseFacet>
Expand Down
114 changes: 113 additions & 1 deletion packages/searchkit-schema/tests/HitsResolver.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import nock from 'nock'
import { gql } from 'apollo-server-micro'
import { SearchkitConfig } from '../src/resolvers/ResultsResolver'
import { MultiMatchQuery } from '../src'
import { setupTestServer, callQuery } from './support/helper'
import HitsMock from './__mock-data__/HitResolver/Hits.json'
import HighlightHitsMock from './__mock-data__/HitResolver/HighlightHits.json'

describe('Hits Resolver', () => {
describe('should return as expected', () => {
const runQuery = async (query = '', page = { size: 10, from: 0 }, sorting?: string) => {
const runQuery = async (
query = '',
page = { size: 10, from: 0 },
sorting?: string,
includeHighlight?: boolean
) => {
const gql = `
{
results(query: "${query}") {
Expand All @@ -21,6 +28,7 @@ describe('Hits Resolver', () => {
writers
actors
}
${includeHighlight ? 'highlight' : ''}
}
}
}
Expand Down Expand Up @@ -207,5 +215,109 @@ describe('Hits Resolver', () => {

expect(response.status).toEqual(200)
})

it('should return correct highlight Results', async () => {
const config: SearchkitConfig = {
host: 'http://localhost:9200',
index: 'movies',
hits: {
fields: ['actors', 'writers'],
highlightedFields: [
'actors',
{ field: 'writers', config: { pre_tags: ['<b>'], post_tags: ['</b>'] } }
]
},
query: new MultiMatchQuery({ fields: ['actors', 'writers', 'title^4', 'plot'] })
}

setupTestServer(
{
config,
addToQueryType: true,
typeName: 'ResultSet',
hitTypeName: 'ResultHit'
},
gql`
type Query {
root: String
}
type Mutation {
root: String
}
type HitFields {
title: String
writers: [String]
actors: [String]
plot: String
poster: String
}
type ResultHit implements SKHit {
id: ID!
fields: HitFields
highlight: String
}
`,
{
ResultHit: {
highlight: (hit: any) => JSON.stringify(hit.highlight)
}
}
)

const scope = nock('http://localhost:9200')
.post('/movies/_search')
.reply((uri, body) => {
expect(body).toMatchInlineSnapshot(`
Object {
"aggs": Object {},
"from": 0,
"highlight": Object {
"fields": Object {
"actors": Object {},
"writers": Object {
"post_tags": Array [
"</b>",
],
"pre_tags": Array [
"<b>",
],
},
},
},
"query": Object {
"bool": Object {
"must": Array [
Object {
"multi_match": Object {
"fields": Array [
"actors",
"writers",
"title^4",
"plot",
],
"query": "al",
},
},
],
},
},
"size": 10,
"sort": Array [
Object {
"_score": "desc",
},
],
}
`)
return [200, HighlightHitsMock]
})

const response = await runQuery('al', { size: 10, from: 0 }, undefined, true)
expect(response.body.data).toMatchSnapshot()
expect(response.status).toEqual(200)
})
})
})
Loading

1 comment on commit a7b971e

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deploy preview for searchkit-docs ready!

✅ Preview
https://searchkit-docs-68tp65baw-joemcelroy.vercel.app

Built with commit a7b971e.
This pull request is being automatically deployed with vercel-action

Please sign in to comment.