DSL initial commit

This commit is contained in:
delvedor
2020-09-02 16:26:52 +02:00
parent 3621e32dac
commit 6eff70a47d
17 changed files with 1894 additions and 0 deletions

17
dsl/examples/README.md Normal file
View File

@ -0,0 +1,17 @@
# Examples
In this folder you will find different examples to show the usage of the DSL.
## Instructions
Befoire to run any of the examples in this folder you should run `npm install` for installing all the required dependenices and the run the `loadRepo` script.
## Run an example
Running an example is very easy, you just need to run the following command:
```sh
npm run example examples/<filename>
```
For example:
```sh
npm run example examples/last-commits.ts
```

View File

@ -0,0 +1,39 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Client } from '../../'
import { Q } from '../'
async function run () {
const client = new Client({ node: 'http://localhost:9200' })
// define the query clauses
const fixDescription = Q.must(Q.match('description', 'fix'))
const files = Q.should(Q.term('files', 'test'), Q.term('files', 'docs'))
const author = Q.filter(Q.term('author.name', Q.param('author')))
const { body } = await client.search({
index: 'git',
// use the boolean utilities to craft the final query
body: Q.and(fixDescription, files, author)
})
console.log(body.hits.hits)
}
run().catch(console.log)

View File

@ -0,0 +1,73 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Client } from '../../'
import { Q } from '../'
// You can compile a query if you need to get
// the best performances out of your code.
// The query crafting and compilation should be done
// outside of your hot code path.
// First of all yu should create your query almost
// in the same way as you were doing before, the only
// difference, is that all the paramegers you are passing
// now should be updated with the `Q.param` API.
// The only parameter or `Q.param`, is the name of the parameter
// that you were passing before.
const query = Q(
Q.match('description', Q.param('description')),
Q.filter(
Q.term('author.name', Q.param('author'))
),
Q.size(10)
)
// Afterwards, you can create an interface that represents
// the input object of the compiled query. The input object
// contains all the parameters you were passing before, the
// keys are the same you have passed to the various `Q.param`
// invocations before. It defaults to `unknown`.
interface Input {
description: string
author: string
}
// Once you have created the query and the input interface,
// you must pass the query to `Q.compile` and store the result
// in a variable. `Q.compile` returns a function that accepts
// a single object parameter, which is the same you have declared
// in the interface before.
const compiledQuery = Q.compile<Input>(query)
async function run () {
const client = new Client({ node: 'http://localhost:9200' })
const { body } = await client.search({
index: 'git',
// Finally, you call the function inside your hot code path,
// the returned value will be the query.
body: compiledQuery({
description: 'fix',
author: 'delvedor'
})
})
console.log(body.hits.hits)
}
run().catch(console.log)

View File

@ -0,0 +1,44 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Client } from '../../'
import { Q, A } from '../'
async function run () {
const client = new Client({ node: 'http://localhost:9200' })
// get the day where the most commits were made
const { body } = await client.search({
index: 'git',
body: Q(
Q.size(0),
// 'day_most_commits' is the name of the aggregation
A(A.day_most_commits.dateHistogram({
field: 'committed_date',
interval: 'day',
min_doc_count: 1,
order: { _count: 'desc' }
}))
)
})
console.log(body.aggregations)
}
run().catch(console.log)

View File

@ -0,0 +1,47 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Client } from '../../'
import { Q, A } from '../'
async function run () {
const client = new Client({ node: 'http://localhost:9200' })
// 'committers' is the name of the aggregation
let committersAgg = A.committers.terms('committer.name.keyword')
// instead of pass other aggregations as parameter
// to the parent aggregation, you can conditionally add them
if (Math.random() >= 0.5) {
committersAgg = A.committers.aggs(
committersAgg, A.line_stats.stats('stat.insertions')
)
}
const { body } = await client.search({
index: 'git',
body: Q(
Q.size(0),
A(committersAgg)
)
})
console.log(body.aggregations)
}
run().catch(console.log)

View File

@ -0,0 +1,66 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Client } from '../../'
import { Q } from '../'
async function run () {
const client = new Client({ node: 'http://localhost:9200' })
// the result must be fixes done by delvedor
let query = Q.bool(
Q.must(Q.match('description', 'fix')),
Q.filter(Q.term('author.name', 'delvedor'))
)
// Based on a condition, we want to enrich our query
if (Math.random() >= 0.5) {
// the results must be fixes done by delvedor
// on test or do files
const should = Q.should(
Q.term('files', 'test'),
Q.term('files', 'docs')
)
// The code below produces the same as the one above
// If you need to check multiple values for the same key,
// you can pass an array of strings instead of calling
// the query function multiple times
// ```
// const should = Q.should(
// Q.term('files', ['test', 'docs'])
// )
// ```
query = Q.and(query, should)
} else {
// the results must be fixes or features done by delvedor
const must = Q.must(
Q.match('description', 'feature')
)
query = Q.or(query, must)
}
const { body } = await client.search({
index: 'git',
body: query
})
console.log(body.hits.hits)
}
run().catch(console.log)

View File

@ -0,0 +1,40 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Client } from '../../'
import { Q } from '../'
async function run () {
const client = new Client({ node: 'http://localhost:9200' })
// search commits that contains 'fix' but do not changes test files
const { body } = await client.search({
index: 'git',
body: Q.bool(
// You can avoid to call `Q.must`, as any query will be
// sent inside a `must` block unless specified otherwise
Q.must(Q.match('description', 'fix')),
Q.mustNot(Q.term('files', 'test'))
)
})
console.log(body.hits.hits)
}
run().catch(console.log)

View File

@ -0,0 +1,39 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Client } from '../../'
import { Q } from '../'
async function run () {
const client = new Client({ node: 'http://localhost:9200' })
// last 10 commits for 'elasticsearch-js' repo
const { body } = await client.search({
index: 'git',
body: Q(
Q.term('repository', 'elasticsearch-js'),
Q.sort('committed_date', { order: 'desc' }),
Q.size(10)
)
})
console.log(body.hits.hits)
}
run().catch(console.log)

159
dsl/examples/loadRepo.js Normal file
View File

@ -0,0 +1,159 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
'use strict'
const minimist = require('minimist')
const Git = require('simple-git/promise')
const { Client } = require('@elastic/elasticsearch')
start(minimist(process.argv.slice(2), {
string: ['elasticsearch', 'index', 'repository'],
default: {
elasticsearch: 'http://localhost:9200',
index: 'git',
repository: 'elasticsearch-js'
}
}))
async function start ({ elasticsearch, index, repository }) {
const client = new Client({ node: elasticsearch })
await createIndex({ client, index })
await loadHistory({ client, index, repository })
}
async function createIndex ({ client, index }) {
const userMapping = {
properties: {
name: {
type: 'text',
fields: {
keyword: { type: 'keyword' }
}
}
}
}
await client.indices.create({
index,
body: {
settings: {
// just one shard, no replicas for testing
number_of_shards: 1,
number_of_replicas: 0,
// custom analyzer for analyzing file paths
analysis: {
analyzer: {
file_path: {
type: 'custom',
tokenizer: 'path_hierarchy',
filter: ['lowercase']
}
}
}
},
mappings: {
properties: {
repository: { type: 'keyword' },
sha: { type: 'keyword' },
author: userMapping,
authored_date: { type: 'date' },
committer: userMapping,
committed_date: { type: 'date' },
parent_shas: { type: 'keyword' },
description: { type: 'text', analyzer: 'snowball' },
files: { type: 'text', analyzer: 'file_path', fielddata: true }
}
}
}
})
}
async function loadHistory ({ client, index, repository }) {
const git = Git(process.cwd())
// Get the result of 'git log'
const { all: history } = await git.log({
format: {
hash: '%H',
parentHashes: '%P',
authorName: '%an',
authorEmail: '%ae',
authorDate: '%ai',
committerName: '%cn',
committerEmail: '%ce',
committerDate: '%cd',
subject: '%s'
}
})
// Get the stats for every commit
for (var i = 0; i < history.length; i++) {
const commit = history[i]
const stat = await git.show(['--numstat', '--oneline', commit.hash])
commit.files = []
commit.stat = stat
.split('\n')
.slice(1)
.filter(Boolean)
.reduce((acc, val, index) => {
const [insertions, deletions, file] = val.split('\t')
commit.files.push(file)
acc.files++
acc.insertions += Number(insertions)
acc.deletions += Number(deletions)
return acc
}, { insertions: 0, deletions: 0, files: 0 })
}
// Index the data, 500 commits at a time
var count = 0
var chunk = history.slice(count, count + 500)
while (chunk.length > 0) {
const { body } = await client.bulk({
body: chunk.reduce((body, commit) => {
body.push({ index: { _index: index, _id: commit.hash } })
body.push({
repository,
sha: commit.hash,
author: {
name: commit.authorName,
email: commit.authorEmail
},
authored_date: new Date(commit.authorDate).toISOString(),
committer: {
name: commit.committerName,
email: commit.committerEmail
},
committed_date: new Date(commit.committerDate).toISOString(),
parent_shas: commit.parentHashes,
description: commit.subject,
files: commit.files,
stat: commit.stat
})
return body
}, [])
})
if (body.errors) {
console.log(JSON.stringify(body.items[0], null, 2))
process.exit(1)
}
count += 500
chunk = history.slice(count, count + 500)
}
}

View File

@ -0,0 +1,47 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Client } from '../../'
import { Q, A } from '../'
async function run () {
const client = new Client({ node: 'http://localhost:9200' })
// top committers aggregation
// 'committers' is the name of the aggregation
const committersAgg = A.committers.terms(
{ field: 'committer.name.keyword' },
// you can nest multiple aggregations by
// passing them to the aggregation constructor
// 'line_stats' is the name of the aggregation
A.line_stats.stats({ field: 'stat.insertions' })
)
const { body } = await client.search({
index: 'git',
body: Q(
Q.matchAll(),
Q.size(0),
A(committersAgg)
)
})
console.log(body.aggregations)
}
run().catch(console.log)

61
dsl/examples/top-month.ts Normal file
View File

@ -0,0 +1,61 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Client } from '../../'
import { Q, A } from '../'
async function run () {
const client = new Client({ node: 'http://localhost:9200' })
const committers = A.committers.terms(
{ field: 'committer.name.keyword' },
A.insertions.sum({ field: 'stat.insertions' })
)
const topCommittersPerMonth = A.top_committer_per_month.maxBucket(
{ bucket_path: 'committers>insertions' }
)
const commitsPerMonth = A.commits_per_month.dateHistogram(
{
field: 'committed_date',
interval: 'day',
min_doc_count: 1,
order: { _count: 'desc' }
},
// nested aggregations
committers,
topCommittersPerMonth
)
const topCommittersPerMonthGlobal = A.top_committer_per_month.maxBucket(
{ bucket_path: 'commits_per_month>top_committer_per_month' }
)
const { body: topMonths } = await client.search({
index: 'git',
body: Q(
// we want to know the top month for 'delvedor'
Q.filter(Q.term('author', 'delvedor')),
Q.size(0),
A(commitsPerMonth, topCommittersPerMonthGlobal)
)
})
console.log(topMonths)
}
run().catch(console.log)

23
dsl/index.d.ts vendored Normal file
View File

@ -0,0 +1,23 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import Q from './lib/query-helpers'
import A from './lib/aggregation-helpers'
export { Q, A }

25
dsl/index.js Normal file
View File

@ -0,0 +1,25 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
'use strict'
const Q = require('./lib/query-helpers').default
const A = require('./lib/aggregation-helpers').default
module.exports = { Q, A }

View File

@ -0,0 +1,385 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* eslint camelcase: 0 */
/* eslint no-undef: 0 */
/* eslint no-use-before-define: 0 */
/* eslint no-redeclare: 0 */
import * as t from './types'
interface anyObject {
[key: string]: any
}
type aggsOptions = anyObject | string
function _A (...aggregations: any[]): any {
return {
aggs: Object.assign.apply(null, aggregations.filter(falsy))
}
}
interface Aggregations {
(...aggregations: any[]): any
[name: string]: {
// add aggregations to a parent aggregation
aggs(...aggregations: any[]): t.Aggregation
// Metric aggregations
avg(opts: aggsOptions): t.Aggregation
weightedAvg(opts: aggsOptions): t.Aggregation
cardinality(opts: aggsOptions): t.Aggregation
extendedStats(opts: aggsOptions): t.Aggregation
geoBounds(opts: aggsOptions): t.Aggregation
geoCentroid(opts: aggsOptions): t.Aggregation
max(opts: aggsOptions): t.Aggregation
min(opts: aggsOptions): t.Aggregation
percentiles(opts: aggsOptions): t.Aggregation
percentileRanks(opts: aggsOptions): t.Aggregation
scriptedMetric(opts: aggsOptions): t.Aggregation
stats(opts: aggsOptions): t.Aggregation
sum(opts: aggsOptions): t.Aggregation
topHits(opts: aggsOptions): t.Aggregation
valueCount(opts: aggsOptions): t.Aggregation
medianAbsoluteDeviation(opts: aggsOptions): t.Aggregation
// Buckets aggregations
adjacencyMatrix(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
autoDateHistogram(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
children(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
composite(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
dateHistogram(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
dateRange(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
diversifiedSampler(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
filter(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
filters(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
geoDistance(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
geohashGrid(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
geotileGrid(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
global(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
histogram(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
ipRange(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
missing(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
nested(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
parent(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
range(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
reverseNested(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
sampler(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
significantTerms(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
significantText(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
terms(opts: aggsOptions, ...aggregations: any[]): t.Aggregation
// Pipeline aggregations
avgBucket (opts: aggsOptions): t.Aggregation
derivative (opts: aggsOptions): t.Aggregation
maxBucket (opts: aggsOptions): t.Aggregation
minBucket (opts: aggsOptions): t.Aggregation
sumBucket (opts: aggsOptions): t.Aggregation
statsBucket (opts: aggsOptions): t.Aggregation
extendedStatsBucket (opts: aggsOptions): t.Aggregation
percentilesBucket (opts: aggsOptions): t.Aggregation
movingAvg (opts: aggsOptions): t.Aggregation
movingFn (opts: aggsOptions): t.Aggregation
cumulativeSum (opts: aggsOptions): t.Aggregation
bucketScript (opts: aggsOptions): t.Aggregation
bucketSelector (opts: aggsOptions): t.Aggregation
bucketSort (opts: aggsOptions): t.Aggregation
serialDiff (opts: aggsOptions): t.Aggregation
// Matrix aggregations
matrixStats (opts: aggsOptions): t.Aggregation
}
}
const aggregations = {
get: function (target: unknown, name: string) {
return {
// add aggregations to a parent aggregation
aggs (...aggregations: any[]): t.Aggregation {
return updateAggsObject(name, aggregations)
},
// Metric aggregations
avg (opts: aggsOptions): t.Aggregation {
return generateAggsObject('avg', name, isString(opts) ? 'field' : null, opts)
},
weightedAvg (opts: aggsOptions): t.Aggregation {
return generateAggsObject('weighted_avg', name, null, opts)
},
cardinality (opts: aggsOptions): t.Aggregation {
return generateAggsObject('cardinality', name, isString(opts) ? 'field' : null, opts)
},
extendedStats (opts: aggsOptions): t.Aggregation {
return generateAggsObject('extended_stats', name, isString(opts) ? 'field' : null, opts)
},
geoBounds (opts: aggsOptions): t.Aggregation {
return generateAggsObject('geo_bounds', name, isString(opts) ? 'field' : null, opts)
},
geoCentroid (opts: aggsOptions): t.Aggregation {
return generateAggsObject('geo_centroid', name, isString(opts) ? 'field' : null, opts)
},
max (opts: aggsOptions): t.Aggregation {
return generateAggsObject('max', name, isString(opts) ? 'field' : null, opts)
},
min (opts: aggsOptions): t.Aggregation {
return generateAggsObject('min', name, isString(opts) ? 'field' : null, opts)
},
percentiles (opts: aggsOptions): t.Aggregation {
return generateAggsObject('percentiles', name, isString(opts) ? 'field' : null, opts)
},
percentileRanks (opts: aggsOptions): t.Aggregation {
return generateAggsObject('percentile_ranks', name, null, opts)
},
scriptedMetric (opts: aggsOptions): t.Aggregation {
return generateAggsObject('scripted_metric', name, null, opts)
},
stats (opts: aggsOptions): t.Aggregation {
return generateAggsObject('stats', name, isString(opts) ? 'field' : null, opts)
},
sum (opts: aggsOptions): t.Aggregation {
return generateAggsObject('sum', name, isString(opts) ? 'field' : null, opts)
},
topHits (opts: aggsOptions): t.Aggregation {
return generateAggsObject('top_hits', name, null, opts)
},
valueCount (opts: aggsOptions): t.Aggregation {
return generateAggsObject('value_count', name, isString(opts) ? 'field' : null, opts)
},
medianAbsoluteDeviation (opts: aggsOptions): t.Aggregation {
return generateAggsObject('median_absolute_deviation', name, isString(opts) ? 'field' : null, opts)
},
// Buckets aggregations
adjacencyMatrix (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('adjacency_matrix', name, null, opts, aggregations)
},
autoDateHistogram (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('auto_date_histogram', name, isString(opts) ? 'field' : null, opts, aggregations)
},
children (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('children', name, isString(opts) ? 'type' : null, opts, aggregations)
},
composite (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('composite', name, null, opts, aggregations)
},
dateHistogram (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('date_histogram', name, null, opts, aggregations)
},
dateRange (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('date_range', name, null, opts, aggregations)
},
diversifiedSampler (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('diversified_sampler', name, isString(opts) ? 'field' : null, opts, aggregations)
},
filter (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('filter', name, null, opts, aggregations)
},
filters (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('filters', name, null, opts, aggregations)
},
geoDistance (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('geo_distance', name, null, opts, aggregations)
},
geohashGrid (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('geohash_grid', name, isString(opts) ? 'field' : null, opts, aggregations)
},
geotileGrid (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('geotile_grid', name, isString(opts) ? 'field' : null, opts, aggregations)
},
global (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('global', name, null, opts, aggregations)
},
histogram (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('histogram', name, null, opts, aggregations)
},
ipRange (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('ip_range', name, null, opts, aggregations)
},
missing (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('missing', name, isString(opts) ? 'field' : null, opts, aggregations)
},
nested (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('nested', name, isString(opts) ? 'path' : null, opts, aggregations)
},
parent (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('parent', name, isString(opts) ? 'type' : null, opts, aggregations)
},
range (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('range', name, null, opts, aggregations)
},
reverseNested (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('reverse_nested', name, null, opts, aggregations)
},
sampler (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('sampler', name, null, opts, aggregations)
},
significantTerms (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('significant_terms', name, isString(opts) ? 'field' : null, opts, aggregations)
},
significantText (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('significant_text', name, isString(opts) ? 'field' : null, opts, aggregations)
},
terms (opts: aggsOptions, ...aggregations: any[]): t.Aggregation {
return generateAggsObject('terms', name, isString(opts) ? 'field' : null, opts, aggregations)
},
// Pipeline aggregations
avgBucket (opts: aggsOptions): t.Aggregation {
return generateAggsObject('avg_bucket', name, isString(opts) ? 'buckets_path' : null, opts)
},
derivative (opts: aggsOptions): t.Aggregation {
return generateAggsObject('derivative', name, isString(opts) ? 'buckets_path' : null, opts)
},
maxBucket (opts: aggsOptions): t.Aggregation {
return generateAggsObject('max_bucket', name, isString(opts) ? 'buckets_path' : null, opts)
},
minBucket (opts: aggsOptions): t.Aggregation {
return generateAggsObject('min_bucket', name, isString(opts) ? 'buckets_path' : null, opts)
},
sumBucket (opts: aggsOptions): t.Aggregation {
return generateAggsObject('sum_bucket', name, isString(opts) ? 'buckets_path' : null, opts)
},
statsBucket (opts: aggsOptions): t.Aggregation {
return generateAggsObject('stats_bucket', name, isString(opts) ? 'buckets_path' : null, opts)
},
extendedStatsBucket (opts: aggsOptions): t.Aggregation {
return generateAggsObject('extended_stats_bucket', name, isString(opts) ? 'buckets_path' : null, opts)
},
percentilesBucket (opts: aggsOptions): t.Aggregation {
return generateAggsObject('percentiles_bucket', name, isString(opts) ? 'buckets_path' : null, opts)
},
movingAvg (opts: aggsOptions): t.Aggregation {
return generateAggsObject('moving_avg', name, isString(opts) ? 'buckets_path' : null, opts)
},
movingFn (opts: aggsOptions): t.Aggregation {
return generateAggsObject('moving_fn', name, null, opts)
},
cumulativeSum (opts: aggsOptions): t.Aggregation {
return generateAggsObject('cumulative_sum', name, isString(opts) ? 'buckets_path' : null, opts)
},
bucketScript (opts: aggsOptions): t.Aggregation {
return generateAggsObject('bucket_script', name, null, opts)
},
bucketSelector (opts: aggsOptions): t.Aggregation {
return generateAggsObject('bucket_selector', name, null, opts)
},
bucketSort (opts: aggsOptions): t.Aggregation {
return generateAggsObject('bucket_sort', name, null, opts)
},
serialDiff (opts: aggsOptions): t.Aggregation {
return generateAggsObject('serial_diff', name, isString(opts) ? 'buckets_path' : null, opts)
},
// Matrix aggregations
matrixStats (opts: aggsOptions): t.Aggregation {
return generateAggsObject('matrix_stats', name, isString(opts) ? 'fields' : null, opts)
}
}
}
}
const A = new Proxy(_A, aggregations) as Aggregations
function generateAggsObject (type: string, name: string, defaultField: string | null, opts: any = {}, aggregations: any[] = []): t.Aggregation {
if (typeof opts === 'string' && typeof defaultField === 'string') {
opts = { [defaultField]: opts }
} else if (typeof opts === 'string' && defaultField === null) {
throw new Error('This method does not support shorthand options')
}
if (aggregations.length > 0) {
return {
[name]: {
[type]: opts,
aggs: Object.assign.apply(null, aggregations.filter(falsy))
}
}
} else {
return {
[name]: {
[type]: opts
}
}
}
}
function updateAggsObject (name: string, aggregations: any[]): t.Aggregation {
const [main, ...others] = aggregations.filter(falsy)
main[name].aggs = Object.assign(main[name].aggs || {}, ...others)
return main
}
function falsy (val: any): boolean {
return !!val
}
function isString (val: any): val is string {
return typeof val === 'string'
}
export default A

726
dsl/src/query-helpers.ts Normal file
View File

@ -0,0 +1,726 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* eslint camelcase: 0 */
/* eslint no-undef: 0 */
/* eslint no-use-before-define: 0 */
/* eslint no-redeclare: 0 */
import deepMerge from 'deepmerge'
import * as t from './types'
function Q (...blocks: t.AnyQuery[]): Record<string, any> {
const { aggs,
collapse,
explain,
from,
highlight,
indices_boost,
min_score,
post_filter,
profile,
rescore,
script_fields,
search_after,
size,
slice,
sort,
_source,
suggest,
terminate_after,
timeout,
track_scores,
version,
...queries
} = Object.assign.apply({}, blocks.flat())
const query: t.AnyQuery[] = Object.keys(queries).map(q => ({ [q]: queries[q] }))
const body: Record<string, any> = query.length > 0 ? Q.bool(...query) : {}
if (aggs) body.aggs = aggs
if (collapse) body.collapse = collapse
if (explain) body.explain = explain
if (from) body.from = from
if (highlight) body.highlight = highlight
if (indices_boost) body.indices_boost = indices_boost
if (min_score) body.min_score = min_score
if (post_filter) body.post_filter = post_filter
if (profile) body.profile = profile
if (rescore) body.rescore = rescore
if (script_fields) body.script_fields = script_fields
if (search_after) body.search_after = search_after
if (size) body.size = size
if (slice) body.slice = slice
if (sort) body.sort = sort
if (_source) body._source = _source
if (suggest) body.suggest = suggest
if (terminate_after) body.terminate_after = terminate_after
if (timeout) body.timeout = timeout
if (track_scores) body.track_scores = track_scores
if (version) body.version = version
return body
}
Object.defineProperty(Q, 'name', { writable: true })
Q.param = function param (key: string) {
return `###${key}###`
}
Q.compile = function compile<TInput = unknown> (query: Record<string, any>): t.compiledFunction<TInput> {
let stringified = JSON.stringify(query)
const keys: string[] = []
const matches = stringified.match(/"###\w+###"/g)
if (matches === null) {
throw new Error('The query does not contain any use of `Q.params`')
}
for (const match of matches) {
const key = match.slice(4, -4)
keys.push(key)
stringified = stringified.replace(new RegExp(match), `input[${JSON.stringify(key)}]`)
}
const code = `
if (input == null) {
throw new Error('Input must not be empty')
}
const keys = ${JSON.stringify(keys)}
for (const key of keys) {
if (input[key] === undefined) {
throw new Error('Missing key: ' + key)
}
}
return ${stringified}
`
// @ts-ignore
return new Function('input', code) // eslint-disable-line
}
function match (key: string, val: string, opts?: Record<string, any>): t.Condition
function match (key: string, val: string[], opts?: Record<string, any>): t.Condition[]
function match (key: string, val: any, opts?: Record<string, any>): t.Condition | t.Condition[] {
return generateQueryObject('match', key, val, opts)
}
Q.match = match
Q.matchPhrase = function matchPhrase (key: string, val: string, opts?: Record<string, any>): t.Condition {
return generateQueryObject('match_phrase', key, val, opts)
}
Q.matchPhrasePrefix = function matchPhrasePrefix (key: string, val: string, opts?: Record<string, any>): t.Condition {
return generateQueryObject('match_phrase_prefix', key, val, opts)
}
Q.multiMatch = function multiMatch (keys: string[], val: string, opts?: Record<string, any>): t.Condition {
return {
multi_match: {
query: val,
fields: keys,
...opts
}
}
}
Q.matchAll = function matchAll (opts?: Record<string, any>): t.Condition {
return { match_all: { ...opts } }
}
Q.matchNone = function matchNone (): t.Condition {
return { match_none: {} }
}
Q.common = function common (key: string, val: string, opts: Record<string, any>): t.Condition {
return generateQueryObject('common', key, val, opts)
}
Q.queryString = function queryString (val: string, opts: Record<string, any>): t.Condition {
return {
query_string: {
query: val,
...opts
}
}
}
Q.simpleQueryString = function simpleQueryString (val: string, opts: Record<string, any>): t.Condition {
return {
simple_query_string: {
query: val,
...opts
}
}
}
Q.term = function term (key: string, val: string | string[], opts?: Record<string, any>): t.Condition {
if (Array.isArray(val)) {
return Q.terms(key, val, opts)
}
return generateValueObject('term', key, val, opts)
}
Q.terms = function terms (key: string, val: string[], opts?: Record<string, any>): t.Condition {
return {
terms: {
[key]: val,
...opts
}
}
}
Q.termsSet = function termsSet (key: string, val: string[], opts: Record<string, any>): t.Condition {
return {
terms_set: {
[key]: {
terms: val,
...opts
}
}
}
}
Q.range = function range (key: string, val: any): t.Condition {
return { range: { [key]: val } }
}
function exists (key: string): t.Condition
function exists (key: string[]): t.Condition[]
function exists (key: string | string[]): t.Condition | t.Condition[] {
if (Array.isArray(key)) {
return key.map(k => exists(k))
}
return { exists: { field: key } }
}
Q.exists = exists
function prefix (key: string, val: string, opts?: Record<string, any>): t.Condition
function prefix (key: string, val: string[], opts?: Record<string, any>): t.Condition[]
function prefix (key: string, val: any, opts?: Record<string, any>): t.Condition | t.Condition[] {
return generateValueObject('prefix', key, val, opts)
}
Q.prefix = prefix
function wildcard (key: string, val: string, opts?: Record<string, any>): t.Condition
function wildcard (key: string, val: string[], opts?: Record<string, any>): t.Condition[]
function wildcard (key: string, val: any, opts?: Record<string, any>): t.Condition | t.Condition[] {
return generateValueObject('wildcard', key, val, opts)
}
Q.wildcard = wildcard
function regexp (key: string, val: string, opts?: Record<string, any>): t.Condition
function regexp (key: string, val: string[], opts?: Record<string, any>): t.Condition[]
function regexp (key: string, val: any, opts?: Record<string, any>): t.Condition | t.Condition[] {
return generateValueObject('regexp', key, val, opts)
}
Q.regexp = regexp
function fuzzy (key: string, val: string, opts?: Record<string, any>): t.Condition
function fuzzy (key: string, val: string[], opts?: Record<string, any>): t.Condition[]
function fuzzy (key: string, val: any, opts?: Record<string, any>): t.Condition | t.Condition[] {
return generateValueObject('fuzzy', key, val, opts)
}
Q.fuzzy = fuzzy
Q.ids = function ids (key: string, val: string[], opts: Record<string, any>): t.Condition {
return {
ids: {
[key]: {
values: val,
...opts
}
}
}
}
Q.must = function must (...queries: t.AnyQuery[]): t.MustClause {
return { must: queries.flatMap(mergeableMust) }
}
Q.should = function should (...queries: t.AnyQuery[]): t.ShouldClause {
return { should: queries.flatMap(mergeableShould) }
}
Q.mustNot = function mustNot (...queries: t.AnyQuery[]): t.MustNotClause {
return { must_not: queries.flatMap(mergeableMustNot) }
}
Q.filter = function filter (...queries: t.AnyQuery[]): t.FilterClause {
return { filter: queries.flatMap(mergeableFilter) }
}
Q.bool = function bool (...queries: t.AnyQuery[]): t.BoolQuery {
if (queries.length === 0) {
return { query: { bool: {} } }
}
const normalizedQueries: t.BoolQueryOptions[] = queries
.flat()
.filter(val => {
// filters empty objects/arrays as well
if (typeof val === 'object' && val != null) {
return Object.keys(val).length > 0
}
return !!val
})
.map(q => {
if (isBool(q)) {
if (q.query.bool._name) {
return { must: [q.query] }
}
return q.query.bool
}
if (isClause(q)) {
return q
}
return { must: [q] }
})
const clauseCount = {
must: 0,
should: 0,
must_not: 0,
filter: 0
}
for (let i = 0; i < normalizedQueries.length; i++) {
const q = normalizedQueries[i]
if (q.must !== undefined) { clauseCount.must++ }
if (q.should !== undefined) { clauseCount.should++ }
if (q.must_not !== undefined) { clauseCount.must_not++ }
if (q.filter !== undefined) { clauseCount.filter++ }
}
// if there is at least one should, we cannot deep merge
// multiple clauses, so we check how many clauses we have per type
// and we throw an error if there is more than one per type
if (clauseCount.should > 0) {
if (clauseCount.must > 1 || clauseCount.must_not > 1 || clauseCount.filter > 1) {
throw new Error('Cannot merge this query')
}
}
const bool: t.BoolQueryOptions = deepMerge.all(normalizedQueries)
// if there are not should clauses,
// we can safely deepmerge queries
return {
query: {
bool: optimize(bool)
}
}
}
// Tries to flat the query based on the content
function optimize (q: t.BoolQueryOptions): t.BoolQueryOptions {
const clauses: t.BoolQueryOptions = {}
if (q.minimum_should_match !== undefined ||
q.should !== undefined || q._name !== undefined) {
return q
}
if (q.must) {
for (const c of q.must) {
if (isBoolBlock(c)) {
if (c.bool.should || c.bool._name) {
clauses.must = clauses.must || []
clauses.must.push(c)
} else {
// if we are in a BoolBlock and there is not a should clause
// then we can "merge up" the other clauses safely
if (c.bool.must) {
clauses.must = clauses.must || []
clauses.must.push.apply(clauses.must, c.bool.must)
}
if (c.bool.must_not) {
clauses.must_not = clauses.must_not || []
clauses.must_not.push.apply(clauses.must_not, c.bool.must_not)
}
if (c.bool.filter) {
clauses.filter = clauses.filter || []
clauses.filter.push.apply(clauses.filter, c.bool.filter)
}
}
} else {
clauses.must = clauses.must || []
clauses.must.push(c)
}
}
}
if (q.filter) {
for (const c of q.filter) {
if (isBoolBlock(c)) {
if (c.bool.should || c.bool.must_not || c.bool._name) {
clauses.filter = clauses.filter || []
clauses.filter.push(c)
} else {
// if there are must clauses and we are inside
// a filter clause, we can safely move them to the upper
// filter clause, since the score is not influenced
if (c.bool.must) {
clauses.filter = clauses.filter || []
clauses.filter.push.apply(clauses.filter, c.bool.must)
}
if (c.bool.filter) {
clauses.filter = clauses.filter || []
clauses.filter.push.apply(clauses.filter, c.bool.filter)
}
}
} else {
clauses.filter = clauses.filter || []
clauses.filter.push(c)
}
}
}
if (q.must_not) {
for (const c of q.must_not) {
if (isBoolBlock(c)) {
if (c.bool.should || c.bool.filter || c.bool._name) {
clauses.must_not = clauses.must_not || []
clauses.must_not.push(c)
} else {
// if 'c' is a BoolBlock and there are only must and must_not,
// then we can swap them safely
if (c.bool.must) {
clauses.must_not = clauses.must_not || []
clauses.must_not.push.apply(clauses.must_not, c.bool.must)
}
if (c.bool.must_not) {
clauses.must = clauses.must || []
clauses.must.push.apply(clauses.must, c.bool.must_not)
}
}
} else {
clauses.must_not = clauses.must_not || []
clauses.must_not.push(c)
}
}
}
return clauses
}
Q.and = function and (...queries: t.AnyQuery[]): t.BoolQuery {
let query = queries[0]
for (let i = 1; i < queries.length; i++) {
query = andOp(query, queries[i])
}
return query as t.BoolQuery
function andOp (q1: t.AnyQuery, q2: t.AnyQuery): t.BoolQuery {
const b1: t.BoolQuery = toMustQuery(q1)
const b2: t.BoolQuery = toMustQuery(q2)
if (!onlyShould(b1.query.bool) && !onlyShould(b2.query.bool)) {
return deepMerge(b1, b2)
} else {
const { must, ...clauses } = b1.query.bool
return Q.bool(
must == null ? Q.must(b2) : Q.must(must, b2),
clauses
)
}
}
}
Q.or = function or (...queries: t.AnyQuery[]): t.BoolQuery {
return Q.bool(Q.should(...queries))
}
Q.not = function not (q: t.AnyQuery): t.BoolQuery {
if (!isBool(q) && !isClause(q)) {
return Q.bool(Q.mustNot(q))
}
const b: t.BoolQuery = isClause(q)
? Q.bool(q as t.BoolQueryOptions)
: q as t.BoolQuery
if (onlyMust(b.query.bool)) {
return Q.bool(Q.mustNot(...b.query.bool.must))
} else if (onlyMustNot(b.query.bool)) {
return Q.bool(Q.must(...b.query.bool.must_not))
} else {
return Q.bool(Q.mustNot(b))
}
}
Q.minShouldMatch = function minShouldMatch (min: number): t.BoolQueryOptions {
return { minimum_should_match: min }
}
Q.name = function name (queryName: string): t.BoolQueryOptions {
return { _name: queryName }
}
Q.nested = function nested (path: string, query: any, opts: Record<string, any>): t.QueryBlock {
return {
query: {
nested: {
path,
...opts,
...query
}
}
}
}
Q.constantScore = function constantScore (query: any, boost: number): t.QueryBlock {
return {
query: {
constant_score: {
...query,
boost
}
}
}
}
Q.disMax = function disMax (queries: t.AnyQuery[], opts?: Record<string, any>): t.QueryBlock {
return {
query: {
dis_max: {
...opts,
queries: queries.flat()
}
}
}
}
Q.functionScore = function functionScore (function_score: any): t.QueryBlock {
return { query: { function_score } }
}
Q.boosting = function boosting (boostOpts: Record<string, any>): t.QueryBlock {
return { query: { boosting: boostOpts } }
}
Q.sort = function sort (key: string | any[], opts?: Record<string, any>): t.Condition {
if (Array.isArray(key) === true) {
return { sort: key }
}
return {
// @ts-ignore
sort: [{ [key]: opts }]
}
}
Q.size = function size (s: number): t.Condition {
return { size: s }
}
function generateQueryObject (queryType: string, key: string, val: string, opts?: Record<string, any>): t.Condition
function generateQueryObject (queryType: string, key: string, val: string[], opts?: Record<string, any>): t.Condition[]
function generateQueryObject (queryType: string, key: string, val: any, opts?: Record<string, any>): t.Condition | t.Condition[] {
if (Array.isArray(val)) {
return val.map(v => generateQueryObject(queryType, key, v, opts))
}
if (opts === undefined) {
return { [queryType]: { [key]: val } }
}
return {
[queryType]: {
[key]: {
query: val,
...opts
}
}
}
}
function generateValueObject (queryType: string, key: string, val: string, opts?: Record<string, any>): t.Condition
function generateValueObject (queryType: string, key: string, val: string[], opts?: Record<string, any>): t.Condition[]
function generateValueObject (queryType: string, key: string, val: any, opts?: Record<string, any>): t.Condition | t.Condition[] {
if (Array.isArray(val)) {
return val.map(v => generateValueObject(queryType, key, v, opts))
}
if (opts === undefined) {
return { [queryType]: { [key]: val } }
}
return {
[queryType]: {
[key]: {
value: val,
...opts
}
}
}
}
function isBool (q: any): q is t.BoolQuery {
return q.query && q.query.bool
}
function isBoolBlock (q: any): q is t.BoolBlock {
return !!q.bool
}
function isClause (q: any): q is t.BoolQueryOptions {
if (q.must !== undefined) return true
if (q.should !== undefined) return true
if (q.must_not !== undefined) return true
if (q.filter !== undefined) return true
if (q.minimum_should_match !== undefined) return true
if (q._name !== undefined) return true
return false
}
function onlyShould (bool: t.BoolQueryOptions): bool is t.ShouldClause {
if (bool.must !== undefined) return false
if (bool.must_not !== undefined) return false
if (bool.filter !== undefined) return false
if (bool.minimum_should_match !== undefined) return false
if (bool._name !== undefined) return false
return true
}
function onlyMust (bool: t.BoolQueryOptions): bool is t.MustClause {
if (bool.should !== undefined) return false
if (bool.must_not !== undefined) return false
if (bool.filter !== undefined) return false
if (bool.minimum_should_match !== undefined) return false
if (bool._name !== undefined) return false
return true
}
function onlyMustNot (bool: t.BoolQueryOptions): bool is t.MustNotClause {
if (bool.should !== undefined) return false
if (bool.must !== undefined) return false
if (bool.filter !== undefined) return false
if (bool.minimum_should_match !== undefined) return false
if (bool._name !== undefined) return false
return true
}
function onlyFilter (bool: t.BoolQueryOptions): bool is t.FilterClause {
if (bool.should !== undefined) return false
if (bool.must !== undefined) return false
if (bool.must_not !== undefined) return false
if (bool.minimum_should_match !== undefined) return false
if (bool._name !== undefined) return false
return true
}
// for a given query it always return a bool query:
// - if is a bool query returns the query
// - if is a clause, wraps the query in a bool block
// - if is condition, wraps the query into a must clause and then in a bool block
function toMustQuery (query: t.AnyQuery): t.BoolQuery {
if (isBool(query)) {
return query
}
if (isClause(query)) {
return { query: { bool: query } }
}
return { query: { bool: { must: [query] } } }
}
// the aim of this mergeable functions
// is to reduce the depth of the query objects
function mergeableMust (q: t.AnyQuery): t.AnyQuery | t.AnyQuery[] {
if (Array.isArray(q)) {
return q.map(mergeableMust)
}
if (isBool(q)) {
if (onlyMust(q.query.bool)) {
return q.query.bool.must
} else {
return q.query
}
} else if (isClause(q)) {
if (onlyMust(q)) {
return q.must
} else {
return { bool: q }
}
} else {
return q
}
}
function mergeableShould (q: t.AnyQuery): t.AnyQuery | t.AnyQuery[] {
if (Array.isArray(q)) {
return q.map(mergeableShould)
}
if (isBool(q)) {
if (onlyShould(q.query.bool)) {
return q.query.bool.should
} else {
return q.query
}
} else if (isClause(q)) {
if (onlyShould(q)) {
return q.should
} else {
return { bool: q }
}
} else {
return q
}
}
function mergeableMustNot (q: t.AnyQuery): t.AnyQuery | t.AnyQuery[] {
if (Array.isArray(q)) {
return q.map(mergeableMustNot)
}
if (isBool(q)) {
if (onlyMustNot(q.query.bool)) {
return q.query.bool.must_not
} else {
return q.query
}
} else if (isClause(q)) {
if (onlyMustNot(q)) {
return q.must_not
} else {
return { bool: q }
}
} else {
return q
}
}
function mergeableFilter (q: t.AnyQuery): t.AnyQuery | t.AnyQuery[] {
if (Array.isArray(q)) {
return q.map(mergeableFilter)
}
if (isBool(q)) {
if (onlyFilter(q.query.bool)) {
return q.query.bool.filter
} else {
return q.query
}
} else if (isClause(q)) {
if (onlyFilter(q)) {
return q.filter
} else {
return { bool: q }
}
} else {
return q
}
}
export default Q

75
dsl/src/types.ts Normal file
View File

@ -0,0 +1,75 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* eslint camelcase: 0 */
/* eslint no-use-before-define: 0 */
export interface Condition {
[key: string]: any
}
export interface QueryBlock {
query: {
[key: string]: any
}
}
export interface MustClause {
must: Condition[]
}
export interface MustNotClause {
must_not: Condition[]
}
export interface ShouldClause {
should: Condition[]
minimum_should_match?: number
}
export interface FilterClause {
filter: Condition[]
}
export interface BoolQuery<TOptions = BoolQueryOptions> {
query: {
bool: TOptions
}
}
export interface BoolBlock {
bool: BoolQueryOptions
}
export interface BoolQueryOptions {
must?: Condition[] | BoolBlock[]
must_not?: Condition[] | BoolBlock[]
should?: Condition[] | BoolBlock[]
filter?: Condition[] | BoolBlock[]
minimum_should_match?: number
_name?: string
}
export type AnyQuery = BoolQuery | BoolQueryOptions | Condition | Condition[]
export interface Aggregation {
[key: string]: any
}
export type compiledFunction<TInput> = (input: TInput) => Record<string, any>;

28
dsl/tsconfig.json Normal file
View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
"moduleResolution": "node",
"declaration": true,
"target": "es2017",
"module": "commonjs",
"outDir": "lib",
"pretty": true,
"noEmitOnError": true,
"experimentalDecorators": true,
"sourceMap": true,
"emitDecoratorMetadata": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"esModuleInterop": true,
"removeComments": true,
"noUnusedLocals": true,
"lib": [
"esnext"
]
},
"formatCodeOptions": {
"identSize": 2,
"tabSize": 2
},
"exclude": ["examples"],
"include": ["./src/*.ts"]
}