Merge pull request !2749 from rhodecode-enterprise-ce-workspace-fork main

Changes from branch: Main
This commit is contained in:
Andrii Verbytskyi 2025-06-02 16:27:35 +00:00
commit bad45bbc4a
7 changed files with 88 additions and 116 deletions

View file

@ -315,9 +315,7 @@ Enabling ElasticSearch
^^^^^^^^^^^^^^^^^^^^^^
ElasticSearch is available in EE edition only. It provides much scalable and more advanced
search capabilities. While Whoosh is fine for upto 1-2GB of data, beyond that amount it
starts slowing down, and can cause other problems.
New ElasticSearch 6 also provides much more advanced query language.
search capabilities. New ElasticSearch also provides much more advanced query language.
It allows advanced filtering by file paths, extensions, use OR statements, ranges etc.
Please check query language examples in the search field for some advanced query language usage.
@ -325,25 +323,15 @@ Please check query language examples in the search field for some advanced query
1. Open the :file:`rhodecode.ini` file for the instance you wish to edit. The
default location is
:file:`home/{user}/.rccontrol/{instance-id}/rhodecode.ini`
2. Find the search configuration section:
.. code-block:: ini
###################################
## SEARCH INDEXING CONFIGURATION ##
###################################
search.module = rhodecode.lib.index.whoosh
search.location = %(here)s/data/index
and change it to:
2. Find the search configuration section and change it to:
.. code-block:: ini
search.module = rc_elasticsearch
search.location = http://localhost:9200
## specify Elastic Search version, 6 for latest or 2 for legacy
search.es_version = 6
search.location = http://elasticsearch:9200
## specify Elastic Search version, 8 for latest
search.es_version = 8
where ``search.location`` points to the ElasticSearch server
by default running on port 9200.
@ -352,8 +340,7 @@ Index invocation also needs change. Please provide --es-version= and
--engine-location= parameters to define ElasticSearch server location and it's version.
For example::
rhodecode-index --instace-name=enterprise-1 --es-version=6 --engine-location=http://localhost:9200
rhodecode-index --instance-name=enterprise-1 --es-version=8 --engine-location=http://elasticsearch:9200
.. _Whoosh: https://pypi.python.org/pypi/Whoosh/
.. _ElasticSearch 6: https://www.elastic.co/

View file

@ -39,6 +39,7 @@ from rhodecode.lib import audit_logger, rc_cache, channelstream
from rhodecode.lib import repo_maintenance
from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi, HasRepoPermissionAnyApi
from rhodecode.lib.celerylib.utils import get_task_id
from rhodecode.lib.str_utils import safe_bytes
from rhodecode.lib.utils2 import str2bool, time_to_datetime, safe_str, safe_int
from rhodecode.lib.ext_json import json
from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
@ -615,7 +616,7 @@ def get_repo_file(
elif details == "full":
extended_info = content = True
file_path = safe_str(file_path)
bytes_path = safe_bytes(file_path)
try:
# check if repo is not empty by any chance, skip quicker if it is.
_scm = repo.scm_instance()
@ -625,7 +626,7 @@ def get_repo_file(
node = ScmModel().get_node(
repo,
commit_id,
file_path,
bytes_path,
extended_info=extended_info,
content=content,
max_file_bytes=max_file_bytes,

View file

@ -483,7 +483,7 @@ class HomeView(BaseAppView, DataGridAppView):
if not searcher:
return []
is_es_6 = searcher.is_es_6
is_es_8 = searcher.is_es_8
queries = []
repo_group_name, repo_name, repo_context = None, None, None
@ -495,7 +495,7 @@ class HomeView(BaseAppView, DataGridAppView):
repo_name = search_context.get("search_context[repo_name]")
repo_context = search_context.get("search_context[repo_view_type]")
if is_es_6 and repo_name:
if is_es_8 and repo_name:
# files
def query_modifier():
qry = query
@ -535,7 +535,7 @@ class HomeView(BaseAppView, DataGridAppView):
else:
queries.extend([commit_qry, file_qry])
elif is_es_6 and repo_group_name:
elif is_es_8 and repo_group_name:
# files
def query_modifier():
qry = query

View file

@ -90,10 +90,12 @@ def perform_search(request, tmpl_context, repo_name=None, repo_group_name=None):
sort=search_sort,
)
search_result_count = search_result["count"]["value"]
formatted_results = Page(
search_result["results"],
page=requested_page,
item_count=search_result["count"],
item_count=search_result_count,
items_per_page=page_limit,
url_maker=url_generator,
)
@ -103,7 +105,7 @@ def perform_search(request, tmpl_context, repo_name=None, repo_group_name=None):
search_tags = searcher.extract_search_tags(search_query)
if not search_result["error"]:
execution_time = "{} results ({:.4f} seconds)".format(search_result["count"], search_result["runtime"])
execution_time = "{} results ({:.4f} seconds)".format(search_result_count, search_result["runtime"])
elif not errors:
node = schema["search_query"]
errors = [validation_schema.Invalid(node, search_result["error"])]

View file

@ -28,16 +28,19 @@ from rhodecode.lib.index.search_utils import normalize_text_for_matching
log = logging.getLogger(__name__)
# leave defaults for backward compat
default_searcher = "rhodecode.lib.index.whoosh"
default_location = "%(here)s/data/index"
default_searcher = "rc_elasticsearch"
ES_VERSION_8 = 8
ES_VERSION_9 = 9
OS_VERSION_2 = 2
# for legacy reasons we keep 8 compat as default
DEFAULT_ES_VERSION = ES_VERSION_8
DEFAULT_OS_VERSION = OS_VERSION_2
ES_VERSION_2 = "2"
ES_VERSION_6 = "6"
# for legacy reasons we keep 2 compat as default
DEFAULT_ES_VERSION = ES_VERSION_2
try:
from rhodecode_tools.lib.fts_index.elasticsearch_engine_6 import ES_CONFIG # pragma: no cover
from rhodecode_tools.lib.fts_index.elasticsearch_engine_8 import ES_CONFIG # pragma: no cover
except ImportError:
log.warning("rhodecode_tools not available, use of full text search is limited")
pass
@ -71,8 +74,8 @@ class BaseSearcher(object):
return " ".join(normalize_text_for_matching(query).split())
@property
def is_es_6(self):
return self.es_version == ES_VERSION_6
def is_es_8(self):
return self.es_version >= ES_VERSION_8
def get_handlers(self):
return {}
@ -140,11 +143,9 @@ def search_config(config, prefix="search."):
def searcher_from_config(config, prefix="search."):
_config = search_config(config, prefix)
if "location" not in _config:
_config["location"] = default_location
if "es_version" not in _config:
# use an old legacy ES version set to 2
_config["es_version"] = "2"
# use an old legacy ES version set to 8
_config["es_version"] = DEFAULT_ES_VERSION
imported = importlib.import_module(_config.get("module", default_searcher))
searcher = imported.Searcher(config=_config)

View file

@ -154,53 +154,32 @@
% endfor
<div class="field">
<p class="filterexample" style="position: inherit" onclick="$('#search-help').toggle()">${_('Query Language examples')}</p>
<pre id="search-help" style="display: none">\
% if c.searcher.name == 'whoosh':
Example filter terms for `Whoosh` search:
query lang: <a href="${c.searcher.query_lang_doc}">Whoosh Query Language</a>
Whoosh has limited query capabilities. For advanced search use ElasticSearch 6 from RhodeCode EE edition.
<div id="search-help" style="display: none">\
Generate wildcards using '*' character:
"repo_name:vcs*" - search everything starting with 'vcs'
"repo_name:*vcs*" - search for repository containing 'vcs'
% if c.searcher.name == 'elasticsearch':
Optional AND / OR operators in queries
"repo_name:vcs OR repo_name:test"
"owner:test AND repo_name:test*" AND extension:py
Move advanced search is available via ElasticSearch6 backend in EE edition.
% elif c.searcher.name == 'elasticsearch' and c.searcher.es_version == '2':
Example filter terms for `ElasticSearch-${c.searcher.es_version}`search:
ElasticSearch-2 has limited query capabilities. For advanced search use ElasticSearch 6 from RhodeCode EE edition.
<h3>Example filter terms for `ElasticSearch-${c.searcher.es_version}`search:</h3>
query lang: <a href="${c.searcher.query_lang_doc}">ES ${c.searcher.es_version} Query Language</a></br>
The reserved characters require escaping by `\`: <pre>+ - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /</pre>
</br>
search type: content (File Content)
indexed fields: content
# search for `fix` string in all files
fix
search type: commit (Commit message)
indexed fields: message
search type: path (File name)
indexed fields: path
% else:
Example filter terms for `ElasticSearch-${c.searcher.es_version}`search:
query lang: <a href="${c.searcher.query_lang_doc}">ES 6 Query Language</a>
The reserved characters needed espace by `\`: + - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /
% for handler in c.searcher.get_handlers().values():
search type: ${handler.search_type_label}
*indexed fields*: ${', '.join( [('\n ' if x[0]%4==0 else '')+x[1] for x in enumerate(handler.es_6_field_names)])}
<h4>Search type: ${handler.search_type_label}</h4>
<strong>indexed fields:</strong>
<pre>${', '.join( [('\n' if x[0]%4==0 else '')+x[1] for x in enumerate(handler.es_6_field_names)])}</pre>
<pre>
% for entry in handler.es_6_example_queries:
${entry.rstrip()}
${h.textwrap.dedent(entry.rstrip())}
% endfor
</pre>
% endfor
% endif
</pre>
</div>
</div>
<div class="field">${c.runtime}</div>

View file

@ -1,8 +1,39 @@
<%namespace name="search" file="/search/search.mako"/>
<%def name="highlight_text_file(has_matched_content, file_content, lexer, html_formatter, matching_lines, shown_matching_lines, url, use_hl_filter)">
<%def name="highlight_text_file(file_path, full_file_content, hl_file_content, mimetype, url, use_hl_filter)">
<%
matching_lines = []
shown_matching_lines = 0
if c.searcher.is_es_8:
# use empty terms so we default to markers usage
total_lines, matching_lines = h.get_matching_line_offsets(hl_file_content, terms=None)
lines_of_interest = set()
for line_number in matching_lines:
if len(lines_of_interest) < max_lines:
lines_of_interest |= set(range(
max(line_number - line_context, 0),
min(line_number + line_context, total_lines + 1)))
shown_matching_lines += 1
has_matched_content = len(lines_of_interest) >= 1
%>
% if has_matched_content:
${h.code_highlight(file_content, lexer, html_formatter, use_hl_filter=use_hl_filter)|n}
<%
lexer = h.get_lexer_safe(mimetype=mimetype, filepath=filepath)
html_formatter = h.SearchContentCodeHtmlFormatter(
linenos=True,
cssclass="code-highlight",
url=match_file_url,
query_terms=terms,
only_line_numbers=lines_of_interest
)
%>
${h.code_highlight(hl_file_content, lexer, html_formatter, use_hl_filter=use_hl_filter)|n}
% else:
${_('No content matched')} <br/>
% endif
@ -20,41 +51,15 @@
%for entry in c.formatted_results:
<%
file_content = entry['content_highlight'] or entry['content']
mimetype = entry.get('mimetype')
hl_file_content = entry['content_highlight']
full_file_content = entry['content']
filepath = entry.get('path')
mimetype = entry.get('mimetype')
max_lines = h.safe_int(request.GET.get('max_lines', '10'))
line_context = h.safe_int(request.GET.get('line_contenxt', '3'))
line_context = h.safe_int(request.GET.get('line_context', '3'))
match_file_url=h.route_path('repo_files',repo_name=entry['repository'], commit_id=entry.get('commit_id', 'tip'),f_path=entry['f_path'], _query={"mark": query_mark})
terms = c.cur_query
if c.searcher.is_es_6:
# use empty terms so we default to markers usage
total_lines, matching_lines = h.get_matching_line_offsets(file_content, terms=None)
else:
total_lines, matching_lines = h.get_matching_line_offsets(file_content, terms)
shown_matching_lines = 0
lines_of_interest = set()
for line_number in matching_lines:
if len(lines_of_interest) < max_lines:
lines_of_interest |= set(range(
max(line_number - line_context, 0),
min(line_number + line_context, total_lines + 1)))
shown_matching_lines += 1
lexer = h.get_lexer_safe(mimetype=mimetype, filepath=filepath)
html_formatter = h.SearchContentCodeHtmlFormatter(
linenos=True,
cssclass="code-highlight",
url=match_file_url,
query_terms=terms,
only_line_numbers=lines_of_interest
)
has_matched_content = len(lines_of_interest) >= 1
%>
## search results are additionally filtered, and this check is just a safe gate
% if c.rhodecode_user.is_admin or h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results content check'):
@ -75,7 +80,7 @@
<div class="stats-info">
<span class="stats-first-item">
${entry.get('lines', 0.)} ${_ungettext('line', 'lines', entry.get('lines', 0.))}
(${len(matching_lines)} ${_ungettext('matched', 'matched', len(matching_lines))})
##(${len(matching_lines)} ${_ungettext('matched', 'matched', len(matching_lines))})
</span>
<span>
% if entry.get('size'):
@ -112,7 +117,7 @@
<a href="${h.route_path('search_repo_group', repo_group_name=repo_group, _query={'q': c.cur_query})}">${_('Narrow to this repository group')}</a>
</span>
% endif
## hiden if in repo view
## hidden if in repo view
% if not c.repo_name:
<span class="tag tag8">
${search.repo_icon(repo_type)}
@ -125,17 +130,14 @@
<div class="clear-fix"></div>
</div>
<div class="code-body search-code-body clear-fix">
${highlight_text_file(
has_matched_content=has_matched_content,
file_content=file_content,
lexer=lexer,
html_formatter=html_formatter,
matching_lines=matching_lines,
shown_matching_lines=shown_matching_lines,
file_path=file_path,
full_file_content=full_file_content,
hl_file_content=hl_file_content,
mimetype=mimetype,
url=match_file_url,
use_hl_filter=c.searcher.is_es_6
use_hl_filter=c.searcher.is_es_8
)}
</div>