Merge pull request #2614 from coreos-inc/search-improvements
Improvements to new Quay search
This commit is contained in:
commit
43e032299c
5 changed files with 43 additions and 12 deletions
|
@ -4,6 +4,7 @@ import random
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
from peewee import JOIN_LEFT_OUTER, fn, SQL, IntegrityError
|
from peewee import JOIN_LEFT_OUTER, fn, SQL, IntegrityError
|
||||||
|
from playhouse.shortcuts import case
|
||||||
from cachetools import ttl_cache
|
from cachetools import ttl_cache
|
||||||
|
|
||||||
from data.model import (config, DataModelException, tag, db_transaction, storage, permission,
|
from data.model import (config, DataModelException, tag, db_transaction, storage, permission,
|
||||||
|
@ -485,14 +486,21 @@ def _get_sorted_matching_repositories(lookup_value, repo_kind='image', include_p
|
||||||
|
|
||||||
# Always search at least on name (init clause)
|
# Always search at least on name (init clause)
|
||||||
clause = Repository.name.match(lookup_value)
|
clause = Repository.name.match(lookup_value)
|
||||||
|
computed_score = RepositorySearchScore.score.alias('score')
|
||||||
|
|
||||||
|
# If the description field is in the search fields, then we need to compute a synthetic score
|
||||||
|
# to discount the weight of the description more than the name.
|
||||||
if SEARCH_FIELDS.description.name in search_fields:
|
if SEARCH_FIELDS.description.name in search_fields:
|
||||||
clause = Repository.description.match(lookup_value) | clause
|
clause = Repository.description.match(lookup_value) | clause
|
||||||
|
|
||||||
last_week = datetime.now() - timedelta(weeks=1)
|
cases = [
|
||||||
|
(Repository.name.match(lookup_value), 100 * RepositorySearchScore.score),
|
||||||
|
]
|
||||||
|
|
||||||
|
computed_score = case(None, cases, RepositorySearchScore.score).alias('score')
|
||||||
|
|
||||||
query = (Repository
|
query = (Repository
|
||||||
.select(Repository, Namespace)
|
.select(Repository, Namespace, computed_score)
|
||||||
.join(Namespace, on=(Namespace.id == Repository.namespace_user))
|
.join(Namespace, on=(Namespace.id == Repository.namespace_user))
|
||||||
.where(clause)
|
.where(clause)
|
||||||
.group_by(Repository.id, Namespace.id))
|
.group_by(Repository.id, Namespace.id))
|
||||||
|
@ -507,7 +515,7 @@ def _get_sorted_matching_repositories(lookup_value, repo_kind='image', include_p
|
||||||
.switch(Repository)
|
.switch(Repository)
|
||||||
.join(RepositorySearchScore)
|
.join(RepositorySearchScore)
|
||||||
.group_by(Repository, Namespace, RepositorySearchScore)
|
.group_by(Repository, Namespace, RepositorySearchScore)
|
||||||
.order_by(RepositorySearchScore.score.desc()))
|
.order_by(SQL('score').desc()))
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
|
@ -35,16 +35,24 @@
|
||||||
first-line-only="true" placeholder-needed="false"></div>
|
first-line-only="true" placeholder-needed="false"></div>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
<span ng-switch-when="application">
|
||||||
|
<span class="avatar" data="result.namespace.avatar" size="16"></span>
|
||||||
|
<span class="result-name">{{ result.namespace.name }}/{{ result.name }}</span>
|
||||||
|
<div class="result-description" ng-if="result.description">
|
||||||
|
<div class="description markdown-view" content="result.description"
|
||||||
|
first-line-only="true" placeholder-needed="false"></div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<input class="form-control" type="text" placeholder="search"
|
<input class="form-control" type="text" placeholder="search"
|
||||||
ng-model="$ctrl.enteredQuery"
|
ng-model="$ctrl.enteredQuery"
|
||||||
typeahead="$ctrl.onTypeahead($event)"
|
typeahead="$ctrl.onTypeahead($event)"
|
||||||
|
ta-debounce="250"
|
||||||
ta-display-key="name"
|
ta-display-key="name"
|
||||||
ta-suggestion-tmpl="search-result-template"
|
ta-suggestion-tmpl="search-result-template"
|
||||||
ta-clear-on-select="true"
|
ta-clear-on-select="$ctrl.clearOnSearch"
|
||||||
(ta-selected)="$ctrl.onSelected($event)"
|
(ta-selected)="$ctrl.onSelected($event)"
|
||||||
(ta-entered)="$ctrl.onEntered($event)">
|
(ta-entered)="$ctrl.onEntered($event)">
|
||||||
<span class="search-icon">
|
<span class="search-icon">
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { Input, Component, Inject } from 'ng-metadata/core';
|
||||||
})
|
})
|
||||||
export class SearchBoxComponent {
|
export class SearchBoxComponent {
|
||||||
@Input('<query') public enteredQuery: string = '';
|
@Input('<query') public enteredQuery: string = '';
|
||||||
|
@Input('@clearOnSearch') public clearOnSearch: string = 'true';
|
||||||
|
|
||||||
private isSearching: boolean = false;
|
private isSearching: boolean = false;
|
||||||
private currentQuery: string = '';
|
private currentQuery: string = '';
|
||||||
|
@ -48,7 +49,7 @@ export class SearchBoxComponent {
|
||||||
|
|
||||||
private onEntered($event): void {
|
private onEntered($event): void {
|
||||||
this.$timeout(() => {
|
this.$timeout(() => {
|
||||||
$event['callback'](true); // Clear the value.
|
$event['callback'](this.clearOnSearch == 'true'); // Clear the value.
|
||||||
this.$location.url('/search');
|
this.$location.url('/search');
|
||||||
this.$location.search('q', $event['value']);
|
this.$location.search('q', $event['value']);
|
||||||
}, 10);
|
}, 10);
|
||||||
|
|
|
@ -13,16 +13,19 @@ export class TypeaheadDirective implements AfterContentInit {
|
||||||
@Input('taDisplayKey') displayKey: string = '';
|
@Input('taDisplayKey') displayKey: string = '';
|
||||||
@Input('taSuggestionTmpl') suggestionTemplate: string = '';
|
@Input('taSuggestionTmpl') suggestionTemplate: string = '';
|
||||||
@Input('taClearOnSelect') clearOnSelect: boolean = false;
|
@Input('taClearOnSelect') clearOnSelect: boolean = false;
|
||||||
|
@Input('taDebounce') debounce: number = 250;
|
||||||
|
|
||||||
@Output('taSelected') selected = new EventEmitter<any>();
|
@Output('taSelected') selected = new EventEmitter<any>();
|
||||||
@Output('taEntered') entered = new EventEmitter<any>();
|
@Output('taEntered') entered = new EventEmitter<any>();
|
||||||
|
|
||||||
private itemSelected: boolean = false;
|
private itemSelected: boolean = false;
|
||||||
|
private existingTimer: Promise = null;
|
||||||
|
|
||||||
constructor(@Inject('$element') private $element: ng.IAugmentedJQuery,
|
constructor(@Inject('$element') private $element: ng.IAugmentedJQuery,
|
||||||
@Inject('$compile') private $compile: ng.ICompileService,
|
@Inject('$compile') private $compile: ng.ICompileService,
|
||||||
@Inject('$scope') private $scope: ng.IScope,
|
@Inject('$scope') private $scope: ng.IScope,
|
||||||
@Inject('$templateRequest') private $templateRequest: ng.ITemplateRequestService) {
|
@Inject('$templateRequest') private $templateRequest: ng.ITemplateRequestService,
|
||||||
|
@Inject('$timeout') private $timeout: ng.ITimeoutService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngAfterContentInit(): void {
|
public ngAfterContentInit(): void {
|
||||||
|
@ -52,12 +55,23 @@ export class TypeaheadDirective implements AfterContentInit {
|
||||||
templates: templates,
|
templates: templates,
|
||||||
display: this.displayKey,
|
display: this.displayKey,
|
||||||
source: (query, results, asyncResults) => {
|
source: (query, results, asyncResults) => {
|
||||||
this.typeahead.emit({'query': query, 'callback': asyncResults});
|
this.debounceQuery(query, asyncResults);
|
||||||
this.itemSelected = false;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private debounceQuery(query: string, asyncResults: Function): void {
|
||||||
|
if (this.existingTimer) {
|
||||||
|
this.$timeout.cancel(this.existingTimer);
|
||||||
|
this.existingTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.existingTimer = this.$timeout(() => {
|
||||||
|
this.typeahead.emit({'query': query, 'callback': asyncResults});
|
||||||
|
this.itemSelected = false;
|
||||||
|
}, this.debounce);
|
||||||
|
}
|
||||||
|
|
||||||
@HostListener('keyup', ['$event'])
|
@HostListener('keyup', ['$event'])
|
||||||
public onKeyup(event: JQueryKeyEventObject): void {
|
public onKeyup(event: JQueryKeyEventObject): void {
|
||||||
if (!this.itemSelected && event.keyCode == 13) {
|
if (!this.itemSelected && event.keyCode == 13) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="cor-container search">
|
<div class="cor-container search">
|
||||||
<div class="search-top-bar">
|
<div class="search-top-bar">
|
||||||
<search-box query="currentQuery"></search-box>
|
<search-box query="currentQuery" clear-on-search="false"></search-box>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-results-section">
|
<div class="search-results-section">
|
||||||
|
@ -21,11 +21,11 @@
|
||||||
</span>
|
</span>
|
||||||
<h4>
|
<h4>
|
||||||
<i class="fa fa-hdd-o" ng-if="result.kind == 'repository'"></i>
|
<i class="fa fa-hdd-o" ng-if="result.kind == 'repository'"></i>
|
||||||
<i class="fa ci-app-cube" ng-if="result.kind == 'application'"></i>
|
<i class="fa ci-appcube" ng-if="result.kind == 'application'"></i>
|
||||||
<a href="{{ result.href }}">{{ result.namespace.name }}/{{ result.name }}</a>
|
<a href="{{ result.href }}">{{ result.namespace.name }}/{{ result.name }}</a>
|
||||||
</h4>
|
</h4>
|
||||||
<p class="description">
|
<p class="description">
|
||||||
<span class="markdown-view" content="result.description"></span>
|
<span class="markdown-view" content="result.description" first-line-only="true"></span>
|
||||||
</p>
|
</p>
|
||||||
<p class="result-info-bar">
|
<p class="result-info-bar">
|
||||||
Last Modified: <span am-time-ago="result.last_modified * 1000"></span>
|
Last Modified: <span am-time-ago="result.last_modified * 1000"></span>
|
||||||
|
|
Reference in a new issue