search: remove whoosh, and add support for ES 8 & 9

This commit is contained in:
RhodeCode Admin 2025-06-02 13:50:04 +02:00
parent aa989aeadd
commit b6c2ac4521
7 changed files with 88 additions and 116 deletions

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>