Fix #2757 - Use full text search ranking when searching in list views. (#2798)

This applies to the queries, dashboard and users views.
This commit is contained in:
Jannis Leidel 2018-10-16 10:38:37 +02:00 committed by GitHub
parent 5b2ec81e65
commit af3a1e00c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 90 additions and 40 deletions

View File

@ -17,14 +17,12 @@ export default class ListCtrl {
if (this.pageOrderReverse) {
this.pageOrder = this.pageOrder.substr(1);
}
this.defaultOptions = {};
// use $parent because we're using a component as route target instead of controller;
// $parent refers to scope created for the page by router
this.resource = $scope.$parent.$resolve.resource;
this.currentPage = $scope.$parent.$resolve.currentPage;
this.currentUser = currentUser;
this.showEmptyState = false;
@ -38,20 +36,12 @@ export default class ListCtrl {
this.isInSearchMode = () => this.searchTerm !== undefined && this.searchTerm !== null && this.searchTerm.length > 0;
const fetcher = (requestedPage, itemsPerPage, orderByField, orderByReverse) => {
const fetcher = (requestedPage, itemsPerPage, orderByField, orderByReverse, paginator, requested = false) => {
$location.search('page', requestedPage);
$location.search('page_size', itemsPerPage);
if (orderByReverse && !orderByField.startsWith(this.orderSeparator)) {
orderByField = this.orderSeparator + orderByField;
}
if (orderByField) {
$location.search('order', orderByField);
} else {
$location.search('order', undefined);
}
const request = this.getRequest(requestedPage, itemsPerPage, orderByField);
const order = this.getOrder(orderByField, orderByReverse, requested, paginator);
$location.search('order', order);
const request = this.getRequest(requestedPage, itemsPerPage, order);
if (this.searchTerm === '') {
this.searchTerm = null;
@ -80,8 +70,12 @@ export default class ListCtrl {
};
this.update = () => {
// `queriesFetcher` will be called by paginator
this.paginator.setPage(this.page, this.pageSize);
this.paginator.setPage(
this.page,
this.pageSize,
this.pageOrder,
this.pageOrderReverse,
);
};
}
@ -89,6 +83,27 @@ export default class ListCtrl {
this.loaded = true;
}
getOrder(orderByField, orderByReverse, requested, paginator) {
// in search mode ignore the ordering and use the ranking order
// provided by the server-side FTS backend instead, unless it was
// requested by the user by actively ordering in search mode
if (this.isInSearchMode() && !requested) {
orderByField = undefined;
} else {
this.pageOrder = orderByField;
this.pageOrderReverse = orderByReverse;
}
// pass the current ordering state to the paginator
// so the sort icons work correctly
paginator.orderByField = orderByField;
paginator.orderByReverse = orderByReverse;
// combine the ordering field and direction in one query parameter
if (orderByField && orderByReverse && !orderByField.startsWith(this.orderSeparator)) {
orderByField = this.orderSeparator + orderByField;
}
return orderByField;
}
getRequest(requestedPage, itemsPerPage, orderByField) {
const request = Object.assign({}, this.defaultOptions, {
page: requestedPage,

View File

@ -10,11 +10,17 @@ export default class LivePaginator {
this.fetchPage(page);
}
fetchPage(page) {
this.rowsFetcher(page, this.itemsPerPage, this.orderByField, this.orderByReverse);
fetchPage(page, requested = false) {
this.rowsFetcher(page, this.itemsPerPage, this.orderByField, this.orderByReverse, this, requested);
}
setPage(page, pageSize) {
setPage(page, pageSize, pageOrder, pageOrderReverse) {
if (pageOrder) {
this.orderByField = pageOrder;
}
if (pageOrderReverse) {
this.orderByReverse = pageOrderReverse;
}
if (pageSize) {
this.itemsPerPage = pageSize;
}
@ -44,7 +50,7 @@ export default class LivePaginator {
}
if (this.orderByField) {
this.fetchPage(this.page);
this.fetchPage(this.page, true);
}
}
}

View File

@ -130,20 +130,26 @@ def filter_by_tags(result_set, column):
if request.args.getlist('tags'):
tags = request.args.getlist('tags')
result_set = result_set.filter(cast(column, postgresql.ARRAY(db.Text)).contains(tags))
return result_set
def order_results(results, default_order, orders_whitelist):
def order_results(results, default_order, allowed_orders, fallback=True):
"""
Orders the given results with the sort order as requested in the
"order" request query parameter or the given default order.
"""
# See if a particular order has been requested
order = request.args.get('order', '').strip() or default_order
requested_order = request.args.get('order', '').strip()
# and if not (and no fallback is wanted) return results as is
if not requested_order and not fallback:
return results
# and if it matches a long-form for related fields, falling
# back to the default order
selected_order = orders_whitelist.get(order, default_order)
selected_order = allowed_orders.get(requested_order, None)
if selected_order is None and fallback:
selected_order = default_order
# The query may already have an ORDER BY statement attached
# so we clear it here and apply the selected order
return sort_query(results.order_by(None), selected_order)

View File

@ -1,5 +1,5 @@
from flask import request, url_for
from funcy import project, rpartial
from funcy import project, partial
from flask_restful import abort
from redash import models, serializers
@ -21,7 +21,11 @@ order_map = {
'-created_at': '-created_at',
}
order_results = rpartial(_order_results, '-created_at', order_map)
order_results = partial(
_order_results,
default_order='-created_at',
allowed_orders=order_map,
)
class DashboardListResource(BaseResource):
@ -56,8 +60,10 @@ class DashboardListResource(BaseResource):
results = filter_by_tags(results, models.Dashboard.tags)
# order results according to passed order parameter
ordered_results = order_results(results)
# order results according to passed order parameter,
# special-casing search queries where the database
# provides an order by search rank
ordered_results = order_results(results, fallback=bool(search_term))
page = request.args.get('page', 1, type=int)
page_size = request.args.get('page_size', 25, type=int)

View File

@ -20,8 +20,10 @@ class QueryFavoriteListResource(BaseResource):
favorites = filter_by_tags(favorites, models.Query.tags)
# order results according to passed order parameter
ordered_favorites = order_results(favorites)
# order results according to passed order parameter,
# special-casing search queries where the database
# provides an order by search rank
ordered_favorites = order_results(favorites, fallback=bool(search_term))
page = request.args.get('page', 1, type=int)
page_size = request.args.get('page_size', 25, type=int)

View File

@ -3,7 +3,7 @@ from flask import jsonify, request, url_for
from flask_login import login_required
from flask_restful import abort
from sqlalchemy.orm.exc import StaleDataError
from funcy import rpartial
from funcy import partial
from redash import models
from redash.authentication.org_resolving import current_org
@ -34,7 +34,11 @@ order_map = {
'-created_by': '-users-name',
}
order_results = rpartial(_order_results, '-created_at', order_map)
order_results = partial(
_order_results,
default_order='-created_at',
allowed_orders=order_map,
)
@routes.route(org_scoped_rule('/api/queries/format'), methods=['POST'])
@ -188,8 +192,10 @@ class QueryListResource(BaseResource):
results = filter_by_tags(results, models.Query.tags)
# order results according to passed order parameter
ordered_results = order_results(results)
# order results according to passed order parameter,
# special-casing search queries where the database
# provides an order by search rank
ordered_results = order_results(results, fallback=bool(search_term))
page = request.args.get('page', 1, type=int)
page_size = request.args.get('page_size', 25, type=int)
@ -239,8 +245,10 @@ class MyQueriesResource(BaseResource):
results = filter_by_tags(results, models.Query.tags)
# order results according to passed order parameter
ordered_results = order_results(results)
# order results according to passed order parameter,
# special-casing search queries where the database
# provides an order by search rank
ordered_results = order_results(results, fallback=bool(search_term))
page = request.args.get('page', 1, type=int)
page_size = request.args.get('page_size', 25, type=int)

View File

@ -5,7 +5,7 @@ from flask_login import current_user
from funcy import project
from sqlalchemy.exc import IntegrityError
from disposable_email_domains import blacklist
from funcy import rpartial
from funcy import partial
from redash import models
from redash.permissions import require_permission, require_admin_or_owner, is_admin_or_owner, \
@ -25,7 +25,11 @@ order_map = {
'-groups': '-group_ids',
}
order_results = rpartial(_order_results, '-created_at', order_map)
order_results = partial(
_order_results,
default_order='-created_at',
allowed_orders=order_map,
)
def invite_user(org, inviter, user):
@ -75,9 +79,12 @@ class UserListResource(BaseResource):
'object_type': 'user',
})
users = order_results(users)
# order results according to passed order parameter,
# special-casing search queries where the database
# provides an order by search rank
ordered_users = order_results(users, fallback=bool(search_term))
return paginate(users, page, page_size, serialize_user)
return paginate(ordered_users, page, page_size, serialize_user)
@require_admin
def post(self):