Merge pull request #2614 from coreos-inc/search-improvements

Improvements to new Quay search
This commit is contained in:
josephschorr 2017-05-03 17:06:58 -04:00 committed by GitHub
commit 43e032299c
5 changed files with 43 additions and 12 deletions

View file

@ -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

View file

@ -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">

View file

@ -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);

View file

@ -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) {

View file

@ -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>