##// END OF EJS Templates
search: added per-backend sorting fields....
dan -
r3968:2ec277c2 default
parent child Browse files
Show More
@@ -1,93 +1,115 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22 from rhodecode.tests import HG_REPO
23 23 from rhodecode.api.tests.utils import (
24 24 build_data, api_call, assert_error, assert_ok)
25 25
26 26
27 27 @pytest.mark.usefixtures("testuser_api", "app")
28 28 class TestApiSearch(object):
29 29
30 @pytest.mark.parametrize("sort_dir", [
31 "asc",
32 "desc",
33 ])
34 @pytest.mark.parametrize("sort", [
35 "xxx",
36 "author_email",
37 "date",
38 "message",
39 ])
30 40 @pytest.mark.parametrize("query, expected_hits, expected_paths", [
31 41 ('todo', 23, [
32 42 'vcs/backends/hg/inmemory.py',
33 43 'vcs/tests/test_git.py']),
34 44 ('extension:rst installation', 6, [
35 45 'docs/index.rst',
36 46 'docs/installation.rst']),
37 47 ('def repo', 87, [
38 48 'vcs/tests/test_git.py',
39 49 'vcs/tests/test_changesets.py']),
40 50 ('repository:%s def test' % HG_REPO, 18, [
41 51 'vcs/tests/test_git.py',
42 52 'vcs/tests/test_changesets.py']),
43 53 ('"def main"', 9, [
44 54 'vcs/__init__.py',
45 55 'vcs/tests/__init__.py',
46 56 'vcs/utils/progressbar.py']),
47 57 ('owner:test_admin', 358, [
48 58 'vcs/tests/base.py',
49 59 'MANIFEST.in',
50 60 'vcs/utils/termcolors.py',
51 61 'docs/theme/ADC/static/documentation.png']),
52 62 ('owner:test_admin def main', 72, [
53 63 'vcs/__init__.py',
54 64 'vcs/tests/test_utils_filesize.py',
55 65 'vcs/tests/test_cli.py']),
56 66 ('owner:michaΕ‚ test', 0, []),
57 67 ])
58 def test_search_content_results(self, query, expected_hits, expected_paths):
68 def test_search_content_results(self, sort_dir, sort, query, expected_hits, expected_paths):
59 69 id_, params = build_data(
60 70 self.apikey_regular, 'search',
61 71 search_query=query,
72 search_sort='{}:{}'.format(sort_dir, sort),
62 73 search_type='content')
63 74
64 75 response = api_call(self.app, params)
65 76 json_response = response.json
66 77
67 78 assert json_response['result']['item_count'] == expected_hits
68 79 paths = [x['f_path'] for x in json_response['result']['results']]
69 80
70 81 for expected_path in expected_paths:
71 82 assert expected_path in paths
72 83
84 @pytest.mark.parametrize("sort_dir", [
85 "asc",
86 "desc",
87 ])
88 @pytest.mark.parametrize("sort", [
89 "xxx",
90 "date",
91 "file",
92 "size",
93 ])
73 94 @pytest.mark.parametrize("query, expected_hits, expected_paths", [
74 95 ('readme.rst', 3, []),
75 96 ('test*', 75, []),
76 97 ('*model*', 1, []),
77 98 ('extension:rst', 48, []),
78 99 ('extension:rst api', 24, []),
79 100 ])
80 def test_search_file_paths(self, query, expected_hits, expected_paths):
101 def test_search_file_paths(self, sort_dir, sort, query, expected_hits, expected_paths):
81 102 id_, params = build_data(
82 103 self.apikey_regular, 'search',
83 104 search_query=query,
105 search_sort='{}:{}'.format(sort_dir, sort),
84 106 search_type='path')
85 107
86 108 response = api_call(self.app, params)
87 109 json_response = response.json
88 110
89 111 assert json_response['result']['item_count'] == expected_hits
90 112 paths = [x['f_path'] for x in json_response['result']['results']]
91 113
92 114 for expected_path in expected_paths:
93 115 assert expected_path in paths
@@ -1,169 +1,173 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import urllib
23 23 from pyramid.view import view_config
24 24 from webhelpers.util import update_params
25 25
26 26 from rhodecode.apps._base import BaseAppView, RepoAppView, RepoGroupAppView
27 27 from rhodecode.lib.auth import (
28 28 LoginRequired, HasRepoPermissionAnyDecorator, HasRepoGroupPermissionAnyDecorator)
29 29 from rhodecode.lib.helpers import Page
30 30 from rhodecode.lib.utils2 import safe_str
31 31 from rhodecode.lib.index import searcher_from_config
32 32 from rhodecode.model import validation_schema
33 33 from rhodecode.model.validation_schema.schemas import search_schema
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 def perform_search(request, tmpl_context, repo_name=None, repo_group_name=None):
39 39 searcher = searcher_from_config(request.registry.settings)
40 40 formatted_results = []
41 41 execution_time = ''
42 42
43 43 schema = search_schema.SearchParamsSchema()
44 44 search_tags = []
45 45 search_params = {}
46 46 errors = []
47 47
48 48 try:
49 49 search_params = schema.deserialize(
50 50 dict(
51 51 search_query=request.GET.get('q'),
52 52 search_type=request.GET.get('type'),
53 53 search_sort=request.GET.get('sort'),
54 54 search_max_lines=request.GET.get('max_lines'),
55 55 page_limit=request.GET.get('page_limit'),
56 56 requested_page=request.GET.get('page'),
57 57 )
58 58 )
59 59 except validation_schema.Invalid as e:
60 60 errors = e.children
61 61
62 62 def url_generator(**kw):
63 63 q = urllib.quote(safe_str(search_query))
64 64 return update_params(
65 65 "?q=%s&type=%s&max_lines=%s&sort=%s" % (
66 66 q, safe_str(search_type), search_max_lines, search_sort), **kw)
67 67
68 68 c = tmpl_context
69 69 search_query = search_params.get('search_query')
70 70 search_type = search_params.get('search_type')
71 71 search_sort = search_params.get('search_sort')
72 72 search_max_lines = search_params.get('search_max_lines')
73 73 if search_params.get('search_query'):
74 74 page_limit = search_params['page_limit']
75 75 requested_page = search_params['requested_page']
76 76
77 77 try:
78 78 search_result = searcher.search(
79 79 search_query, search_type, c.auth_user, repo_name, repo_group_name,
80 80 requested_page=requested_page, page_limit=page_limit, sort=search_sort)
81 81
82 82 formatted_results = Page(
83 83 search_result['results'], page=requested_page,
84 84 item_count=search_result['count'],
85 85 items_per_page=page_limit, url=url_generator)
86 86 finally:
87 87 searcher.cleanup()
88 88
89 89 search_tags = searcher.extract_search_tags(search_query)
90 90
91 91 if not search_result['error']:
92 92 execution_time = '%s results (%.4f seconds)' % (
93 93 search_result['count'],
94 94 search_result['runtime'])
95 95 elif not errors:
96 96 node = schema['search_query']
97 97 errors = [
98 98 validation_schema.Invalid(node, search_result['error'])]
99 99
100 100 c.perm_user = c.auth_user
101 101 c.repo_name = repo_name
102 102 c.repo_group_name = repo_group_name
103 103 c.url_generator = url_generator
104 104 c.errors = errors
105 105 c.formatted_results = formatted_results
106 106 c.runtime = execution_time
107 107 c.cur_query = search_query
108 108 c.search_type = search_type
109 109 c.searcher = searcher
110 110 c.search_tags = search_tags
111 111
112 112 direction, sort_field = searcher.get_sort(search_type, search_sort)
113 c.sort = '{}:{}'.format(direction, sort_field)
114 c.sort_tag = sort_field
113 sort_definition = searcher.sort_def(search_type, direction, sort_field)
114 c.sort = ''
115 c.sort_tag = None
115 116 c.sort_tag_dir = direction
117 if sort_definition:
118 c.sort = '{}:{}'.format(direction, sort_field)
119 c.sort_tag = sort_field
116 120
117 121
118 122 class SearchView(BaseAppView):
119 123 def load_default_context(self):
120 124 c = self._get_local_tmpl_context()
121 125 return c
122 126
123 127 @LoginRequired()
124 128 @view_config(
125 129 route_name='search', request_method='GET',
126 130 renderer='rhodecode:templates/search/search.mako')
127 131 def search(self):
128 132 c = self.load_default_context()
129 133 perform_search(self.request, c)
130 134 return self._get_template_context(c)
131 135
132 136
133 137 class SearchRepoView(RepoAppView):
134 138 def load_default_context(self):
135 139 c = self._get_local_tmpl_context()
136 140 c.active = 'search'
137 141 return c
138 142
139 143 @LoginRequired()
140 144 @HasRepoPermissionAnyDecorator(
141 145 'repository.read', 'repository.write', 'repository.admin')
142 146 @view_config(
143 147 route_name='search_repo', request_method='GET',
144 148 renderer='rhodecode:templates/search/search.mako')
145 149 @view_config(
146 150 route_name='search_repo_alt', request_method='GET',
147 151 renderer='rhodecode:templates/search/search.mako')
148 152 def search_repo(self):
149 153 c = self.load_default_context()
150 154 perform_search(self.request, c, repo_name=self.db_repo_name)
151 155 return self._get_template_context(c)
152 156
153 157
154 158 class SearchRepoGroupView(RepoGroupAppView):
155 159 def load_default_context(self):
156 160 c = self._get_local_tmpl_context()
157 161 c.active = 'search'
158 162 return c
159 163
160 164 @LoginRequired()
161 165 @HasRepoGroupPermissionAnyDecorator(
162 166 'group.read', 'group.write', 'group.admin')
163 167 @view_config(
164 168 route_name='search_repo_group', request_method='GET',
165 169 renderer='rhodecode:templates/search/search.mako')
166 170 def search_repo_group(self):
167 171 c = self.load_default_context()
168 172 perform_search(self.request, c, repo_group_name=self.db_repo_group_name)
169 173 return self._get_template_context(c)
@@ -1,143 +1,152 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Index schema for RhodeCode
23 23 """
24 24
25 25 import importlib
26 26 import logging
27 27
28 28 from rhodecode.lib.index.search_utils import normalize_text_for_matching
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32 # leave defaults for backward compat
33 33 default_searcher = 'rhodecode.lib.index.whoosh'
34 34 default_location = '%(here)s/data/index'
35 35
36 36 ES_VERSION_2 = '2'
37 37 ES_VERSION_6 = '6'
38 38 # for legacy reasons we keep 2 compat as default
39 39 DEFAULT_ES_VERSION = ES_VERSION_2
40 40
41 41 from rhodecode_tools.lib.fts_index.elasticsearch_engine_6 import \
42 42 ES_CONFIG # pragma: no cover
43 43
44 44
45 45 class BaseSearcher(object):
46 46 query_lang_doc = ''
47 47 es_version = None
48 48 name = None
49 49 DIRECTION_ASC = 'asc'
50 50 DIRECTION_DESC = 'desc'
51 51
52 52 def __init__(self):
53 53 pass
54 54
55 55 def cleanup(self):
56 56 pass
57 57
58 58 def search(self, query, document_type, search_user,
59 59 repo_name=None, repo_group_name=None,
60 60 raise_on_exc=True):
61 61 raise Exception('NotImplemented')
62 62
63 63 @staticmethod
64 64 def query_to_mark(query, default_field=None):
65 65 """
66 66 Formats the query to mark token for jquery.mark.js highlighting. ES could
67 67 have a different format optionally.
68 68
69 69 :param default_field:
70 70 :param query:
71 71 """
72 72 return ' '.join(normalize_text_for_matching(query).split())
73 73
74 74 @property
75 75 def is_es_6(self):
76 76 return self.es_version == ES_VERSION_6
77 77
78 78 def get_handlers(self):
79 79 return {}
80 80
81 81 @staticmethod
82 82 def extract_search_tags(query):
83 83 return []
84 84
85 85 @staticmethod
86 86 def escape_specials(val):
87 87 """
88 88 Handle and escape reserved chars for search
89 89 """
90 90 return val
91 91
92 def sort_def(self, search_type, direction, sort_field):
93 """
94 Defines sorting for search. This function should decide if for given
95 search_type, sorting can be done with sort_field.
96
97 It also should translate common sort fields into backend specific. e.g elasticsearch
98 """
99 raise NotImplementedError()
100
92 101 @staticmethod
93 102 def get_sort(search_type, search_val):
94 103 """
95 104 Method used to parse the GET search sort value to a field and direction.
96 105 e.g asc:lines == asc, lines
97 106
98 107 There's also a legacy support for newfirst/oldfirst which defines commit
99 108 sorting only
100 109 """
101 110
102 111 direction = BaseSearcher.DIRECTION_ASC
103 112 sort_field = None
104 113
105 114 if not search_val:
106 115 return direction, sort_field
107 116
108 117 if search_val.startswith('asc:'):
109 118 sort_field = search_val[4:]
110 119 direction = BaseSearcher.DIRECTION_ASC
111 120 elif search_val.startswith('desc:'):
112 121 sort_field = search_val[5:]
113 122 direction = BaseSearcher.DIRECTION_DESC
114 123 elif search_val == 'newfirst' and search_type == 'commit':
115 124 sort_field = 'date'
116 125 direction = BaseSearcher.DIRECTION_DESC
117 126 elif search_val == 'oldfirst' and search_type == 'commit':
118 127 sort_field = 'date'
119 128 direction = BaseSearcher.DIRECTION_ASC
120 129
121 130 return direction, sort_field
122 131
123 132
124 133 def search_config(config, prefix='search.'):
125 134 _config = {}
126 135 for key in config.keys():
127 136 if key.startswith(prefix):
128 137 _config[key[len(prefix):]] = config[key]
129 138 return _config
130 139
131 140
132 141 def searcher_from_config(config, prefix='search.'):
133 142 _config = search_config(config, prefix)
134 143
135 144 if 'location' not in _config:
136 145 _config['location'] = default_location
137 146 if 'es_version' not in _config:
138 147 # use old legacy ES version set to 2
139 148 _config['es_version'] = '2'
140 149
141 150 imported = importlib.import_module(_config.get('module', default_searcher))
142 151 searcher = imported.Searcher(config=_config)
143 152 return searcher
@@ -1,293 +1,311 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Index schema for RhodeCode
23 23 """
24 24
25 25 from __future__ import absolute_import
26 26 import os
27 27 import re
28 28 import logging
29 29
30 30 from whoosh import query as query_lib
31 31 from whoosh.highlight import HtmlFormatter, ContextFragmenter
32 32 from whoosh.index import create_in, open_dir, exists_in, EmptyIndexError
33 33 from whoosh.qparser import QueryParser, QueryParserError
34 34
35 35 import rhodecode.lib.helpers as h
36 36 from rhodecode.lib.index import BaseSearcher
37 37 from rhodecode.lib.utils2 import safe_unicode
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 try:
43 43 # we first try to import from rhodecode tools, fallback to copies if
44 44 # we're unable to
45 45 from rhodecode_tools.lib.fts_index.whoosh_schema import (
46 46 ANALYZER, FILE_INDEX_NAME, FILE_SCHEMA, COMMIT_INDEX_NAME,
47 47 COMMIT_SCHEMA)
48 48 except ImportError:
49 49 log.warning('rhodecode_tools schema not available, doing a fallback '
50 50 'import from `rhodecode.lib.index.whoosh_fallback_schema`')
51 51 from rhodecode.lib.index.whoosh_fallback_schema import (
52 52 ANALYZER, FILE_INDEX_NAME, FILE_SCHEMA, COMMIT_INDEX_NAME,
53 53 COMMIT_SCHEMA)
54 54
55 55
56 56 FORMATTER = HtmlFormatter('span', between='\n<span class="break">...</span>\n')
57 57 FRAGMENTER = ContextFragmenter(200)
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 class WhooshSearcher(BaseSearcher):
63 63 # this also shows in UI
64 64 query_lang_doc = 'http://whoosh.readthedocs.io/en/latest/querylang.html'
65 65 name = 'whoosh'
66 66
67 67 def __init__(self, config):
68 68 super(Searcher, self).__init__()
69 69 self.config = config
70 70 if not os.path.isdir(self.config['location']):
71 71 os.makedirs(self.config['location'])
72 72
73 73 opener = create_in
74 74 if exists_in(self.config['location'], indexname=FILE_INDEX_NAME):
75 75 opener = open_dir
76 76 file_index = opener(self.config['location'], schema=FILE_SCHEMA,
77 77 indexname=FILE_INDEX_NAME)
78 78
79 79 opener = create_in
80 80 if exists_in(self.config['location'], indexname=COMMIT_INDEX_NAME):
81 81 opener = open_dir
82 82 changeset_index = opener(self.config['location'], schema=COMMIT_SCHEMA,
83 83 indexname=COMMIT_INDEX_NAME)
84 84
85 85 self.commit_schema = COMMIT_SCHEMA
86 86 self.commit_index = changeset_index
87 87 self.file_schema = FILE_SCHEMA
88 88 self.file_index = file_index
89 89 self.searcher = None
90 90
91 91 def cleanup(self):
92 92 if self.searcher:
93 93 self.searcher.close()
94 94
95 95 def _extend_query(self, query):
96 96 hashes = re.compile('([0-9a-f]{5,40})').findall(query)
97 97 if hashes:
98 98 hashes_or_query = ' OR '.join('commit_id:%s*' % h for h in hashes)
99 99 query = u'(%s) OR %s' % (query, hashes_or_query)
100 100 return query
101 101
102 def sort_def(self, search_type, direction, sort_field):
103
104 if search_type == 'commit':
105 field_defs = {
106 'message': 'message',
107 'date': 'date',
108 'author_email': 'author',
109 }
110 elif search_type == 'path':
111 field_defs = {
112 'file': 'path',
113 'size': 'size',
114 'lines': 'lines',
115 }
116 elif search_type == 'content':
117 # NOTE(dan): content doesn't support any sorting
118 field_defs = {}
119 else:
120 return ''
121
122 if sort_field in field_defs:
123 return field_defs[sort_field]
124
102 125 def search(self, query, document_type, search_user,
103 126 repo_name=None, repo_group_name=None,
104 127 requested_page=1, page_limit=10, sort=None, raise_on_exc=True):
105 128
106 129 original_query = query
107 130 query = self._extend_query(query)
108 131
109 132 log.debug(u'QUERY: %s on %s', query, document_type)
110 133 result = {
111 134 'results': [],
112 135 'count': 0,
113 136 'error': None,
114 137 'runtime': 0
115 138 }
116 139 search_type, index_name, schema_defn = self._prepare_for_search(
117 140 document_type)
118 141 self._init_searcher(index_name)
119 142 try:
120 143 qp = QueryParser(search_type, schema=schema_defn)
121 144 allowed_repos_filter = self._get_repo_filter(
122 145 search_user, repo_name)
123 146 try:
124 147 query = qp.parse(safe_unicode(query))
125 148 log.debug('query: %s (%s)', query, repr(query))
126 149
127 def sort_def(_direction, _sort_field):
128 field2whoosh = {
129 'message.raw': 'message',
130 'author.email.raw': 'author',
131 }
132 return field2whoosh.get(_sort_field) or _sort_field
133
134 150 reverse, sorted_by = False, None
135 151 direction, sort_field = self.get_sort(search_type, sort)
136 152 if sort_field:
137 if direction == Searcher.DIRECTION_DESC:
138 reverse = True
139 if direction == Searcher.DIRECTION_ASC:
140 reverse = False
141 sorted_by = sort_def(direction, sort_field)
153 sort_definition = self.sort_def(search_type, direction, sort_field)
154 if sort_definition:
155 sorted_by = sort_definition
156 if direction == Searcher.DIRECTION_DESC:
157 reverse = True
158 if direction == Searcher.DIRECTION_ASC:
159 reverse = False
142 160
143 161 whoosh_results = self.searcher.search(
144 162 query, filter=allowed_repos_filter, limit=None,
145 163 sortedby=sorted_by, reverse=reverse)
146 164
147 165 # fixes for 32k limit that whoosh uses for highlight
148 166 whoosh_results.fragmenter.charlimit = None
149 167 res_ln = whoosh_results.scored_length()
150 168 result['runtime'] = whoosh_results.runtime
151 169 result['count'] = res_ln
152 170 result['results'] = WhooshResultWrapper(
153 171 search_type, res_ln, whoosh_results)
154 172
155 173 except QueryParserError:
156 174 result['error'] = 'Invalid search query. Try quoting it.'
157 175 except (EmptyIndexError, IOError, OSError):
158 176 msg = 'There is no index to search in. Please run whoosh indexer'
159 177 log.exception(msg)
160 178 result['error'] = msg
161 179 except Exception:
162 180 msg = 'An error occurred during this search operation'
163 181 log.exception(msg)
164 182 result['error'] = msg
165 183
166 184 return result
167 185
168 186 def statistics(self, translator):
169 187 _ = translator
170 188 stats = [
171 189 {'key': _('Index Type'), 'value': 'Whoosh'},
172 190 {'sep': True},
173 191
174 192 {'key': _('File Index'), 'value': str(self.file_index)},
175 193 {'key': _('Indexed documents'), 'value': self.file_index.doc_count()},
176 194 {'key': _('Last update'), 'value': h.time_to_datetime(self.file_index.last_modified())},
177 195
178 196 {'sep': True},
179 197
180 198 {'key': _('Commit index'), 'value': str(self.commit_index)},
181 199 {'key': _('Indexed documents'), 'value': str(self.commit_index.doc_count())},
182 200 {'key': _('Last update'), 'value': h.time_to_datetime(self.commit_index.last_modified())}
183 201 ]
184 202 return stats
185 203
186 204 def _get_repo_filter(self, auth_user, repo_name):
187 205
188 206 allowed_to_search = [
189 207 repo for repo, perm in
190 208 auth_user.permissions['repositories'].items()
191 209 if perm != 'repository.none']
192 210
193 211 if repo_name:
194 212 repo_filter = [query_lib.Term('repository', repo_name)]
195 213
196 214 elif 'hg.admin' in auth_user.permissions.get('global', []):
197 215 return None
198 216
199 217 else:
200 218 repo_filter = [query_lib.Term('repository', _rn)
201 219 for _rn in allowed_to_search]
202 220 # in case we're not allowed to search anywhere, it's a trick
203 221 # to tell whoosh we're filtering, on ALL results
204 222 repo_filter = repo_filter or [query_lib.Term('repository', '')]
205 223
206 224 return query_lib.Or(repo_filter)
207 225
208 226 def _prepare_for_search(self, cur_type):
209 227 search_type = {
210 228 'content': 'content',
211 229 'commit': 'message',
212 230 'path': 'path',
213 231 'repository': 'repository'
214 232 }.get(cur_type, 'content')
215 233
216 234 index_name = {
217 235 'content': FILE_INDEX_NAME,
218 236 'commit': COMMIT_INDEX_NAME,
219 237 'path': FILE_INDEX_NAME
220 238 }.get(cur_type, FILE_INDEX_NAME)
221 239
222 240 schema_defn = {
223 241 'content': self.file_schema,
224 242 'commit': self.commit_schema,
225 243 'path': self.file_schema
226 244 }.get(cur_type, self.file_schema)
227 245
228 246 log.debug('IDX: %s', index_name)
229 247 log.debug('SCHEMA: %s', schema_defn)
230 248 return search_type, index_name, schema_defn
231 249
232 250 def _init_searcher(self, index_name):
233 251 idx = open_dir(self.config['location'], indexname=index_name)
234 252 self.searcher = idx.searcher()
235 253 return self.searcher
236 254
237 255
238 256 Searcher = WhooshSearcher
239 257
240 258
241 259 class WhooshResultWrapper(object):
242 260 def __init__(self, search_type, total_hits, results):
243 261 self.search_type = search_type
244 262 self.results = results
245 263 self.total_hits = total_hits
246 264
247 265 def __str__(self):
248 266 return '<%s at %s>' % (self.__class__.__name__, len(self))
249 267
250 268 def __repr__(self):
251 269 return self.__str__()
252 270
253 271 def __len__(self):
254 272 return self.total_hits
255 273
256 274 def __iter__(self):
257 275 """
258 276 Allows Iteration over results,and lazy generate content
259 277
260 278 *Requires* implementation of ``__getitem__`` method.
261 279 """
262 280 for hit in self.results:
263 281 yield self.get_full_content(hit)
264 282
265 283 def __getitem__(self, key):
266 284 """
267 285 Slicing of resultWrapper
268 286 """
269 287 i, j = key.start, key.stop
270 288 for hit in self.results[i:j]:
271 289 yield self.get_full_content(hit)
272 290
273 291 def get_full_content(self, hit):
274 292 # TODO: marcink: this feels like an overkill, there's a lot of data
275 293 # inside hit object, and we don't need all
276 294 res = dict(hit)
277 295 # elastic search uses that, we set it empty so it fallbacks to regular HL logic
278 296 res['content_highlight'] = ''
279 297
280 298 f_path = '' # pragma: no cover
281 299 if self.search_type in ['content', 'path']:
282 300 f_path = res['path'][len(res['repository']):]
283 301 f_path = f_path.lstrip(os.sep)
284 302
285 303 if self.search_type == 'content':
286 304 res.update({'content_short_hl': hit.highlights('content'),
287 305 'f_path': f_path})
288 306 elif self.search_type == 'path':
289 307 res.update({'f_path': f_path})
290 308 elif self.search_type == 'message':
291 309 res.update({'message_hl': hit.highlights('message')})
292 310
293 311 return res
@@ -1,241 +1,241 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 %if c.repo_name:
6 6 ${_('Search inside repository {repo_name}').format(repo_name=c.repo_name)}
7 7 %elif c.repo_group_name:
8 8 ${_('Search inside repository group {repo_group_name}').format(repo_group_name=c.repo_group_name)}
9 9 %else:
10 10 ${_('Search inside all accessible repositories')}
11 11 %endif
12 12 %if c.rhodecode_name:
13 13 &middot; ${h.branding(c.rhodecode_name)}
14 14 %endif
15 15 </%def>
16 16
17 17 <%def name="breadcrumbs_links()">
18 18 %if c.repo_name:
19 19 ${_('Search inside repository {repo_name}').format(repo_name=c.repo_name)}
20 20 %elif c.repo_group_name:
21 21 ${_('Search inside repository group {repo_group_name}').format(repo_group_name=c.repo_group_name)}
22 22 %else:
23 23 ${_('Search inside all accessible repositories')}
24 24 %endif
25 25
26 26 </%def>
27 27
28 28 <%def name="menu_bar_nav()">
29 29 %if c.repo_name:
30 30 ${self.menu_items(active='search')}
31 31 %elif c.repo_group_name:
32 32 ${self.menu_items(active='search')}
33 33 %else:
34 34 ${self.menu_items(active='search')}
35 35 %endif
36 36 </%def>
37 37
38 38 <%def name="menu_bar_subnav()">
39 39 %if c.repo_name:
40 40 <% active_entry = {'content':'files', 'path':'files', 'commit':'commits'}.get(c.search_type, 'summary')%>
41 41 ${self.repo_menu(active=active_entry)}
42 42 %elif c.repo_group_name:
43 43 ${self.repo_group_menu(active='home')}
44 44 %endif
45 45 </%def>
46 46
47 47 <%def name="repo_icon(db_repo)">
48 48 %if h.is_hg(db_repo):
49 49 <i class="icon-hg"></i>
50 50 %endif
51 51 %if h.is_git(db_repo):
52 52 <i class="icon-git"></i>
53 53 %endif
54 54 %if h.is_svn(db_repo):
55 55 <i class="icon-svn"></i>
56 56 %endif
57 57 </%def>
58 58
59 59 <%def name="repo_group_icon()">
60 60 <i class="icon-repo-group"></i>
61 61 </%def>
62 62
63 63
64 64 <%def name="field_sort(field_name)">
65 65
66 66 <%
67 67 if c.sort.startswith('asc:'):
68 68 return c.url_generator(sort='desc:{}'.format(field_name))
69 69 elif c.sort.startswith('desc:'):
70 70 return c.url_generator(sort='asc:{}'.format(field_name))
71 71
72 return 'asc:{}'.format(field_name)
72 return c.url_generator(sort='asc:{}'.format(field_name))
73 73 %>
74 74 </%def>
75 75
76 76
77 77 <%def name="main()">
78 78 <div class="box">
79 79 %if c.repo_name:
80 80 <!-- box / title -->
81 81 ${h.form(h.route_path('search_repo',repo_name=c.repo_name),method='get')}
82 82 %elif c.repo_group_name:
83 83 <!-- box / title -->
84 84 ${h.form(h.route_path('search_repo_group',repo_group_name=c.repo_group_name),method='get')}
85 85 %else:
86 86 <!-- box / title -->
87 87 <div class="title">
88 88 ${self.breadcrumbs()}
89 89 <ul class="links">&nbsp;</ul>
90 90 </div>
91 91 <!-- end box / title -->
92 92 ${h.form(h.route_path('search'), method='get')}
93 93 %endif
94 94 <div class="form search-form">
95 95 <div class="fields">
96 96
97 97 ${h.text('q', c.cur_query, placeholder="Enter query...")}
98 98
99 99 ${h.select('type',c.search_type,[('content',_('Files')), ('path',_('File path')),('commit',_('Commits'))],id='id_search_type')}
100 100 ${h.hidden('max_lines', '10')}
101 101
102 102 <input type="submit" value="${_('Search')}" class="btn"/>
103 103 <br/>
104 104
105 105 <div class="search-tags">
106 106 <span class="tag tag8">
107 107 %if c.repo_name:
108 108 <a href="${h.route_path('search', _query={'q': c.cur_query, 'type': request.GET.get('type', 'content')})}">${_('Global Search')}</a>
109 109 %elif c.repo_group_name:
110 110 <a href="${h.route_path('search', _query={'q': c.cur_query, 'type': request.GET.get('type', 'content')})}">${_('Global Search')}</a>
111 111 % else:
112 112 ${_('Global Search')}
113 113 %endif
114 114 </span>
115 115
116 116 %if c.repo_name:
117 117 Β»
118 118 <span class="tag tag8">
119 119 ${repo_icon(c.rhodecode_db_repo)}
120 120 ${c.repo_name}
121 121 </span>
122 122
123 123 %elif c.repo_group_name:
124 124 Β»
125 125 <span class="tag tag8">
126 126 ${repo_group_icon()}
127 127 ${c.repo_group_name}
128 128 </span>
129 129 %endif
130 130
131 131 % if c.sort_tag:
132 132 <span class="tag tag8">
133 133 % if c.sort_tag_dir == 'asc':
134 134 <i class="icon-angle-down"></i>
135 135 % elif c.sort_tag_dir == 'desc':
136 136 <i class="icon-angle-up"></i>
137 137 % endif
138 138 ${_('sort')}:${c.sort_tag}
139 139 </span>
140 140 % endif
141 141
142 142 % for search_tag in c.search_tags:
143 143 <br/><span class="tag disabled" style="margin-top: 3px">${search_tag}</span>
144 144 % endfor
145 145
146 146 </div>
147 147
148 148 <div class="search-feedback-items">
149 149 % for error in c.errors:
150 150 <span class="error-message">
151 151 % for k,v in error.asdict().items():
152 152 ${k} - ${v}
153 153 % endfor
154 154 </span>
155 155 % endfor
156 156 <div class="field">
157 157 <p class="filterexample" style="position: inherit" onclick="$('#search-help').toggle()">${_('Query Language examples')}</p>
158 158 <pre id="search-help" style="display: none">\
159 159
160 160 % if c.searcher.name == 'whoosh':
161 161 Example filter terms for `Whoosh` search:
162 162 query lang: <a href="${c.searcher.query_lang_doc}">Whoosh Query Language</a>
163 163 Whoosh has limited query capabilities. For advanced search use ElasticSearch 6 from RhodeCode EE edition.
164 164
165 165 Generate wildcards using '*' character:
166 166 "repo_name:vcs*" - search everything starting with 'vcs'
167 167 "repo_name:*vcs*" - search for repository containing 'vcs'
168 168
169 169 Optional AND / OR operators in queries
170 170 "repo_name:vcs OR repo_name:test"
171 171 "owner:test AND repo_name:test*" AND extension:py
172 172
173 173 Move advanced search is available via ElasticSearch6 backend in EE edition.
174 174 % elif c.searcher.name == 'elasticsearch' and c.searcher.es_version == '2':
175 175 Example filter terms for `ElasticSearch-${c.searcher.es_version}`search:
176 176 ElasticSearch-2 has limited query capabilities. For advanced search use ElasticSearch 6 from RhodeCode EE edition.
177 177
178 178 search type: content (File Content)
179 179 indexed fields: content
180 180
181 181 # search for `fix` string in all files
182 182 fix
183 183
184 184 search type: commit (Commit message)
185 185 indexed fields: message
186 186
187 187 search type: path (File name)
188 188 indexed fields: path
189 189
190 190 % else:
191 191 Example filter terms for `ElasticSearch-${c.searcher.es_version}`search:
192 192 query lang: <a href="${c.searcher.query_lang_doc}">ES 6 Query Language</a>
193 193 The reserved characters needed espace by `\`: + - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /
194 194 % for handler in c.searcher.get_handlers().values():
195 195
196 196 search type: ${handler.search_type_label}
197 197 *indexed fields*: ${', '.join( [('\n ' if x[0]%4==0 else '')+x[1] for x in enumerate(handler.es_6_field_names)])}
198 198 % for entry in handler.es_6_example_queries:
199 199 ${entry.rstrip()}
200 200 % endfor
201 201 % endfor
202 202
203 203 % endif
204 204 </pre>
205 205 </div>
206 206
207 207 <div class="field">${c.runtime}</div>
208 208 </div>
209 209 </div>
210 210 </div>
211 211
212 212 ${h.end_form()}
213 213 <div class="search">
214 214 % if c.search_type == 'content':
215 215 <%include file='search_content.mako'/>
216 216 % elif c.search_type == 'path':
217 217 <%include file='search_path.mako'/>
218 218 % elif c.search_type == 'commit':
219 219 <%include file='search_commit.mako'/>
220 220 % elif c.search_type == 'repository':
221 221 <%include file='search_repository.mako'/>
222 222 % endif
223 223 </div>
224 224 </div>
225 225 <script>
226 226 $(document).ready(function(){
227 227 $("#id_search_type").select2({
228 228 'containerCssClass': "drop-menu",
229 229 'dropdownCssClass': "drop-menu-dropdown",
230 230 'dropdownAutoWidth': true,
231 231 'minimumResultsForSearch': -1
232 232 });
233 233
234 234 $('#q').autoGrowInput({maxWidth: 920});
235 235
236 236 setTimeout(function() {
237 237 $('#q').keyup()
238 238 }, 1);
239 239 })
240 240 </script>
241 241 </%def>
@@ -1,98 +1,98 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2 <%namespace name="search" file="/search/search.mako"/>
3 3
4 4 % if c.formatted_results:
5 5
6 6 <table class="rctable search-results">
7 7 <tr>
8 8 <th>${_('Repository')}</th>
9 9 <th>${_('Commit')}</th>
10 10 <th></th>
11 11 <th>
12 <a href="${search.field_sort('message.raw')}">${_('Commit message')}</a>
12 <a href="${search.field_sort('message')}">${_('Commit message')}</a>
13 13 </th>
14 14 <th>
15 <a href="${search.field_sort('date')}">${_('Age')}</a>
15 <a href="${search.field_sort('date')}">${_('Commit date')}</a>
16 16 </th>
17 17 <th>
18 <a href="${search.field_sort('author.email.raw')}">${_('Author')}</a>
18 <a href="${search.field_sort('author_email')}">${_('Author')}</a>
19 19 </th>
20 20 </tr>
21 21 %for entry in c.formatted_results:
22 22 ## search results are additionally filtered, and this check is just a safe gate
23 23 % if c.rhodecode_user.is_admin or h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results commit check'):
24 24 <tr class="body">
25 25 <td class="td-componentname">
26 26 <% repo_type = entry.get('repo_type') or h.get_repo_type_by_name(entry.get('repository')) %>
27 27 ${search.repo_icon(repo_type)}
28 28 ${h.link_to(entry['repository'], h.route_path('repo_summary',repo_name=entry['repository']))}
29 29 </td>
30 30 <td class="td-hash">
31 31 ${h.link_to(h._shorten_commit_id(entry['commit_id']),
32 32 h.route_path('repo_commit',repo_name=entry['repository'],commit_id=entry['commit_id']))}
33 33 </td>
34 34 <td class="td-message expand_commit search open" data-commit-id="${h.md5_safe(entry['repository'])+entry['commit_id']}" id="t-${h.md5_safe(entry['repository'])+entry['commit_id']}" title="${_('Expand commit message')}">
35 35 <div>
36 36 <i class="icon-expand-linked"></i>&nbsp;
37 37 </div>
38 38 </td>
39 39 <td data-commit-id="${h.md5_safe(entry['repository'])+entry['commit_id']}" id="c-${h.md5_safe(entry['repository'])+entry['commit_id']}" class="message td-description open">
40 40 %if entry.get('message_hl'):
41 41 ${h.literal(entry['message_hl'])}
42 42 %else:
43 43 ${h.urlify_commit_message(entry['message'], entry['repository'])}
44 44 %endif
45 45 </td>
46 46 <td class="td-time">
47 47 ${h.age_component(h.time_to_utcdatetime(entry['date']))}
48 48 </td>
49 49
50 50 <td class="td-user author">
51 51 <%
52 52 ## es6 stores this as object
53 53 author = entry['author']
54 54 if isinstance(author, dict):
55 55 author = author['email']
56 56 %>
57 57 ${base.gravatar_with_user(author)}
58 58 </td>
59 59 </tr>
60 60 % endif
61 61 %endfor
62 62 </table>
63 63
64 64 %if c.cur_query:
65 65 <div class="pagination-wh pagination-left">
66 66 ${c.formatted_results.pager('$link_previous ~2~ $link_next')}
67 67 </div>
68 68 %endif
69 69
70 70 <script>
71 71 $('.expand_commit').on('click',function(e){
72 72 var target_expand = $(this);
73 73 var cid = target_expand.data('commit-id');
74 74
75 75 if (target_expand.hasClass('open')){
76 76 $('#c-'+cid).css({'height': '1.5em', 'white-space': 'nowrap', 'text-overflow': 'ellipsis', 'overflow':'hidden'});
77 77 $('#t-'+cid).css({'height': 'auto', 'line-height': '.9em', 'text-overflow': 'ellipsis', 'overflow':'hidden'});
78 78 target_expand.removeClass('open');
79 79 }
80 80 else {
81 81 $('#c-'+cid).css({'height': 'auto', 'white-space': 'normal', 'text-overflow': 'initial', 'overflow':'visible'});
82 82 $('#t-'+cid).css({'height': 'auto', 'max-height': 'none', 'text-overflow': 'initial', 'overflow':'visible'});
83 83 target_expand.addClass('open');
84 84 }
85 85 });
86 86
87 87 $(".message.td-description").mark(
88 88 "${c.searcher.query_to_mark(c.cur_query, 'message')}",
89 89 {
90 90 "className": 'match',
91 91 "accuracy": "complementary",
92 92 "ignorePunctuation": ":._(){}[]!'+=".split("")
93 93 }
94 94 );
95 95
96 96 </script>
97 97
98 98 % endif
@@ -1,53 +1,53 b''
1 1 <%namespace name="search" file="/search/search.mako"/>
2 2
3 3 % if c.formatted_results:
4 4
5 5 <table class="rctable search-results">
6 6 <tr>
7 7 <th>${_('Repository')}</th>
8 8 <th>
9 <a href="${search.field_sort('file.raw')}">${_('File')}</a>
9 <a href="${search.field_sort('file')}">${_('File')}</a>
10 10 </th>
11 11 <th>
12 12 <a href="${search.field_sort('size')}">${_('Size')}</a>
13 13 </th>
14 14 <th>
15 15 <a href="${search.field_sort('lines')}">${_('Lines')}</a>
16 16 </th>
17 17 </tr>
18 18 %for entry in c.formatted_results:
19 19 ## search results are additionally filtered, and this check is just a safe gate
20 20 % if c.rhodecode_user.is_admin or h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results path check'):
21 21 <tr class="body">
22 22 <td class="td-componentname">
23 23 <% repo_type = entry.get('repo_type') or h.get_repo_type_by_name(entry.get('repository')) %>
24 24 ${search.repo_icon(repo_type)}
25 25 ${h.link_to(entry['repository'], h.route_path('repo_summary',repo_name=entry['repository']))}
26 26 </td>
27 27 <td class="td-componentname">
28 28 <i class="icon-file"></i>
29 29 ${h.link_to(h.literal(entry['f_path']),
30 30 h.route_path('repo_files',repo_name=entry['repository'],commit_id='tip',f_path=entry['f_path']))}
31 31 </td>
32 32 <td>
33 33 %if entry.get('size'):
34 34 ${h.format_byte_size_binary(entry['size'])}
35 35 %endif
36 36 </td>
37 37 <td>
38 38 %if entry.get('lines'):
39 39 ${entry.get('lines', 0.)}
40 40 %endif
41 41 </td>
42 42 </tr>
43 43 % endif
44 44 %endfor
45 45 </table>
46 46
47 47 %if c.cur_query:
48 48 <div class="pagination-wh pagination-left">
49 49 ${c.formatted_results.pager('$link_previous ~2~ $link_next')}
50 50 </div>
51 51 %endif
52 52
53 53 % endif
General Comments 0
You need to be logged in to leave comments. Login now