-
Notifications
You must be signed in to change notification settings - Fork 3
/
search.ts
151 lines (149 loc) · 6.88 KB
/
search.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/** Textual node search.*/
import * as sparql from "../sparql";
import * as util from "./util";
import * as fuse from "../fuse";
import { progress } from "./progress";
import { View } from "./view";
import MicroModal from "micromodal";
import log from "loglevel";
// disable bif:contains search because it does not even accept all non-space strings and the performance hit is negliglible
// BIF contains also breaks space insensitiveness, which we require and also check in the unit test
// const USE_BIF_CONTAINS = false;
const SEARCH_LIMIT = 100;
export class Search {
resultNodes = [];
/** Add search functionality to the form.
* @param form - a form with a search field named "query" */
constructor(form: HTMLFormElement) {
form.addEventListener("submit", (event: Event) => {
event.preventDefault();
// @ts-expect-error event target is the form itself
progress(() => this.showSearch(event.target.children.query.value));
});
log.debug("search initialized");
}
/**
* @param query - The user query.
* @param uris - An array of OWL class URIs
* @returns whether the search results are nonempty.
*/
showSearchResults(query: string, uris: Array<string>): boolean {
this.resultNodes = [];
const table = util.getElementById("tab:search-results") as HTMLTableElement;
// clear leftovers from last time
while (table.rows.length > 0) {
table.deleteRow(0);
}
if (uris.length === 0) {
util.getElementById("h2:search-results").innerHTML = `No Search Results for "${query}"`;
return false;
}
if (uris.length === 1) {
MicroModal.close("search-results");
View.activeState().graph.presentUri(uris[0]);
return true;
}
if (uris.length === SEARCH_LIMIT) {
util.getElementById("h2:search-results").innerHTML = `First ${SEARCH_LIMIT} Search Results for "${query}"`;
} else {
util.getElementById("h2:search-results").innerHTML = `${uris.length} Search Results for "${query}"`;
}
// Preprocessing: Classify URIs as (0) in graph and visible, (1) in graph and hidden but not filtered, (2) in graph and filtered and (3) not in the graph.
const uriType = {};
uris.forEach((uri) => {
const node = View.activeState().cy.getElementById(uri)[0];
if (node) {
uriType[uri] = 0;
if (node.hasClass("hidden") && !node.hasClass("filtered")) {
uriType[uri] = 1;
} else if (node.hasClass("filtered")) {
uriType[uri] = 2;
}
} else {
uriType[uri] = 3;
}
});
const selected: Set<string> = new Set();
// JavaScript search implementation is up to the browser but most should have a stable array search, which means that URIs within a URI type should keep their relative ranking
uris.sort((a, b) => uriType[a] - uriType[b]);
uris.forEach((uri) => {
const row = table.insertRow();
const checkCell = row.insertCell();
const checkBox = document.createElement("input");
checkBox.type = "checkbox";
checkCell.appendChild(checkBox);
checkCell.addEventListener("change", (e) => {
selected[(e.target as HTMLInputElement).checked ? "add" : "remove"](uri);
});
const locateCell = row.insertCell();
const lodLiveCell = row.insertCell();
(window as any).presentUri = View.activeState().graph.presentUri;
// todo: listener to add to selected uris
locateCell.innerHTML = `<a class="search-class${uriType[uri]}" href="javascript:MicroModal.close('search-results');window.presentUri('${uri}');void(0)">
${uri.replace(sparql.SNIK.PREFIX, "")}</a>`;
const html = `<a class="search-class${uriType[uri]}" href="${uri}" target="_blank">Description</a>`;
lodLiveCell.innerHTML = html;
});
const row = table.insertRow(0);
row.insertCell();
{
const cell = row.insertCell();
cell.innerHTML = "<a href='#'>Highlight All</a>";
cell.addEventListener("click", (e) => {
MicroModal.close("search-results");
View.activeState().graph.presentUris(uris);
e.preventDefault();
});
}
{
const cell = row.insertCell();
cell.innerHTML = "<a href='#'>Highlight Selected</a>";
cell.addEventListener("click", (e) => {
MicroModal.close("search-results");
View.activeState().graph.presentUris([...selected]);
e.preventDefault();
});
}
return true;
}
/** Searches the SPARQL endpoint for classes with the given label.
Case and space insensitive when not using bif:contains. Can be used by node.js.
@deprecated Old search without fuse index. Not used anymore.
@param userQuery - the search query as given by the user
@returns A promise with an array of class URIs.
*/
async search(userQuery: string): Promise<Array<string>> {
// prevent invalid SPARQL query and injection by keeping only alphanumeric English and German characters
// if other languages with other characters are to be supported, extend the regular expression
// remove space to make queries space insensitive, as people might search for URI suffixes which can be similar to the label so we get more recall
// works in conjuction with also ignoring whitespace for labels in the SPARQL query
// If this results in too low of a precision, the search can be made space sensitive again by changing /[\x22\x27\x5C\x0A\x0D ]/ to /[\x22\x27\x5C\x0A\x0D]/
// and adapting the SPARQL query along with it.
// Does not work with bif:contains.
// to avoid injection attacks and errors, so not allowed characters are replaced to match sparul syntax
// [156] STRING_LITERAL1 ::= "'" ( ([^#x27#x5C#xA#xD]) | ECHAR )* "'"
// [157] STRING_LITERAL2 ::= '"' ( ([^#x22#x5C#xA#xD]) | ECHAR )* '"'
// source: https://www.w3.org/TR/sparql11-query/#func-lcase
// Hexadecimal escape sequences require a leading zero in JavaScript, see https://mathiasbynens.be/notes/javascript-escapes.
const searchQuery = userQuery.replace(/[\x22\x27\x5C\x0A\x0D -]/g, "");
// use this when labels are available, URIs are not searched
const sparqlQuery = `select distinct(?s) { {?s a owl:Class.} UNION {?s a rdf:Property.}
{?s rdfs:label ?l.} UNION {?s skos:altLabel ?l.} filter(regex(lcase(replace(str(?l),"[ -]","")),lcase("${searchQuery}"))) } order by asc(strlen(str(?l))) limit ${SEARCH_LIMIT}`;
log.debug(sparqlQuery);
const bindings = await sparql.select(sparqlQuery);
return bindings.map((b) => b["s"].value);
// `select ?s {{?s a owl:Class.} UNION {?s a rdf:Property.}.
//filter (regex(replace(replace(str(?s),"${SPARQL_PREFIX}",""),"_"," "),"${query}","i")).}
}
/** Search the class labels and display the result to the user.
* @param userQuery - the search query as given by the user
* @returns false to prevent page reload triggered by submit.*/
async showSearch(userQuery): Promise<false> {
MicroModal.show("search-results");
// fuse returns results ordered by increasing score, where a low score is a better match than a high score
const items = await fuse.search(userQuery);
const uris = items.map((x) => x.item.uri);
this.showSearchResults(userQuery, uris);
return false; // prevent page reload triggered by submit
}
}