-
Notifications
You must be signed in to change notification settings - Fork 8.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New type of filter to get all the values for a field within the current context #17571
New type of filter to get all the values for a field within the current context #17571
Conversation
Since this is a community submitted pull request, a Jenkins build has not been kicked off automatically. Can an Elastic organization member please verify the contents of this patch and then kick off a build manually? |
I know most of us are pretty busy right now (@Bargs and @lukasolson especially) but I'll take a look at this in the next couple days, thanks for submitting it! |
Just to be clear, we'll need to get this ported over to master before merging, but should be able to review most of it in its current state. |
@spalger Thank you.
I totally agree I opened this PR to get a feedback about the design. I am planning to rebase on top of master as soon as 6.3 gets out. As on now, I am building my custom Kibana form source with my changes. |
This filter type gets all the values for one field matching the current state of time picker and filters on the filter bar, then it makes a filter to select all of them.
I did some tidying up of the commits to make your life easier in the review |
Actually, I also have a branch rebased on master but I did not have the time to test it. |
Also now it is hardcoded to a 10k limit for the value, I would like to make this configurable maybe with an advanced setting or with an argument in the filter pop up. |
} | ||
|
||
function getBody(field, { query }) { | ||
const aggregationSize = 10000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also now it is hardcoded to a 10k limit for the value, I would like to make this configurable maybe with an advanced setting or with an argument in the filter pop up.
Yeah, I think it would be idea to support this as a parameter and default to something smaller, like 100.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also think the the name Top Terms
or Top Values
would be more appropriate for this filter, since it's always going to be limited by the size (even if we set it to something huge like 10k).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think it would be idea to support this as a parameter and default to something smaller, like 100.
What about having the default value in an advanced setting? It is easier to have a custom default value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works for me, the default can be read from the route handler with:
const defaultSize = await request.getUiSettingsService().get('...')
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some initial comments
server.route({ | ||
path: '/api/kibana/values/{index}', | ||
method: ['POST'], | ||
handler: async function (req, reply) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please define validation rules for the supported params and payload parameters. I think it should look something like:
server.route({
path: '/api/kibana/values/{index}',
method: ['POST'],
config: {
validate: {
params: Joi.object().keys({
index: Joi.string().required()
}).default(),
payload: Joi.object().keys({
field: Joi.string().required(),
query: Joi.object().required(),
}).required()
},
async handler(req, reply) {
// ...
}
}
})
PS, the query
parameter supports a lot of stuff. @elastic/kibana-operations @elastic/kibana-platform does this concern any of you?
method: ['POST'], | ||
handler: async function (req, reply) { | ||
const { index } = req.params; | ||
const { field } = req.payload; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd merge this and the next line
|
||
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); | ||
const body = getBody(field, query); | ||
let fieldList = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This collects the list of values, not a list of fields, right?
const currentField = buckets[i].key; | ||
if (typeof (currentField) === 'object') { | ||
// its an array | ||
fieldList = fieldList.concat(currentField); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had no idea that bucket keys could be arrays.
Also, you could merge the handling of array/non-array bucket keys and avoid going from Array -> Set -> Array
if you just use one from the beginning:
const response = await callWithRequest(req, 'search', { index, body });
const values = new Set();
for (const bucket of response.aggregations.aggResults) {
for (const value of [].concat(bucket.key)) {
values.add(value)
}
}
reply(Array.from(values))
} | ||
} | ||
const uniqueValues = Array.from(new Set(fieldList)); | ||
reply(uniqueValues); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JSON array responses can have weird security implications, I suggest responding with an object that has a values
property instead.
@@ -68,6 +69,10 @@ export function requestFetchParamsToBody( | |||
index = indexList; | |||
} | |||
|
|||
|
|||
$rootScope.filterState = { query: body.query }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function is called for each request on the dashboard, which means it gets the specific filters for each panel. We need to find a better way to communicate this information to the values filter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really don't want to introduce more reliance on rootScope in courier. I've been trying hard to de-angularize it. Is there perhaps another way we can achieve the same result? (I ask this without actually looking at the code beyond spying this one tidbit).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the intention behind the discussion in #17571 (comment)
|
||
const postParams = { | ||
field: field.name, | ||
query: $rootScope.filterState |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than writing the filter state to $rootScope
, maybe we can use a temporary SearchSource
to determine the current filters? @elastic/kibana-discovery? Might work something like this:
import chrome from 'ui/chrome';
import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter';
export function filterParamsValuesController($http, $scope, courier, Private, Promise) {
const baseUrl = chrome.addBasePath('/api/kibana/values');
const queryFilter = Private(FilterBarQueryFilterProvider);
function getRootSearchSource(searchSource) {
const parent = searchSource.getParent();
return parent ? getRootSearchSource(parent) : searchSource;
}
this.getAllFieldValues = function (field) {
field = $scope.field;
if (!field || !field.aggregatable || field.type !== 'string') {
return Promise.resolve([]);
}
const searchSource = new courier.SearchSource();
return searchSource
.set('index', field.indexPattern)
.set('filter', queryFilter.getFilters())
// this is necessary because apps like dashboard modify the "root search source" in
// the courier to ensure that all search sources inherit from the dashboard saved
// object. Since not every app does this we override this by inheriting from the true
// root by walking up until we find a searchSource that has no parents.
.inherits(getRootSearchSource(searchSource))
.getSearchRequestBody()
.then(({ query }) => (
$http.post(`${baseUrl}/${field.indexPattern.title}`, {
field: field.name,
query
})
))
.then(response => {
$scope.$emit('value_event', response.data);
});
};
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
btw, these are just ideas, not tested or sure it works well in all situations :)
@stacey-gammon do you know why we don't set the search source in apps like Discover to be the root search source?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, no, but I don't even like that dashboard does it. It's created some strange bugs. See #17123 and #16641 - we are workin towards getting rid of the search source inheritance all together.
Instead we want dashboard to pass filters, time range and query explicitly down to it's embeddables.
I haven't looked really at any of this PR but I'd be worried that any reliance on searchSource inheritance will break in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could probably just pull in the queryFilter
and timeFilter
services, I'm mostly just worried about not doing things the same way they are done by the app itself that might cause the values retrieved by this filter to not match the values being rendered by the vis/discover/dashboard they are supposed to match.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't had much time to look into this either, but just want to second Stacey here. We will remove search source inheritance rather sooner than later (hopefully within one of the next minor versions). I really think we shouldn't build anything new that relies on that anymore. And of course also second, not introducing any new Angular code here, since we are really hardly trying to get rid of Angular.
|
||
const baseUrl = chrome.addBasePath('/api/kibana/values'); | ||
|
||
export function filterParamsValuesController($http, $scope, $rootScope) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sure to pull in the Promise
service here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But we are trying to remove reliance on the angular promise service, and the filter editor should be moving to react eventually. I don't actually think we want to use the angular Promise class here. #13855
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is using a mix of angular promises from $http
I think it should also return angular promises when it returns early. This is also an angular controller, so I think angular promises are appropriate.
return { | ||
size: 0, | ||
timeout: '60s', | ||
aggs: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a more efficient way to get this values back? Terms aggregation are pretty expensive especially when you set up a very high aggregation size. In my use case, I am planning to have this number 50k.
I am going to wait for the change about |
Thanks for your understanding and giving us the time to clean this up properly beforehand and many many thanks for your contribution ❤️ |
Also, I will rebase against master and update the PR. @timroes better doing things once and the right way 😉 |
@timroes sorry I closed it by mistake can you reopen it? |
Sorry, you deleted the branch, that's why GitHub doesn't allow reopening it. Just open a new one and refer to this PR number, so we still have the link to this discussion. |
@timroes right, I was doing some cleanup. I cleaned up the wrong branch. I will open a new one against master. |
Hi everyone,
My colleagues and I are working on a new filter type. We called it
values
because it gets all the values of a field given the current filter/time context.I want to get the idea out so if someone of the Kibana dev team like this idea and has some time to look into I will appreciate the feedback we could work together to make it better code and design wise.
@nreese @spalger @Bargs @lukasolson: We talked about this, I would appreciate your take on this.
I am opening this PR against 6.2 instead of master for simplicity.
Relevant issue: #16702
Why we did it?
After we drilled down to in the dataset we often want to see what other data the entities matching the drill down filters have produced overall or with other filters applied. At the moment this could be achieved by creating a filter
is one of
with all the values we want to select but what if the list of values is 10k? You cannot manually add 10k element it is not usable. Our first workaround was to export all the values and create a DSL filtered query to select this values. But we wanted to be able to this within Kibana UI. Hence, this PR.Description
values
that have a button to fetch all the values of the selected field considering the current filters/time context.Goal
Improve the data exploration of Kibana giving a quick and user-friendly way to do exploration like "if this than that"