Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add keyword search to filter example queries #83

Merged
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions backend/static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ Custom Elements
.tooltip .tooltip-inner { background-color: #82B36F; max-width: 600px; word-wrap: break-word; }
.tooltip .tooltip-arrow { border-top-color: #82B36F !important; }

.keyword-search-highlight {
text-decoration: underline;
text-decoration-color: #82B36F;
text-underline-position: under;
text-decoration-thickness: 3px;
}

.keyword-search-hover {
color: #262626;
text-decoration: none;
background-color: #f5f5f5;
}

.table > tbody > tr > td:first-child { color: #d3d3d3; }
th { color: #82B36F; white-space: nowrap; }
Expand Down
162 changes: 162 additions & 0 deletions backend/static/js/keywordSearch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// This variable contains the actual example spans that match the query.
let exampleSpans = {};
// This varibale keeps track of the selected example.
let selectedExample = -1;


// This removes highlighting from the input string by replacing all occurrences of
// < span> elements with the class `keyword-search-highlight` with their inner content.
function unHighlight(input_str){
return input_str.replaceAll(/\<span class\=\"keyword-search-highlight\"\>(.*?)\<\/span\>/gi, "$1");
}

// This Highlights specified words or patterns within an input string by wrapping them with a <span> element
// with the class `keyword-search-highlight`.
//
// Algorithm:
// 1. Remove any existing highlighting.
// 2. Iterate over each `regex` in the list of `regexes` to find matching sections in the input string.
// 3. Consolidate overlapping sections if any.
// 4. Replace the matching sections with HTML <span> tags for highlighting.
// 5. Return the modified string with highlighted words.
function highlightWords(input_str, regexps) {
let return_str = unHighlight(input_str);
// find matching sections
let matching_sections = [];
for (regexp of regexps){
const matches = input_str.matchAll(regexp);
for (const match of matches) {
matching_sections.push([match.index, match.index + match[0].length]);
}
}
if (matching_sections.length === 0){
return return_str;
}
// consolidate overlapping sections
matching_sections.sort((a,b) => a[0] - b[0]);
matching_sections = matching_sections.reduce((accu,elem) => {
const [last, ...rest] = accu;
if (elem[0] <= last[1]){
return [[last[0], Math.max(elem[1], last[1])], ...rest]
}
return [elem].concat(accu);
}, [matching_sections[0]]);
// replace matching sections with highlighting span
matching_sections.forEach(([from, to]) => {
return_str = `${return_str.substring(0, from)}<span class="keyword-search-highlight">${return_str.substring(from,to)}</span>${return_str.substring(to)}`;
});
return return_str;
}

// This filters the list of examples given a string containing a space sperated list of regexes.
// The filtering is accieved by hiding non-matching examples and highlighting matching ones.
function filterExamples(regexes_str) {
const keywords = regexes_str
.trim()
.split(' ')
.filter((keyword) => {
if(keyword == ''){
return false;
}
try{
new RegExp(keyword);
}catch (SyntaxError){
return false;
}
return true;
})
.map((word) => new RegExp(word, 'gi'));
let hits = 0;
exampleSpans.each(function(idx) {
const exampleText = $(this).text().trim();
if (keywords.every((keyword) => exampleText.match(keyword) != null)){
$(this).addClass("keyword-search-match")
$(this).parent().parent().show();
$(this).html(highlightWords(exampleText, keywords));
hits++;
}else {
$(this).parent().parent().hide();
}
});
exampleSpans = $(".keyword-search-match");
if (hits === 0){
$("#empty-examples-excuse").show();
}
else {
$("#empty-examples-excuse").hide();
}
}


// This creates a debounced version of a function.
// The "debouncing" is implemented by delaying the execution for a certain amount of time since the last call.
function debounce(fn, delay=500) {
let timerId = null;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(() => fn(...args), delay);
};
}

const filterExamplesDebounced = debounce(filterExamples, 200);

function cleanup() {
// Calculate the example spans.
exampleSpans = $("ul#exampleList .example-name");
// Reset the selected example to nothing.
selectedExample = -1;
// Remove artifacts from previous usage.
exampleSpans.each(function(idx) {
$(this).removeClass("keyword-search-match")
$(this).parent().parent().show();
$(this).parent().removeClass('keyword-search-hover');
$(this).text(unHighlight($(this).text()));
});
}

$("#exampleKeywordSearchInput").on( "keydown", function( event ) {
// The down key was pressed.
if (exampleSpans.length > 0){
if (event.which == 40){
if (exampleSpans.length > 0){
$(exampleSpans[selectedExample]).parent().removeClass('keyword-search-hover');
selectedExample = (selectedExample + 1) % exampleSpans.length;
$(exampleSpans[selectedExample]).parent().addClass('keyword-search-hover');
}
}
// The up key was pressed.
else if (event.which == 38){
$(exampleSpans[selectedExample]).parent().removeClass('keyword-search-hover');
selectedExample = selectedExample - 1;
if (selectedExample == -1){
selectedExample = exampleSpans.length - 1;
}
$(exampleSpans[selectedExample]).parent().addClass('keyword-search-hover');
}
// The enter key was pressed.
else if (event.which == 13 && selectedExample >=0){
setTimeout(() => {
$(exampleSpans[selectedExample]).parent().click();
}, 50);
}
}
// The escape key was pressed.
if (event.which == 27){
$('#examplesDropdownToggle').click();
}
})

$('#exampleKeywordSearchInput').on("input", debounce(function (event) {
cleanup();
filterExamples(event.target.value);
}, 200));

// This initializes the keyword search when the dropdown has loaded.
$('#exampleList').parent().on('shown.bs.dropdown', function () {
// Clear value of the input field.
$("#exampleKeywordSearchInput").val('');
// Focus the input field.
$("#exampleKeywordSearchInput").focus();
// Cleanup keyword search.
cleanup();
})
15 changes: 11 additions & 4 deletions backend/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -197,23 +197,30 @@ <h4 style="margin-top: 0px;">QLever UI Shortcuts:</h4>
<button class="btn btn-default" onclick="showQueryPlanningTree();">
<i class="glyphicon glyphicon-object-align-vertical"></i> Analysis
</button>
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
<button id="examplesDropdownToggle" class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
<i class="glyphicon glyphicon-align-left"></i> Examples
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<ul id="exampleList" class="dropdown-menu">
<div style="padding: 10px 20px;">
<input id="exampleKeywordSearchInput" type="text" placeholder="search for keywords" style="width: 100%"> </input>
</div>
<li class="divider"></li>
{% for example in examples %}
<li>
<a onclick="example=1;editor.setValue(examples[{{ forloop.counter0 }}]);">
<a onclick="example=1;editor.setValue(examples[{{ forloop.counter0 }}]);editor.focus()">
{% if request.user.is_authenticated %}
<span onclick="window.open('/admin/backend/example/{{ example.pk }}/change/','_blank');" style="float: right;">
<i class="glyphicon glyphicon-pencil"></i>
</span>
{% endif %}
<span style="margin-right: 30px">{{ example.name }}</span>
<span class="example-name" style="margin-right: 30px">{{ example.name }}</span>
</a>
</li>
{% endfor %}
<li id="empty-examples-excuse" style="display: none">
<span style="padding: 3px 20px;">Sorry, no examples found :(</span>
</li>
{% if request.user.is_authenticated %}
<li class="divider"></li>
<li>
Expand Down
1 change: 1 addition & 0 deletions backend/templates/partials/head.html
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
<script src="{% static "js/raphael.js" %}"></script>
<script src="{% static "js/treant.js" %}"></script>
<script src="{% static "js/qleverUI.js" %}"></script>
<script src="{% static "js/keywordSearch.js" %}" defer></script>

<!-- CodeMirror and it's modules and language mode -->
<script src="{% static "js/codemirror/codemirror.js" %}"></script>
Expand Down
Binary file modified db/qleverui.sqlite3
Binary file not shown.
2 changes: 1 addition & 1 deletion qlever/settings_secret.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Change these values and rename this file to "settings_secret.py"
# SECURITY WARNING: If you are using this in production, do NOT use the default SECRET KEY!
# See: https://docs.djangoproject.com/en/3.0/ref/settings/#std:setting-SECRET_KEY
SECRET_KEY = '!!super_secret!!'
SECRET_KEY = 'RlQNe1rnd6XbGoHilGusDD0NhhCktURy'

# https://docs.djangoproject.com/en/3.0/ref/settings/#allowed-hosts
ALLOWED_HOSTS = ['*']