Merge Frank's branch 'dev.frontend/opensearch' into feat/opensearch

This commit is contained in:
Porras-Bernardez 2024-08-06 16:41:48 +02:00
commit cc62df68da
25 changed files with 1350 additions and 745 deletions

View File

@ -1,4 +1,6 @@
APP_URL=//tethys.at APP_URL=//tethys.at
VUE_API=//www.tethys.at VUE_API=//www.tethys.at
SOLR_HOST=tethys.at # SOLR_HOST=tethys.at
SOLR_CORE=rdr_data # SOLR_CORE=rdr_data
OPEN_HOST=192.168.21.18
OPEN_CORE=tethys-records

9
OpenSearch queries.txt Normal file
View File

@ -0,0 +1,9 @@
Get all documents in the index ("core" in solr)
----------------------------------------------------
curl -XGET "http://192.168.21.18/tethys-records/_search?pretty" -H 'Content-Type: application/json' -d'
{
"query": {
"match_all": {}
}
}'

60
SOLR queries.txt Normal file
View File

@ -0,0 +1,60 @@
Search by a specific value for one of the fields
https://tethys.at/solr/rdr_data/select?&q=year:2024
Search within a specific range of values for one of the fields
https://tethys.at/solr/rdr_data/select?&q=year:2023%20TO%202024 years 2023 to 2024
Search for a term (search done in predefined field?)
https://tethys.at/solr/rdr_data/select?&q=linz
Predefined Tethys Search
https://tethys.at/solr/rdr_data/select?&0=fl%3Did%2Clicence%2Cserver_date_published%2Cabstract_output%2Cidentifier%2Ctitle_output%2Ctitle_additional%2Cauthor%2Csubject%2Cdoctype&q=%2A&q.op=or&defType=edismax&qf=title%5E3%20author%5E2%20subject%5E1&indent=on&wt=json&rows=10&start=0&sort=server_date_published%20desc&facet=on&json.facet.language=%7B%20type%3A%20%22terms%22%2C%20field%3A%20%22language%22%20%7D&json.facet.subject=%7B%20type%3A%20%22terms%22%2C%20field%3A%20%22subject%22%2C%20limit%3A%20-1%20%7D&json.facet.year=%7B%20type%3A%20%22terms%22%2C%20field%3A%20%22year%22%20%7D&json.facet.author=%7B%20type%3A%20%22terms%22%2C%20field%3A%20%22author_facet%22%2C%20limit%3A%20-1%20%7D
Predefined Tethys search changing the last facet to "doctype"
https://tethys.at/solr/rdr_data/select?&0=fl%3Did%2Clicence%2Cserver_date_published%2Cabstract_output%2Cidentifier%2Ctitle_output%2Ctitle_additional%2Cauthor%2Csubject%2Cdoctype&q=*&q.op=or&defType=edismax&qf=title^3%20author^2%20subject^1&indent=on&wt=json&rows=10&start=0&sort=server_date_published%20desc&facet=on&json.facet.language={%20type%3A%20%22terms%22%2C%20field%3A%20%22language%22%20}&json.facet.subject={%20type%3A%20%22terms%22%2C%20field%3A%20%22subject%22%2C%20limit%3A%20-1%20}&json.facet.author={%20type%3A%20%22terms%22%2C%20field%3A%20%22author_facet%22%2C%20limit%3A%20-1%20}&json.facet.doctype={%20type%3A%20%22terms%22%2C%20field%3A%20%22doctype%22%2C%20limit%3A%20-1%20}
Giving a value for only one facet => Author: Coric, Stjepan (16)
https://tethys.at/solr/rdr_data/select?&0=fl%3Did%2Clicence%2Cserver_date_published%2Cabstract_output%2Cidentifier%2Ctitle_output%2Ctitle_additional%2Cauthor%2Csubject%2Cdoctype&q=%2A&q.op=or&defType=edismax&qf=title%5E3%20author%5E2%20subject%5E1&indent=on&wt=json&rows=10&fq=author%3A%28%22Coric%2C%20Stjepan%22%29&start=0&sort=server_date_published%20desc&facet=on&json.facet.language=%7B%20type%3A%20%22terms%22%2C%20field%3A%20%22language%22%20%7D&json.facet.subject=%7B%20type%3A%20%22terms%22%2C%20field%3A%20%22subject%22%2C%20limit%3A%20-1%20%7D&json.facet.year=%7B%20type%3A%20%22terms%22%2C%20field%3A%20%22year%22%20%7D&json.facet.author=%7B%20type%3A%20%22terms%22%2C%20field%3A%20%22author_facet%22%2C%20limit%3A%20-1%20%7D
ASCII
+----+-----+----+-----+----+-----+----+-----+
| Hx | Chr | Hx | Chr | Hx | Chr | Hx | Chr |
+----+-----+----+-----+----+-----+----+-----+
| 00 | NUL | 20 | SPC | 40 | @ | 60 | ` |
| 01 | SOH | 21 | ! | 41 | A | 61 | a |
| 02 | STX | 22 | " | 42 | B | 62 | b |
| 03 | ETX | 23 | # | 43 | C | 63 | c |
| 04 | EOT | 24 | $ | 44 | D | 64 | d |
| 05 | ENQ | 25 | % | 45 | E | 65 | e |
| 06 | ACK | 26 | & | 46 | F | 66 | f |
| 07 | BEL | 27 | ' | 47 | G | 67 | g |
| 08 | BS | 28 | ( | 48 | H | 68 | h |
| 09 | TAB | 29 | ) | 49 | I | 69 | i |
| 0A | LF | 2A | * | 4A | J | 6A | j |
| 0B | VT | 2B | + | 4B | K | 6B | k |
| 0C | FF | 2C | , | 4C | L | 6C | l |
| 0D | CR | 2D | - | 4D | M | 6D | m |
| 0E | SO | 2E | . | 4E | N | 6E | n |
| 0F | SI | 2F | / | 4F | O | 6F | o |
| 10 | DLE | 30 | 0 | 50 | P | 70 | p |
| 11 | DC1 | 31 | 1 | 51 | Q | 71 | q |
| 12 | DC2 | 32 | 2 | 52 | R | 72 | r |
| 13 | DC3 | 33 | 3 | 53 | S | 73 | s |
| 14 | DC4 | 34 | 4 | 54 | T | 74 | t |
| 15 | NAK | 35 | 5 | 55 | U | 75 | u |
| 16 | SYN | 36 | 6 | 56 | V | 76 | v |
| 17 | ETB | 37 | 7 | 57 | W | 77 | w |
| 18 | CAN | 38 | 8 | 58 | X | 78 | x |
| 19 | EM | 39 | 9 | 59 | Y | 79 | y |
| 1A | SUB | 3A | : | 5A | Z | 7A | z |
| 1B | ESC | 3B | ; | 5B | [ | 7B | { |
| 1C | FS | 3C | < | 5C | \ | 7C | | |
| 1D | GS | 3D | = | 5D | ] | 7D | } |
| 1E | RS | 3E | > | 5E | ^ | 7E | ~ |
| 1F | US | 3F | ? | 5F | _ | 7F | DEL |
+----+-----+----+-----+----+-----+----+-----+
https://www.asciitable.com/ see Hx
If you write encodeURIComponent(",") in your JavaScript console, then you will also get %2C. And with decodeURIComponent("%2C") you will get back the ","

22
package-lock.json generated
View File

@ -14,6 +14,7 @@
"axios": "^1.2.2", "axios": "^1.2.2",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"dayjs": "^1.10.7", "dayjs": "^1.10.7",
"dompurify": "^3.1.5",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
"qs": "^6.10.1", "qs": "^6.10.1",
"rxjs": "^7.5.5", "rxjs": "^7.5.5",
@ -29,6 +30,7 @@
"@babel/plugin-proposal-decorators": "^7.22.5", "@babel/plugin-proposal-decorators": "^7.22.5",
"@babel/preset-env": "^7.22.5", "@babel/preset-env": "^7.22.5",
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
"@types/dompurify": "^3.0.5",
"@types/leaflet": "^1.7.9", "@types/leaflet": "^1.7.9",
"@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0", "@typescript-eslint/parser": "^7.2.0",
@ -2497,6 +2499,15 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/dompurify": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
"dev": true,
"dependencies": {
"@types/trusted-types": "*"
}
},
"node_modules/@types/eslint": { "node_modules/@types/eslint": {
"version": "8.56.7", "version": "8.56.7",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.7.tgz", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.7.tgz",
@ -2694,6 +2705,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true
},
"node_modules/@types/webpack-env": { "node_modules/@types/webpack-env": {
"version": "1.18.4", "version": "1.18.4",
"resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.4.tgz", "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.4.tgz",
@ -5626,6 +5643,11 @@
"url": "https://github.com/fb55/domhandler?sponsor=1" "url": "https://github.com/fb55/domhandler?sponsor=1"
} }
}, },
"node_modules/dompurify": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.5.tgz",
"integrity": "sha512-lwG+n5h8QNpxtyrJW/gJWckL+1/DQiYMX8f7t8Z2AZTPw1esVrqjI63i7Zc2Gz0aKzLVMYC1V1PL/ky+aY/NgA=="
},
"node_modules/domutils": { "node_modules/domutils": {
"version": "2.8.0", "version": "2.8.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",

View File

@ -17,6 +17,7 @@
"axios": "^1.2.2", "axios": "^1.2.2",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"dayjs": "^1.10.7", "dayjs": "^1.10.7",
"dompurify": "^3.1.5",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
"qs": "^6.10.1", "qs": "^6.10.1",
"rxjs": "^7.5.5", "rxjs": "^7.5.5",
@ -32,6 +33,7 @@
"@babel/plugin-proposal-decorators": "^7.22.5", "@babel/plugin-proposal-decorators": "^7.22.5",
"@babel/preset-env": "^7.22.5", "@babel/preset-env": "^7.22.5",
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
"@types/dompurify": "^3.0.5",
"@types/leaflet": "^1.7.9", "@types/leaflet": "^1.7.9",
"@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0", "@typescript-eslint/parser": "^7.2.0",

View File

@ -34,7 +34,7 @@
</li> </li>
<li class="navbar-item"> <li class="navbar-item">
<!-- <a class="navbar-link is-arrowless" href="#">SEARCH</a> --> <!-- <a class="navbar-link is-arrowless" href="#">SEARCH</a> -->
<router-link class="navbar-link is-arrowless" to="/search">SEARCH</router-link> <router-link class="navbar-link is-arrowless" to="/search">OPENSEARCH</router-link>
</li> </li>
<li class="navbar-item"> <li class="navbar-item">
<!-- <a class="navbar-link is-arrowless" href="#">SERVICES</a> --> <!-- <a class="navbar-link is-arrowless" href="#">SERVICES</a> -->

View File

@ -1,46 +1,37 @@
import initializeAxios from "./axiosSetup"; // Import the necessary modules and functions
import { axiosRequestConfiguration } from "./config"; import initializeAxios from "./axiosSetup"; // Function to initialize the Axios instance
import { map } from "rxjs/operators"; import { axiosRequestConfiguration } from "./config"; // Axios configuration settings
// import { Observable } from "@reactivex/rxjs/compat"; import { map } from "rxjs/operators"; // Operator to transform the items emitted by an Observable
import { defer, Observable } from "rxjs"; import { defer, Observable } from "rxjs"; // RxJS utilities for creating and working with Observables
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios"; // Axios response type
// https://ichi.pro/de/so-wickeln-sie-axios-mit-typescript-und-react-in-rxjs-ein-118892823169891
// Initialize the Axios instance with the provided configuration
const axiosInstance = initializeAxios(axiosRequestConfiguration); const axiosInstance = initializeAxios(axiosRequestConfiguration);
// Function to make a GET request using Axios wrapped in an Observable
// eslint-disable-next-line // eslint-disable-next-line
const get = <T>(url: string, queryParams?: any): Observable<T> => { const get = <T>(url: string, queryParams?: any): Observable<T> => {
return defer(() => axiosInstance.get<T>(url, { params: queryParams })).pipe(map((result: AxiosResponse) => result.data)); // Use defer to create an Observable that makes the Axios GET request when subscribed to
return defer(() => axiosInstance.get<T>(url, { params: queryParams }))
// Use map to transform the Axios response to extract the data property
.pipe(map((result: AxiosResponse) => result.data));
}; };
// const post = <T>(url: string, body: object, queryParams?: any): Observable<T | void> => { // Function to make a POST request using Axios wrapped in an Observable
// return defer(() => axiosInstance.post<T>(url, body, { params: queryParams })).pipe(map((result: AxiosResponse) => result.data)); const post = <T>(url: string, body: any, queryParams?: any): Observable<T> => {
// }; // Use defer to create an Observable that makes the Axios POST request when subscribed to
// console.log(body);
// console.log(queryParams);
return defer(() => axiosInstance.post<T>(url, body, {
params: queryParams,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
}))
.pipe(map((result: AxiosResponse) => result.data)); // Use map to transform the Axios response to extract the data property
};
// const put = <T>( // Export the get and post functions as part of the default export
// url: string, export default { get, post };
// body: object,
// queryParams?: object
// ): Observable<T | void> => {
// return defer(() =>
// axiosInstance.put<T>(url, body, { params: queryParams })
// ).pipe(map((result) => result.data));
// };
// const patch = <T>(
// url: string,
// body: object,
// queryParams?: object
// ): Observable<T | void> => {
// return defer(() =>
// axiosInstance.patch<T>(url, body, { params: queryParams })
// ).pipe(map((result) => result.data));
// };
// const deleteR = <T>(url: string, id: number): Observable<T | void> => {
// return defer(() => axiosInstance.delete(`${url}/${id}`)).pipe(
// map((result) => result.data)
// );
// };
export default { get };

View File

@ -1,20 +1,12 @@
import axios, { AxiosRequestConfig, AxiosInstance } from "axios"; import axios, { AxiosRequestConfig, AxiosInstance } from "axios";
const initialization = (config: AxiosRequestConfig): AxiosInstance => { const initialization = (config: AxiosRequestConfig): AxiosInstance => {
//axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";
delete axios.defaults.headers.common["X-Requested-With"]; delete axios.defaults.headers.common["X-Requested-With"];
// axios.defaults.withCredentials = true;
// const token = document.head.querySelector('meta[name="csrf-token"]');
// if (token) {
// axios.defaults.headers.common["X-CSRF-TOKEN"] = token.innerHTML;
// }
const axiosInstance = axios.create(config);
/* const axiosInstance = axios.create(config);
Add default headers, interceptors etc..
*/
return axiosInstance; return axiosInstance;
}; };
export default initialization; export default initialization;

View File

@ -1,31 +1,16 @@
import { AxiosRequestConfig } from "axios"; import { AxiosRequestConfig } from "axios";
import { stringify } from "qs"; import { stringify } from "qs";
// let headers: AxiosRequestConfig['headers'] = /* This file configures Axios to send requests with the specified headers and parameters serialization format for URL-encoded form data */
// headers['Content-Type'] = 'multipart/form-data';
export const axiosRequestConfiguration: AxiosRequestConfig = { export const axiosRequestConfiguration: AxiosRequestConfig = {
// responseType: "text",
// headers: {
// // "Content-Type": "text/plain",
// "Content-Type": "application/x-www-form-urlencoded",
// // "Content-Type": "application/x-www-form-urlencoded",
// // credentials: "same-origin",
// // "Access-Control-Allow-Credentials": "true",
// // "Access-Control-Allow-Origin": "*",
// },
headers: { headers: {
//: AxiosHeaders | Partial<RawAxiosHeaders & MethodsHeaders & CommonHeaders> | undefined
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
}, },
// headers: {
// "Content-type": "application/json; charset=UTF-8",
// },
// paramsSerializer: {
// indexes: null, // by default: false
// },
paramsSerializer: { paramsSerializer: {
/* The serialize function takes an object of key-value pairs as input and uses the qs.stringify method to convert it into a URL-encoded string.
The arrayFormat: "repeat" option specifies how arrays should be serialized in the URL parameters. */
serialize: (params: Record<string, number>) => { serialize: (params: Record<string, number>) => {
return stringify(params, { arrayFormat: "repeat" }); return stringify(params, { arrayFormat: "repeat" });
}, },

View File

@ -20,6 +20,7 @@ export default class FacetCategory extends Vue {
filterName!: string; filterName!: string;
get alias(): string { get alias(): string {
// console.log("filterName:", this.filterName);
return this.filterName == "datatype" ? "doctype" : this.filterName; return this.filterName == "datatype" ? "doctype" : this.filterName;
} }
@ -53,6 +54,7 @@ export default class FacetCategory extends Vue {
@Emit("filter") @Emit("filter")
activateItem(filterItem: FacetItem): FacetItem { activateItem(filterItem: FacetItem): FacetItem {
// console.log("ActivateItem");
filterItem.category = this.alias; filterItem.category = this.alias;
filterItem.active = true; filterItem.active = true;
// this.$emit("filter", filterItem); // this.$emit("filter", filterItem);

View File

@ -2,12 +2,17 @@
// import debounce from 'lodash/debounce'; // import debounce from 'lodash/debounce';
// import { DatasetService } from "../../services/dataset.service"; // import { DatasetService } from "../../services/dataset.service";
import DatasetService from "../../services/dataset.service"; import DatasetService from "../../services/dataset.service";
import { SolrSettings } from "@/models/solr"; // import { SolrSettings } from "@/models/solr"; // PENDING USE
// import { ref } from "vue";
import { OpenSettings } from "@/models/solr";
import { Component, Vue, Prop, Emit } from "vue-facing-decorator"; import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
// import { Prop, Emit } from "vue-property-decorator";
import { Dataset, Suggestion, SearchType } from "@/models/dataset"; import { Dataset, Suggestion, SearchType } from "@/models/dataset";
import { SOLR_HOST, SOLR_CORE } from "@/constants"; // import { SOLR_HOST, SOLR_CORE } from "@/constants";
import { OPEN_HOST, OPEN_CORE } from "@/constants"; // PENDING USE
import { HitHighlight } from "@/models/headers";
import DOMPurify from 'dompurify'; // To sanitize the HTML content to prevent XSS attacks!
@Component({ @Component({
name: "VsInput", name: "VsInput",
@ -16,30 +21,40 @@ export default class VsInput extends Vue {
// @Prop() // @Prop()
// private title!: string; // private title!: string;
// Define the placeholder text for the input field
@Prop({ default: "Search" }) @Prop({ default: "Search" })
readonly placeholder!: string; readonly placeholder!: string;
private display = ""; private display = ""; // Input display value
@Prop() @Prop()
private propDisplay = ""; private propDisplay = "";
private value!: Suggestion | string; private value!: Suggestion | string;
private error = ""; private error = "";
private results: Array<Dataset> = []; private results: Array<Dataset> = []; // Array to store search results
private loading = false; private highlights: Array<HitHighlight> = [];
private selectedIndex = -1;
// private selectedDisplay = ""; private loading = false; // Loading state indicator
private solr: SolrSettings = { private selectedIndex = -1; // Index of the currently selected suggestion
core: SOLR_CORE, //"rdr_data", // SOLR.core;
host: SOLR_HOST, //"tethys.at", // private solr: SolrSettings = {
// core: SOLR_CORE, //"rdr_data", // SOLR.core;
// host: SOLR_HOST, //"tethys.at",
// };
private openSearch: OpenSettings = {
core: OPEN_CORE, //"rdr_data", // SOLR.core;
host: OPEN_HOST, //"tethys.at",
// core: "test_data", // SOLR.core; // core: "test_data", // SOLR.core;
// host: "repository.geologie.ac.at", // host: "repository.geologie.ac.at",
}; };
// private rdrAPI!: DatasetService;
itemRefs!: Array<Element>;
emits = ["filter"];
// private rdrAPI!: DatasetService;
itemRefs!: Array<Element>; // Array to store references to suggestion items
emits = ["filter"]; // Emits filter event
// Set reference for each item
setItemRef(el: Element): void { setItemRef(el: Element): void {
this.itemRefs.push(el); this.itemRefs.push(el);
} }
@ -68,53 +83,113 @@ export default class VsInput extends Vue {
return this.error !== null; return this.error !== null;
} }
// Computed property to generate suggestions based on search results
get suggestions(): Suggestion[] { get suggestions(): Suggestion[] {
// const suggestion = {
// titles: new Array<string>(),
// authors: new Array<string>(),
// subjects: new Array<string>(),
// };
const suggestions = new Array<Suggestion>(); const suggestions = new Array<Suggestion>();
this.results.forEach((dataset) => { // console.log("getSuggestions > Display:", this.display);
// const del = dataset.title_output?.toLowerCase(); // console.log("results:", this.results );
if (dataset.title_output.toLowerCase().includes(this.display.toLowerCase())) { // console.log("highlights:", this.highlights);
const title = dataset.title_output;
// if (!suggestion["titles"].find((value) => value === title)) { //The method checks if there are any highlighted titles in the highlight object. If found, it joins the highlighted fragments into a single string
// suggestion.titles.push(title); // Generate suggestions based on search results
// } this.results.forEach((dataset, index) => {
const hasTitleSuggestion = suggestions.some((suggestion) => suggestion.value === title && suggestion.type == SearchType.Title);
if (!hasTitleSuggestion) {
const suggestion = new Suggestion(title, SearchType.Title);
suggestions.push(suggestion);
}
}
if (this.find(dataset.author, this.display.toLowerCase()) !== "") {
const author = this.find(dataset.author, this.display.toLowerCase());
const hasAuthorSuggestion = suggestions.some((suggestion) => suggestion.value === author && suggestion.type == SearchType.Author); const highlight = this.highlights[index];
// console.log("get suggestions:id", dataset.id);
// console.log("get suggestions:title_output", dataset.title_output);
// console.log("get suggestions:author", dataset.author);
// console.log("get suggestions:subjects", dataset.subjects);
// Checks if a suggestion with the same title and type already exists in the suggestions array. If not, it creates a new Suggestion object and adds it to the suggestions array.
if (highlight.title && highlight.title.length > 0) {
/** This line checks if the highlight object has a title property and if that property is an array with at least one element.
* The highlight object contains highlighted fragments of the search term in various fields (e.g., title, author, subjects) as returned by the OpenSearch API.
* This check ensures that we only process results that have highlighted titles. */
const highlightedTitle = highlight.title.join(" ");
/**
* The highlight.title property is an array of strings, where each string is a highlighted fragment of the title. join(" ") combines these fragments into a single string with spaces between them.
* This step constructs a full highlighted title from the individual fragments.
* OpenSearch can return multiple fragments of a field (like the title) in its response, especially when the field contains multiple terms that match the search query.
* This can happen because OpenSearch's highlighting feature is designed to provide context around each match within the field, which can result in multiple highlighted fragments.
*/
const hasTitleSuggestion = suggestions.some((suggestion) => suggestion.highlight.toLowerCase() === highlightedTitle.toLowerCase() && suggestion.type == SearchType.Title);
if (!hasTitleSuggestion) {
const suggestion = new Suggestion(dataset.title_output, highlightedTitle, SearchType.Title);
suggestions.push(suggestion);
}
}
if (highlight.author && highlight.author.length > 0) {
const highlightedAuthor = highlight.author.join(" ");
const datasetAuthor = this.find(dataset.author, this.display.toLowerCase());
const hasAuthorSuggestion = suggestions.some((suggestion) => suggestion.highlight.toLowerCase() === highlightedAuthor.toLowerCase() && suggestion.type == SearchType.Author);
if (!hasAuthorSuggestion) { if (!hasAuthorSuggestion) {
const suggestion = new Suggestion(author, SearchType.Author); const suggestion = new Suggestion(datasetAuthor, highlightedAuthor, SearchType.Author);
suggestions.push(suggestion); suggestions.push(suggestion);
} }
} }
if (this.find(dataset.subject, this.display.toLowerCase()) != "") { if (highlight.subjects && highlight.subjects.length > 0) {
const subject = this.find(dataset.subject, this.display.toLowerCase()); const highlightedSubject = highlight.subjects.join(" ");
const hasSubjectSuggestion = suggestions.some((suggestion) => suggestion.value === subject && suggestion.type == SearchType.Subject); const datasetSubject = this.find(dataset.subjects, this.display.toLowerCase());
const hasSubjectSuggestion = suggestions.some((suggestion) => suggestion.highlight.toLowerCase() === highlightedSubject.toLowerCase() && suggestion.type == SearchType.Subject);
if (!hasSubjectSuggestion) { if (!hasSubjectSuggestion) {
const suggestion = new Suggestion(subject, SearchType.Subject); const suggestion = new Suggestion(datasetSubject, highlightedSubject, SearchType.Subject);
suggestions.push(suggestion); suggestions.push(suggestion);
} }
} }
// ORIGINAL SOLR ===================================================================================================
// if (dataset.title_output.toLowerCase().includes(this.display.toLowerCase())) {
// const title = dataset.title_output;
// // Check if there is already a suggestion with this title and type
// const hasTitleSuggestion = suggestions.some((suggestion) => suggestion.value === title && suggestion.type == SearchType.Title);
// if (!hasTitleSuggestion) {
// // If there is no such suggestion, create a new one and add it to the suggestions array
// const suggestion = new Suggestion(title, SearchType.Title);
// suggestions.push(suggestion);
// }
// }
// if (this.find(dataset.author, this.display.toLowerCase()) !== "") {
// const author = this.find(dataset.author, this.display.toLowerCase());
// // Check if there is already a suggestion with this author and type
// const hasAuthorSuggestion = suggestions.some((suggestion) => suggestion.value === author && suggestion.type == SearchType.Author);
// if (!hasAuthorSuggestion) {
// const suggestion = new Suggestion(author, SearchType.Author);
// suggestions.push(suggestion);
// }
// }
// if (this.find(dataset.subjects, this.display.toLowerCase()) != "") {
// const subject = this.find(dataset.subjects, this.display.toLowerCase());
// const hasSubjectSuggestion = suggestions.some((suggestion) => suggestion.value === subject && suggestion.type == SearchType.Subject);
// if (!hasSubjectSuggestion) {
// const suggestion = new Suggestion(subject, SearchType.Subject);
// suggestions.push(suggestion);
// }
// }
}); });
return suggestions; return suggestions;
} }
/**
* This method combines the suggestion value and type into a single HTML string. It also sanitizes the HTML content using DOMPurify to prevent XSS attacks.
* The vue file uses the v-html directive to bind the combined HTML string to the label element. This ensures that the HTML content (e.g., <em>Wien</em>) is rendered correctly in the browser.
*/
formatSuggestion(result: Suggestion): string {
const sanitizedValue = DOMPurify.sanitize(result.highlight);
// Replacing the predefined format for highlights given by OpenSearch from <em> emphasys to <b> bold
const replacedValue = sanitizedValue.replace(/<em>/g, '<b>').replace(/<\/em>/g, '</b>');
return `${replacedValue} <em>| ${result.type}</em>`;
}
/** /**
* Clear all values, results and errors * Clear all values, results and errors
**/ **/
clear(): void { clear(): void {
console.log("clear");
this.display = ""; this.display = "";
// this.value = null; // this.value = null;
this.results = []; this.results = [];
@ -122,15 +197,20 @@ export default class VsInput extends Vue {
// this.$emit("clear"); // this.$emit("clear");
} }
/* When the search button is clicked or the search input is changed, it updates the value property of the component with the current value of display,
and emits a search-change event with the current value of display as the argument. */
@Emit("search-change") @Emit("search-change")
search(): string { search(): string {
console.log("search");
this.results = []; this.results = [];
// this.$emit("search", this.display) // this.$emit("search", this.display)
this.value = this.display; //(obj["title_output"]) ? obj["title_output"] : obj.id this.value = this.display; //(obj["title_output"]) ? obj["title_output"] : obj.id
return this.display; return this.display;
} }
// Handler for search input change
searchChanged(): void { searchChanged(): void {
// console.log("Search changed!");
this.selectedIndex = -1; this.selectedIndex = -1;
// Let's warn the parent that a change was made // Let's warn the parent that a change was made
// this.$emit("input", this.display); // this.$emit("input", this.display);
@ -142,7 +222,9 @@ export default class VsInput extends Vue {
} }
} }
// Perform the search request
private resourceSearch() { private resourceSearch() {
// console.log("resourceSearch");
if (!this.display) { if (!this.display) {
this.results = []; this.results = [];
return; return;
@ -152,23 +234,29 @@ export default class VsInput extends Vue {
this.request(); this.request();
} }
// Make the API request to search for datasets
private request(): void { private request(): void {
DatasetService.searchTerm(this.display, this.solr.core, this.solr.host).subscribe({ console.log("request()");
next: (res: Dataset[]) => this.dataHandler(res), // DatasetService.searchTerm(this.display, this.solr.core, this.solr.host).subscribe({
DatasetService.searchTerm(this.display, this.openSearch.core, this.openSearch.host).subscribe({
// next: (res: Dataset[]) => this.dataHandler(res),
next: (res: { datasets: Dataset[], highlights: HitHighlight[] }) => this.dataHandler(res.datasets, res.highlights),
error: (error: string) => this.errorHandler(error), error: (error: string) => this.errorHandler(error),
complete: () => (this.loading = false), complete: () => (this.loading = false),
}); });
} }
private dataHandler(datasets: Dataset[]): void { // Handle the search results
private dataHandler(datasets: Dataset[], highlights: HitHighlight[]): void {
this.results = datasets; this.results = datasets;
// this.$emit("search", this.display); this.highlights = highlights; // Store highlights
// this.loading = false; // console.log(datasets);
} }
// Handle errors from the search request
private errorHandler(err: string): void { private errorHandler(err: string): void {
this.error = err; this.error = err;
// this.loading = false;
} }
/** /**
@ -180,7 +268,9 @@ export default class VsInput extends Vue {
return key === this.selectedIndex; return key === this.selectedIndex;
} }
// Handle arrow down key press to navigate suggestions
onArrowDown(ev: Event): void { onArrowDown(ev: Event): void {
console.log("onArrowDown");
ev.preventDefault(); ev.preventDefault();
if (this.selectedIndex === -1) { if (this.selectedIndex === -1) {
this.selectedIndex = 0; this.selectedIndex = 0;
@ -190,6 +280,7 @@ export default class VsInput extends Vue {
this.fixScrolling(); this.fixScrolling();
} }
// Scroll the selected suggestion into view
private fixScrolling() { private fixScrolling() {
const currentElement = this.itemRefs[this.selectedIndex]; const currentElement = this.itemRefs[this.selectedIndex];
currentElement.scrollIntoView({ currentElement.scrollIntoView({
@ -199,7 +290,9 @@ export default class VsInput extends Vue {
}); });
} }
// Handle arrow up key press to navigate suggestions
onArrowUp(ev: Event): void { onArrowUp(ev: Event): void {
console.log("onArrowUp");
ev.preventDefault(); ev.preventDefault();
if (this.selectedIndex === -1) { if (this.selectedIndex === -1) {
this.selectedIndex = this.suggestions.length - 1; this.selectedIndex = this.suggestions.length - 1;
@ -209,7 +302,10 @@ export default class VsInput extends Vue {
this.fixScrolling(); this.fixScrolling();
} }
// Handle enter key press to select a suggestion
onEnter(): void { onEnter(): void {
console.log("onEnter");
if (this.selectedIndex === -1) { if (this.selectedIndex === -1) {
// this.$emit("nothingSelected", this.display); // this.$emit("nothingSelected", this.display);
this.display && this.search(); this.display && this.search();
@ -222,18 +318,18 @@ export default class VsInput extends Vue {
@Emit("search-change") @Emit("search-change")
private select(obj: Suggestion): Suggestion { private select(obj: Suggestion): Suggestion {
// if (!obj) { console.log("select:");
// return; this.value = obj;
// } console.log(obj);
this.value = obj; //(obj["title_output"]) ? obj["title_output"] : obj.id
this.display = obj.value; // this.formatDisplay(obj) this.display = obj.value;
// this.selectedDisplay = this.display;
this.close(); this.close();
// this.$emit("update", this.value);
return this.value; return this.value;
} }
// Find a search term in an array
private find(myarray: Array<string>, searchterm: string): string { private find(myarray: Array<string>, searchterm: string): string {
for (let i = 0, len = myarray.length; i < len; i += 1) { for (let i = 0, len = myarray.length; i < len; i += 1) {
if (typeof myarray[i] === "string" && myarray[i].toLowerCase().indexOf(searchterm) !== -1) { if (typeof myarray[i] === "string" && myarray[i].toLowerCase().indexOf(searchterm) !== -1) {
@ -248,15 +344,11 @@ export default class VsInput extends Vue {
* Close the results list. If nothing was selected clear the search * Close the results list. If nothing was selected clear the search
*/ */
close(): void { close(): void {
console.log("close");
if (!this.value) { if (!this.value) {
this.clear(); this.clear();
} }
// if (this.selectedDisplay !== this.display && this.value) {
// this.display = this.selectedDisplay;
// }
this.results = []; this.results = [];
this.error = ""; this.error = "";
//this.removeEventListener()
// this.$emit("close");
} }
} }

View File

@ -1,10 +1,15 @@
<template> <template>
<!-- Parent container with multiple rows -->
<div class="is-multiline"> <div class="is-multiline">
<!-- <div class="content column is-half is-offset-one-quarter" style="margin-top: 30px; padding-bottom: 0; margin-bottom: 0px"> --> <!-- Search input wrapper -->
<div class="column is-two-thirds-tablet is-half-desktop is-one-third-widescreen mx-auto"> <div class="column is-two-thirds-tablet is-half-desktop is-one-third-widescreen mx-auto">
<!-- Search box -->
<div class="search-box mx-auto"> <div class="search-box mx-auto">
<!-- Search field -->
<div class="field has-addons main-search-from-bg"> <div class="field has-addons main-search-from-bg">
<div class="control is-expanded"> <div class="control is-expanded">
<!-- Input field for search query -->
<input <input
id="search_query" id="search_query"
v-model="display" v-model="display"
@ -20,24 +25,26 @@
@keydown.tab="close" @keydown.tab="close"
@focus="focus" @focus="focus"
/> />
<!-- <p>Display is: {{ display }}</p> -->
<!-- v-on:input="searchChanged" -->
</div> </div>
<!-- Search button -->
<div class="control"> <div class="control">
<button class="button input is-medium search-button-icon" @click="search()"> <button class="button input is-medium search-button-icon" @click="search()">
<!-- <img src="../../assets/fa/search.svg" style="height: 22px; width: 22px" /> --> <!-- Search icon -->
<i class="fas fa-search text-white"></i> <i class="fas fa-search text-white"></i>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- <div class="column is-half is-offset-one-quarter"> -->
<!-- Suggestions list -->
<div class="column is-two-thirds-tablet is-half-desktop is-one-third-widescreen mx-auto"> <div class="column is-two-thirds-tablet is-half-desktop is-one-third-widescreen mx-auto">
<ul v-show="showResults" class="autocomplete-results pure-u-23-24"> <ul v-show="showResults" class="autocomplete-results pure-u-23-24">
<!-- Loading indicator -->
<li v-if="isLoading" class="loading">Loading results...</li> <li v-if="isLoading" class="loading">Loading results...</li>
<!-- Iterating over suggestions -->
<li <li
v-for="(result, i) in suggestions" v-for="(result, i) in suggestions"
v-else v-else
@ -47,8 +54,10 @@
v-bind:class="{ 'is-active': isSelected(i) }" v-bind:class="{ 'is-active': isSelected(i) }"
@click.prevent="select(result)" @click.prevent="select(result)"
> >
<!-- Displaying suggestion result -->
<div class="small-label"> <div class="small-label">
<label>{{ result.value }} ({{ result.type }})</label> <!-- <label>{{ result.value }} ({{ result.type }})</label> -->
<label v-html="formatSuggestion(result)"></label>
</div> </div>
</li> </li>
</ul> </ul>
@ -140,7 +149,7 @@ input {
.autocomplete-result-item { .autocomplete-result-item {
list-style: none; list-style: none;
text-align: left; text-align: left;
/* padding: 7px 10px; */ padding: 0px 0px 0px 5px; // top,right,bottom,left
cursor: pointer; cursor: pointer;
} }

View File

@ -23,13 +23,15 @@ export default class VsResult extends Vue {
.join("."); .join(".");
} }
private convert(unixtimestamp: number): string { // private convert(unixtimestamp: number): string { // SOLR
private convert(unixtimestamp: string): string { // OpenSearch
// Unixtimestamp // Unixtimestamp
// var unixtimestamp = document.getElementById('timestamp').value; // var unixtimestamp = document.getElementById('timestamp').value;
// Months array // Months array
const months_arr = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; const months_arr = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
// Convert timestamp to milliseconds // Convert timestamp to milliseconds
const date = new Date(unixtimestamp * 1000); // const date = new Date(unixtimestamp * 1000); // SOLR
const date = new Date(Number(unixtimestamp) * 1000); // OpenSearch
// Year // Year
const year = date.getFullYear(); const year = date.getFullYear();
// Month // Month

View File

@ -3,49 +3,25 @@
<div class="card result-list-container"> <div class="card result-list-container">
<div class="card-content record-elem"> <div class="card-content record-elem">
<p v-if="document.identifier && document.identifier.length > 0"> <p v-if="document.identifier && document.identifier.length > 0">
<!-- <span>Author: {{ document.identifier.join(', ') }}</span> -->
<!-- <span v-for="(author,index) in document.author" :key="index">{{ author }}; </span> -->
<!-- <span>'https://doi.org/' + {{ document.identifier[0] }}</span> -->
<a target="_blank" v-bind:href="'https://doi.org/' + document.identifier[0]"> <a target="_blank" v-bind:href="'https://doi.org/' + document.identifier[0]">
{{ "https://doi.org/" + document.identifier[0] + " &#10148;" }} </a {{ "https://doi.org/" + document.identifier[0] + " &#10148;" }} </a
>&nbsp; >&nbsp;
<span v-if="document.author && document.author.length > 0" class="disabled">{{ document.author[0] }}</span> <span v-if="document.author && document.author.length > 0" class="disabled">{{ document.author[0] }}</span>
</p> </p>
<!-- <span class="label label-info" data-container="div" data-title="Publication date">
{{ convert(document.server_date_published) }}
</span>
<span class="label label-default ng-binding">{{ document.doctype }}</span>
<span v-if="openAccessLicences.includes(document.licence)" class="label label-success titlecase">Open Access</span> -->
<h4> <h4>
<!-- <a
v-if="document.identifier && document.identifier.length > 0"
target="_self"
v-bind:href="'https://doi.' + getDomainWithoutSubdomain() + '/' + document.identifier[0]"
class="ng-binding"
>
{{ document.title_output }}
</a> -->
<!-- <a target="_self" v-bind:href="'dataset/' + document.id" class="ng-binding">
{{ document.title_output }}
</a> -->
<router-link class="ng-binding" v-bind:to="{ name: 'dataset', params: { datasetId: document.id } }">{{ <router-link class="ng-binding" v-bind:to="{ name: 'dataset', params: { datasetId: document.id } }">{{
document.title_output document.title_output
}}</router-link> }}</router-link>
</h4> </h4>
<!-- <p v-if="document.author && document.author.length > 0">
<span>Author: {{ document.author.join(', ') }}</span>
<span v-for="(author, index) in document.author" :key="index">{{ author }}; </span>
</p> -->
<p class="clamped clamped-2"> <p class="clamped clamped-2">
<span class="disabled" data-container="div" data-title="Publication date"> <span class="disabled" data-container="div" data-title="Publication date">
{{ convert(document.server_date_published) + ":&nbsp;" }} {{ convert(document.server_date_published) + ":&nbsp;" }}
</span> </span>
<span class="text"> <span class="text">
{{ document.abstract_output }} <!-- {{ document.abstract_output }} -->
{{ document.abstract[0] }}
<span class="ellipsis">...</span> <span class="ellipsis">...</span>
<span class="fill"></span> <span class="fill"></span>
</span> </span>
@ -53,11 +29,9 @@
<p> <p>
<span class="label"><i class="fas fa-file"></i> {{ document.doctype }}</span> <span class="label"><i class="fas fa-file"></i> {{ document.doctype }}</span>
<!-- <span>Licence: {{ document.licence }}</span> -->
<span v-if="openAccessLicences.includes(document.licence)" class="label titlecase"><i class="fas fa-lock-open"></i> Open Access</span> <span v-if="openAccessLicences.includes(document.licence)" class="label titlecase"><i class="fas fa-lock-open"></i> Open Access</span>
</p> </p>
<!-- <span class="label label-keyword titlecase" v-for="(item, index) in document.subject" :key="index"> #{{ item }} </span> -->
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,19 +2,31 @@
// declare const EDGE_URL: string; // declare const EDGE_URL: string;
declare const APP_URL: string; declare const APP_URL: string;
declare const VUE_API: string; declare const VUE_API: string;
declare const SOLR_HOST: string; // declare const SOLR_HOST: string;
declare const SOLR_CORE: string; // declare const SOLR_CORE: string;
// OPENSEARCH
declare const OPEN_HOST: string;
declare const OPEN_CORE: string;
// const _EDGE_URL = EDGE_URL; // const _EDGE_URL = EDGE_URL;
// const _POINT_URL = POINT_URL; // const _POINT_URL = POINT_URL;
const _APP_URL = APP_URL; const _APP_URL = APP_URL;
const _VUE_API = VUE_API; const _VUE_API = VUE_API;
const _SOLR_HOST = SOLR_HOST; // const _SOLR_HOST = SOLR_HOST;
const _SOLR_CORE = SOLR_CORE; // const _SOLR_CORE = SOLR_CORE;
// OPENSEARCH
const _OPEN_HOST = OPEN_HOST;
const _OPEN_CORE = OPEN_CORE;
// export { _EDGE_URL as EDGE_URL }; // export { _EDGE_URL as EDGE_URL };
// export { _POINT_URL as POINT_URL }; // export { _POINT_URL as POINT_URL };
export { _APP_URL as APP_URL }; export { _APP_URL as APP_URL };
export { _VUE_API as VUE_API }; export { _VUE_API as VUE_API };
export { _SOLR_HOST as SOLR_HOST }; // export { _SOLR_HOST as SOLR_HOST };
export { _SOLR_CORE as SOLR_CORE }; // export { _SOLR_CORE as SOLR_CORE };
// OPENSEARCH
export { _OPEN_HOST as OPEN_HOST };
export { _OPEN_CORE as OPEN_CORE };

View File

@ -1,72 +1,91 @@
// import moment from "moment"; // import moment from "moment";
import dayjs from "dayjs"; import dayjs from "dayjs";
// // SOLR Dataset original
// export interface Dataset {
// abstract_additional: Array<string>;// OpenSearch: abstract: Array<string>
// abstract_output: string;// -----
// author: Array<string>;// EQUAL
// author_sort: Array<string>;// -----
// belongs_to_bibliography: boolean;// EQUAL
// creating_corporation: string;// EQUAL
// doctype: string;// EQUAL
// geo_location: string;// EQUAL
// id: number;// EQUAL
// identifier: Identifier;// OpenSearch: identifier: Array<string>
// language: string;// EQUAL
// licence: string;// EQUAL
// publisher_name: string;// EQUAL
// server_date_published: Array<number>;// OpenSearch not array!
// subject: Array<string>;// OpenSearch: subjectS
// title_output: string;// EQUAL
// year: number;// EQUAL
// year_inverted: number;// EQUAL
// }
// OpenSearch Dataset
export interface Dataset { export interface Dataset {
abstract_additional: Array<string>; abstract: Array<string>;// OpenSearch: abstract: Array<string>
abstract_output: string; // abstract_output: string;// ----- NOT in OpenSearch
author: Array<string>; author: Array<string>;// EQUAL
author_sort: Array<string>; // author_sort: Array<string>;// ----- NOT in OpenSearch
belongs_to_bibliography: boolean; belongs_to_bibliography: boolean;// EQUAL
creating_corporation: string; creating_corporation: string;// EQUAL
doctype: string; doctype: string;// EQUAL
geo_location: string; geo_location: string;// EQUAL
id: number; id: number;// EQUAL
identifier: Identifier; // identifier: Identifier;// OpenSearch: identifier: Array<string>
language: string; identifier: Array<string>// DIFF DATATYPE
licence: string; language: string;// EQUAL
publisher_name: string; licence: string;// EQUAL
server_date_published: Array<number>; publisher_name: string;// EQUAL
subject: Array<string>; // server_date_published: Array<number>;// OpenSearch string!
title_output: string; server_date_published: string;// DIFF DATATYPE
year: number; // subject: Array<string>;// OpenSearch: subjectS
year_inverted: number; subjects: Array<string>;// DIFF DATATYPE
title_output: string;// EQUAL
year: number;// EQUAL
year_inverted: number;// EQUAL
title: string // Unique in OpenSearch
title_additional: Array<string> // Unique in OpenSearch
bbox_xmin: string // Unique in OpenSearch
bbox_xmax: string // Unique in OpenSearch
bbox_ymin: string // Unique in OpenSearch
bbox_ymax: string // Unique in OpenSearch
reference: Array<string> // Unique in OpenSearch
abstract_additional: Array<string>;// Unique in OpenSearch
} }
export class Suggestion { export class Suggestion {
constructor( constructor(
public value: string, public value: string, // Store the text value returned by OpenSearch
public type: SearchType, // Store the highlight: i.e. the text value with the emphasised term that generated that results by OpenSearch.
// In this way we can highlight the real term existing in the publication independently of how different was the inserted term used for the FUZZY search. e.g. "Vien" fuzzy matched with "Wien"
public highlight: string,
public type: SearchType, // Type of search element
) {} ) {}
// value!: string; // value!: string;
// type!: SearchType; // type!: SearchType;
} }
// export class Suggestion {
// constructor(
// public value: string,
// public type: SearchType,
// ) {}
// // value!: string;
// // type!: SearchType;
// }
export enum SearchType { export enum SearchType {
Title = "title", Title = "title",
Author = "author", Author = "author",
Subject = "subject", Subject = "subjects" // ** !! The field has this name in OpenSearch!!
} }
export class DbDataset { export class DbDataset {
// public id!: number;
// public url!: string;
// public contributing_corporation!: string;
// public creating_corporation!: string;
// public publisher_name!: string;
// public embargo_date!: string;
// public publish_id!: number;
// public project_id!: number;
// public type!: string;
// public language!: string;
// public server_state!: string;
// public belongs_to_bibliography!: boolean;
// public created_at!: string;
// public server_date_modified!: string;
// public server_date_published!: string;
// public account_id!: number;
// public editor_id!: number;
// public reviewer_id!: number;
// public preferred_reviewer!: number;
// public preferred_reviewer_email!: string;
// public reject_editor_note!: string;
// public reject_reviewer_note!: string;
// public reviewer_note_visible!: string;
// public titles!: Array<Title>;
// public abstracts!: Array<Abstract>;
// public authors!: Array<Author>;
// public contributors!: Array<Author>;
// public user!: Person;
// public subjects!: Array<Subject>;
constructor( constructor(
public id: string, public id: string,

View File

@ -18,19 +18,6 @@ export interface ResponseHeaderParams {
rows?: number; rows?: number;
start?: number; start?: number;
wt?: string; wt?: string;
// 0:'fl=id,licence,server_date_published,abstract_output,identifier,title_output,title_additional,author,subject,doctype'
// df:'title'
// facet:'on'
// indent:'on'
// json.facet.language:'{ type: "terms", field: "language" }'
// json.facet.subject:'{ type: "terms", field: "subject" }'
// q:'title:Geodaten - Blatt 49 Wels (1:50.000)'
// q.op:'and'
// rows:'10'
// start:'0'
// wt:'json'
} }
export interface ResponseContent { export interface ResponseContent {
@ -39,36 +26,21 @@ export interface ResponseContent {
docs: Array<Dataset>; docs: Array<Dataset>;
} }
// export interface FacetCount { //Used
// facet_fields: FacetCategory<any>;
// }
// export class FacetCategory<T> {
// [key: string]: {
// values: T[];
// };
// }
export class FacetResults { export class FacetResults {
// language!: Array<FacetItem>;
// subject!: Array<FacetItem>;
[key: string]: Array<FacetItem>; [key: string]: Array<FacetItem>;
} }
//#region solr response facets
export class FacetFields { export class FacetFields {
// count: number;
language!: FacetInstance; language!: FacetInstance;
subject!: FacetInstance; subject!: FacetInstance;
// [key: string]: FacetInstance;
} }
export interface FacetInstance { export interface FacetInstance {
[key: string]: Array<FacetItem>; [key: string]: Array<FacetItem>;
// buckets: Array<FacetItem>;
} }
//Used
export class FacetItem { export class FacetItem {
val: string; val: string;
count: number; count: number;
@ -83,3 +55,77 @@ export class FacetItem {
} }
} }
//#endregion //#endregion
// OPENSEARCH
// ========================================================================
export interface OpenSearchResponse {
took: number;
timed_out: boolean;
_shards: Shards;
hits: Hits; // Equivalent SOLR: response > docs
aggregations?: Aggregations; // Equivalent SOLR: facets
}
export interface Shards {
total: number;
successful: number;
skipped: number;
failed: number;
}
export interface Hits {
total: Total;
max_score: number;
hits: Array<Hit>;
}
export interface Total {
value: number; // Equivalent SOLR: response > numFound
relation: string;
}
export interface Hit {
_index: string;
_id: string;
_score: number;
_source: Dataset;
highlight: HitHighlight; // !! This name is to avoid collision with Typescript "Highlight" class
}
export interface HitHighlight {
subjects?: Array<string>;
title?: Array<string>;
author?: Array<string>;
}
export interface Aggregations { // Equivalent SOLR: FacetFields
subjects: Subjects;
language: Language;
}
export interface Subjects {
doc_count_error_upper_bound: number;
sum_other_doc_count: number;
buckets: Array<Bucket>;
}
export interface Language {
doc_count_error_upper_bound: number;
sum_other_doc_count: number;
buckets: Array<Bucket>;
}
export interface Bucket {
key: string;
doc_count: number;
}
// // Needed?
// export interface Aggregations {
// [key: string]: Aggregation;
// }
// export interface Aggregation {
// buckets: Array<Bucket>;
// }

View File

@ -1,4 +1,9 @@
export interface SolrSettings { // export interface SolrSettings {
// core: string;
// host: string;
// }
export interface OpenSettings {
core: string; core: string;
host: string; host: string;
} }

View File

@ -1,361 +1,620 @@
import api from "../api/api"; import api from "../api/api";
// import { Observable, of } from "rxjs"; // import { Observable, of } from "rxjs";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { map } from "rxjs/operators"; import { tap, map } from "rxjs/operators";
import { Dataset, DbDataset, Suggestion } from "@/models/dataset"; import { Dataset, DbDataset, Suggestion } from "@/models/dataset";
import { SolrResponse } from "@/models/headers"; import { HitHighlight, OpenSearchResponse, SolrResponse } from "@/models/headers";
import { ActiveFilterCategories } from "@/models/solr"; import { ActiveFilterCategories } from "@/models/solr";
import { VUE_API } from "@/constants"; import { VUE_API } from "@/constants";
// import { deserialize, instanceToInstance } from "class-transformer";
import { deserialize } from "class-transformer"; import { deserialize } from "class-transformer";
// import { OAI_DATASETS } from "./mock-oai-datasets";
// import { OaiDataset, OaiPerson } from "@/models/oai";
// import xml2js from "xml2js";
class DatasetService { class DatasetService {
// for the autocomplete search /**
public searchTerm(term: string, solrCore: string, solrHost: string): Observable<Dataset[]> { * Fetch data from the OpenSearch endpoint with fuzzy search enabled.
// solr endpoint * This function allows for misspellings in the search term and boosts
// const host = 'http://voyagerdemo.com/'; * the relevance of matches in the title, author, and subject fields.
// const host = 'https://www.tethys.at/';'' *
const host = "https://" + solrHost; * @param {string} searchTerm - The search term to query.
const path = "/solr/" + solrCore + "/select?"; */
// const base = "https://geomon.geologie.ac.at/52n-sos-webapp/api/features"; //host + path;
/* https://tethys.at/solr/rdr_data/select?&0=fl%3Did%2Clicence%2Cserver_date_published%2Cabstract_output%2Cidentifier%2Ctitle_output%2Ctitle_additional%2Cauthor%2Csubject%2Cdoctype&q=%2A
&q.op=or&defType=edismax&qf=title%5E3%20author%5E2%20subject%5E1&indent=on&wt=json&rows=10&start=0&sort=server_date_published%20desc&facet=on&json.facet.language=%7B%20type%3A%20%22
terms%22%2C%20field%3A%20%22language%22%20%7D&json.facet.subject=%7B%20type%3A%20%22terms%22%2C%20field%3A%20%22subject%22%2C%20limit%3A%20-1%20%7D&json.facet.year=%7B%20type%3A%20%22
terms%22%2C%20field%3A%20%22year%22%20%7D&json.facet.author=%7B%20type%3A%20%22terms%22%2C%20field%3A%20%22author_facet%22%2C%20limit%3A%20-1%20%7D
*/
// private openSearchUrl = "http://opensearch.geoinformation.dev/tethys-records/_search";
// private openSearchUrl = "http://192.168.21.18/tethys-records/_search";
public searchTerm(term: string, openCore: string, openHost: string): Observable<{ datasets: Dataset[], highlights: HitHighlight[] }> {
// OpenSearch endpoint
const host = openHost; // When using local OpenSearch dev endpoint
const path = "/" + openCore + "/_search";
const base = host + path; const base = host + path;
/**
//const fields = 'id,server_date_published,abstract_output,title_output,title_additional,author,subject'; // fields we want returned * The match query used for title, author, and subjects fields is case-insensitive by default. The standard analyzer is typically used, which lowercases the terms.
const fields = [ * The wildcard query is case-sensitive by default. To make it case-insensitive, it is needed to use a lowercase filter */
"id", const lowercaseTerm = term.toLowerCase(); // Lowercase the search term
"licence", const body = {
"server_date_published", query: {
"abstract_output", bool: {
"title_output", should: [
"title_additional", { match: { title: { query: term, fuzziness: "AUTO", boost: 3 } } },
"author", { match: { author: { query: term, fuzziness: "AUTO", boost: 2 } } },
"subject", { match: { subjects: { query: term, fuzziness: "AUTO", boost: 1 } } }, // In SOLR is "subject"!
"doctype", { wildcard: { title: { value: `${lowercaseTerm}*`, boost: 3 } } },
].toString(); { wildcard: { author: { value: `${lowercaseTerm}*`, boost: 2 } } },
{ wildcard: { subjects: { value: `${lowercaseTerm}*`, boost: 1 } } } // In SOLR is "subject"!
//var dismaxFields = "title^3 abstract^2 subject^1"; ],
const qfFields = "title^3 author^2 subject^1"; minimum_should_match: 1
// let params = "fl=" + fields; }
// // if (term == "*%3A*") { // *: },
// // params += "&defType=edismax&wt=json&indent=on"; //edismax size: 10,
// // } else { from: 0,
// params += "&defType=edismax&qf=" + qfFields + "&wt=json&indent=on"; //dismax // sort: [{ server_date_published: { order: "desc" } }],
// // } sort: [{ _score: { order: "desc" } }], // Sort by _score in descending order
track_scores: true, // This ensures "_score" is included even when sorting by other criteria. Otherwise the relevance score is not calculated
// const query = "&q=" + term + "*"; aggs: {
// const apiU = base + params + query; subjects: { terms: { field: "subjects.keyword", size: 1000 } }, // In SOLR is "subject"!
language: { terms: { field: "language" } }, // << ".keyword" HAS TO BE REMOVED. OTHERWISE BUCKETS ARE NOT OBTAINED FOR THIS
const q_params = { author: { terms: { field: "author.keyword", size: 1000 } },
"0": "fl=" + fields, year: { terms: { field: "year", size: 100 } } // << ".keyword" HAS TO BE REMOVED. OTHERWISE BUCKETS ARE NOT OBTAINED FOR THIS
q: term + "*", },
defType: "edismax", // // CONTABO ================================================================================
qf: qfFields, // aggs: {
indent: "on", // subjects: { terms: { field: "subjects.keyword", size: 1000 } }, // In SOLR is "subject"!
wt: "json", // language: { terms: { field: "language.keyword" } }, // << ".keyword" HAS TO BE REMOVED. OTHERWISE BUCKETS ARE NOT OBTAINED FOR THIS
// author: { terms: { field: "author.keyword", size: 1000 } },
// year: { terms: { field: "year.keyword", size: 100 } } // << ".keyword" HAS TO BE REMOVED. OTHERWISE BUCKETS ARE NOT OBTAINED FOR THIS
// },
// // ===========================================================================================
highlight: {
fields: {
title: {},
author: {},
subjects: {}
}
}
}; };
const stations = api.get<SolrResponse>(base, q_params).pipe(map((res: SolrResponse) => res.response.docs)); // Make API call to OpenSearch and return the result
/**
return stations; * When a POST request is made to the OpenSearch server using the api.post<OpenSearchResponse> method, the response received from OpenSearch is an object that includes various details about the search results.
* One of the key properties of this response object is _source, which is an array of documents (datasets) that match the search criteria.
* It is used the pipe method to chain RxJS operators to the Observable returned by api.get. The map operator is used to transform the emitted items of the Observable.
*/
return api.post<OpenSearchResponse>(base, body).pipe(
// tap(response => console.log("OpenSearchResponse:", response)), // Log the complete response
// tap(response => console.log("Aggre:", response.aggregations?.subjects.buckets[0])), // log the first subject of the array of subjects returned
// tap(response => console.log("Hits:", response.hits)), // log the first subject of the array of subjects returned
// map(response => response.hits.hits.map(hit => hit._source))
map(response => ({
datasets: response.hits.hits.map(hit => hit._source),
highlights: response.hits.hits.map(hit => hit.highlight)
}))
);
} }
public facetedSearch( // // For the autocomplete search. Method to perform a search based on a term
// public searchTerm_SOLR(term: string, solrCore: string, solrHost: string): Observable<Dataset[]> {
// // SOLR endpoint
// const host = "https://" + solrHost;
// const path = "/solr/" + solrCore + "/select?";
// const base = host + path;
// //const fields = 'id,server_date_published,abstract_output,title_output,title_additional,author,subject'; // fields we want returned
// const fields = [
// "id",
// "licence",
// "server_date_published",
// "abstract_output",
// "title_output",
// "title_additional",
// "author",
// "subject",
// "doctype",
// ].toString();
// const qfFields = "title^3 author^2 subject^1";
// const q_params = {
// "0": "fl=" + fields,
// q: term + "*",
// defType: "edismax",
// qf: qfFields,
// indent: "on",
// wt: "json",
// };
// // Make API call to Solr and return the result
// /**
// * When a GET request is made to the Solr server using the api.get<SolrResponse> method, the response received from Solr is an object that includes various details about the search results.
// * One of the key properties of this response object is docs, which is an array of documents (datasets) that match the search criteria.
// * It is used the pipe method to chain RxJS operators to the Observable returned by api.get. The map operator is used to transform the emitted items of the Observable.
// */
// const stations = api.get<SolrResponse>(base, q_params).pipe(map((res: SolrResponse) => res.response.docs));
// return stations;
// }
// public facetedSearchOPEN(
// suggestion: Suggestion | string,
// activeFilterCategories: ActiveFilterCategories,
// openCore: string,
// openHost: string,
// start?: string, // Starting page
// ): Observable<OpenSearchResponse> {
// // OpenSearch endpoint
// const host = openHost;
// const path = "/" + openCore + "/_search";
// const base = host + path;
// // Constructing Filters Based on Active Filter Categories
// const filters = Object.entries(activeFilterCategories).map(([category, values]) => {
// if (category === "language" || category === "year") {
// return { terms: { [category]: values } };
// } else {
// return { terms: { [`${category}.keyword`]: values } };
// }
// });
// console.log("filters:", filters);
// // Determine search term and query fields based on the suggestion type
// let query;
// if (typeof suggestion === "string") { // If suggestion is a string, append a wildcard (*) for partial matches
// const lowercaseTerm = suggestion.toLowerCase()
// query = {
// bool: {
// should: [
// { match: { title: { query: suggestion, fuzziness: "AUTO", boost: 3 } } },
// { match: { author: { query: suggestion, fuzziness: "AUTO", boost: 2 } } },
// { match: { subjects: { query: suggestion, fuzziness: "AUTO", boost: 1 } } },
// { wildcard: { title: { value: `${lowercaseTerm}*`, boost: 3 } } },
// { wildcard: { author: { value: `${lowercaseTerm}*`, boost: 2 } } },
// { wildcard: { subjects: { value: `${lowercaseTerm}*`, boost: 1 } } }
// ],
// minimum_should_match: 1
// }
// };
// } else if (suggestion instanceof Suggestion) { // If suggestion is a Suggestion object, form a specific query
// query = {
// match: {
// [suggestion.type]: {
// query: suggestion.value,
// operator: 'and' // all the terms in the query must be present in the field e.g. if is a title, the complete title
// }
// }
// };
// }
// // Set default value for start if not provided
// const startValue = start ? parseInt(start) : 0;
// // console.log(activeFilterCategories);
// // console.log("mainQuery:", mainQuery);
// // Construct the body of the OpenSearch query
// const body = {
// query: {
// bool: {
// must: [
// mainQuery, // Ensure the main query must be satisfied
// ...filters // Ensure all filters must be satisfied
// ]
// }
// },
// // // WORKS // Expected: 12 results
// // query: {
// // bool: {
// // "must": [
// // { "term": { "language": "en" } },
// // { "term": { "subjects.keyword": "Lower Austria" } },
// // { "term": { "subjects.keyword": "counting data" } }
// // ],
// // }
// // },
// // // THIS WORKS: // Expected: 19 results
// // query: {
// // bool: {
// // must: [
// // { "match": { "title": "Blatt" } },
// // { "term": { "subjects": "bayern" } }
// // ],
// // }
// // },
// // // WORKS // Expected: 4 results
// // query: {
// // bool: {
// // "must": [
// // { "match": { "title": "blatt" } },
// // { "term": { "subjects": "bayern" } },
// // { "term": { "subjects": "salzburg" } }
// // ],
// // }
// // },
// // // WORKS // Expected: 2 results
// // query: {
// // bool: {
// // "must": [
// // { "match": { "title": "blatt" } },
// // { "term": { "subjects": "ungarn" } },
// // { "term": { "subjects": "steiermark" } }
// // ],
// // }
// // },
// // WORKS // Expected: 12 results
// query: {
// bool: {
// "must": [
// { "term": { "language": "en" } },
// { "term": { "subjects.keyword": "Lower Austria" } },
// { "term": { "subjects.keyword": "counting data" } }
// ],
// "should": [
// { match: { title: { query: "halger", fuzziness: "AUTO", boost: 3 } } },
// { match: { author: { query: "halger", fuzziness: "AUTO", boost: 2 } } },
// { match: { subjects: { query: "halger", fuzziness: "AUTO", boost: 1 } } },
// { wildcard: { title: { value: "halger", boost: 3 } } },
// { wildcard: { author: { value: "halger", boost: 2 } } },
// { wildcard: { subjects: { value: "halger", boost: 1 } } }
// ],
// minimum_should_match: 1
// }
// },
// size: 10,
// from: startValue,
// sort: [{ _score: { order: "desc" } }],
// track_scores: true,
// aggs: {
// subjects: { terms: { field: "subjects.keyword", size: 1000 } },
// language: { terms: { field: "language" } },
// author: { terms: { field: "author.keyword", size: 1000 } },
// year: { terms: { field: "year", size: 100 } }
// },
// highlight: {
// fields: {
// title: {},
// author: {},
// subjects: {}
// }
// }
// };
// console.log("mainQuery:", mainQuery);
// console.log("filters:", filters);
// console.log("body:", body);
// // Make API call to OpenSearch and return the result
// const stations = api.post<OpenSearchResponse>(base, body);
// return stations;
// }
public facetedSearchOPEN(
suggestion: Suggestion | string, suggestion: Suggestion | string,
activeFilterCategories: ActiveFilterCategories, activeFilterCategories: ActiveFilterCategories,
solrCore: string, openCore: string,
solrHost: string, openHost: string,
start?: string, start?: string, // Starting page
): Observable<SolrResponse> { ): Observable<OpenSearchResponse> {
// solr endpoint // OpenSearch endpoint
// const host = 'http://voyagerdemo.com/'; const host = openHost;
//const host = 'https://www.tethys.at/'; const path = "/" + openCore + "/_search";
//const host = 'https://' + solrHost;
const host = "https://" + solrHost;
const path = "/solr/" + solrCore + "/select?";
const base = host + path; const base = host + path;
const lowercaseTerm = typeof suggestion === 'string' ? suggestion.toLowerCase() : suggestion.value;
// console.log("facetedsearchOPEN > suggestion entered:");
// console.log(suggestion);
// console.log("typeof:", typeof suggestion);
/**
* The query construction depends on whether the suggestion is a string or a Suggestion object. */
// When suggestion is a string:
const mainQuery = typeof suggestion === 'string'
? {
bool: {
should: [
{ match: { title: { query: suggestion, fuzziness: "AUTO", boost: 3 } } },
{ match: { author: { query: suggestion, fuzziness: "AUTO", boost: 2 } } },
{ match: { subjects: { query: suggestion, fuzziness: "AUTO", boost: 1 } } },
{ wildcard: { title: { value: `${lowercaseTerm}*`, boost: 3 } } },
{ wildcard: { author: { value: `${lowercaseTerm}*`, boost: 2 } } },
{ wildcard: { subjects: { value: `${lowercaseTerm}*`, boost: 1 } } }
],
minimum_should_match: 1
}
}
// When suggestion is a suggestion object
: {
match: {
[suggestion.type]: {
query: suggestion.value,
operator: 'and' // all the terms in the query must be present in the field
}
}
};
// CONTABO ====================================================
// // Constructing Filters Based on Active Filter Categories
// const filters = Object.entries(activeFilterCategories).map(([category, values]) => ({
// terms: { [`${category}.keyword`]: values }
// }));
// ================================================================
//const fields = 'id,server_date_published,abstract_output,title_output,title_additional,author,subject'; // fields we want returned // // Constructing Filters Based on Active Filter Categories
const fields = [ // const filters = Object.entries(activeFilterCategories).map(([category, values]) => {
"id", // if (category === "language" || category === "year") {
"licence", // return { terms: { [category]: values } };
"server_date_published", // } else {
"abstract_output", // return { terms: { [`${category}.keyword`]: values } };
"identifier", // }
"title_output", // });
"title_additional",
"author",
"subject",
"doctype",
].toString();
// const qfFields = "title^3 author^2 subject^1";
// let params = "fl=" + fields;
// if (term == "*%3A*") {
// params += "&defType=edismax&wt=json&indent=on"; //edismax
// } else {
// params += "&defType=dismax&qf=" + qfFields + "&wt=json&indent=on"; //dismax
// }
let term, queryOperator, qfFields; // const filters = Object.entries(activeFilterCategories).map(([category, values]) =>
if (typeof suggestion === "string") { // values.map(value => ({ term: { [`${category}.keyword`]: value } }))
term = suggestion + "*"; // ).flat();
queryOperator = "or";
qfFields = "title^3 author^2 subject^1";
} else if (suggestion instanceof Suggestion) {
term = suggestion.type + ':"' + suggestion.value + '"';
queryOperator = "and";
qfFields = undefined;
}
if (start === undefined) start = "0";
// start = "&start=" + start;
// const facetFields = const filters = Object.entries(activeFilterCategories).map(([category, values]) => {
// "&facet=on&facet.field=language&facet.field={!key=datatype}doctype&facet.field=subject"; //&fq=year:(2019)";//&facet.query=year:2018"; if (category === "language" || category === "year") {
return values.map(value => ({ term: { [category]: value } }));
const filterFields = new Array<string>(); } else {
if (Object.keys(activeFilterCategories).length > 0) { return values.map(value => ({ term: { [`${category}.keyword`]: value } }));
// filterFields = '["';
// const filterFields: string[] = [];
// const facet_fields: FacetFields = res.facets;
let prop: keyof typeof activeFilterCategories;
for (prop in activeFilterCategories) {
const filterItems = activeFilterCategories[prop];
// const filterItems = facetCategory.values;
// filterItems.forEach((filterItem) => {
// console.log(`${key} ${valueArray}`);
filterItems.forEach(function (value: string) {
// filterFields += "&fq=" + key + ':("' + value + '")';
filterFields.push(prop + ':("' + value + '")');
// filterFields += prop + ":" + value;
});
} }
// filterFields += '"]'; }).flat();
}
// const query = "&sort=server_date_published desc" + "&q=" + term;
// const api = // console.log(activeFilterCategories);
// base + params + limit + start + query + filterFields + facetFields; console.log("mainQuery:", mainQuery);
console.log("filters:", filters);
// https://solr.apache.org/guide/8_4/json-request-api.html const body = {
const q_params = { // query: {
"0": "fl=" + fields, // bool: {
q: term, // must: query, // Contains the main query constructed earlier.
"q.op": queryOperator, // filter: filters // Contains the filters constructed from activeFilterCategories.
defType: "edismax", // // filter: [
qf: qfFields, // // { "terms": { "subjects.keyword": ["Lower Austria", "counting data"] } },
// df: "title", // // { "term": { "language": "en" } }
indent: "on", // // ] // Contains the filters constructed from activeFilterCategories.
wt: "json", // }
rows: 10, // },
// fq: ["subject:Steiermark", "language:de"],
fq: filterFields, query: {
start: start, bool: {
sort: "server_date_published desc", must: [
facet: "on", mainQuery, // Ensure the main query must be satisfied
// "facet.field": "language", ...filters // Ensure all filters must be satisfied
"json.facet.language": '{ type: "terms", field: "language" }', ]
"json.facet.subject": '{ type: "terms", field: "subject", limit: -1 }', }
"json.facet.year": '{ type: "terms", field: "year" }', },
"json.facet.author": '{ type: "terms", field: "author_facet", limit: -1 }',
// // THIS DOESNT WORK // Expected: 12 results, Ouput: 16
// query: {
// bool: {
// "must": [
// { "term": { "language": "en" } },
// { "terms": { "subjects.keyword": ["Lower Austria", "counting data"] } }
// ],
// "should": [
// { match: { title: { query: "halger", fuzziness: "AUTO", boost: 3 } } },
// { match: { author: { query: "halger", fuzziness: "AUTO", boost: 2 } } },
// { match: { subjects: { query: "halger", fuzziness: "AUTO", boost: 1 } } },
// { wildcard: { title: { value: "halger", boost: 3 } } },
// { wildcard: { author: { value: "halger", boost: 2 } } },
// { wildcard: { subjects: { value: "halger", boost: 1 } } }
// ],
// minimum_should_match: 1
// }
// },
size: 10,
from: start ? parseInt(start) : 0,
sort: [{ _score: { order: "desc" } }],
track_scores: true,
aggs: { // Defines aggregations for facets
// terms: Aggregation type that returns the most common terms in a field.
// !For a large number of terms setting an extremely large size might not be efficient
// If you genuinely need all unique terms and expect a large number of them, consider using a composite aggregation for more efficient pagination of terms.
subjects: { terms: { field: "subjects.keyword", size: 1000 } }, // In SOLR is "subject"!
language: { terms: { field: "language" } }, // ".keyword" HAS TO BE REMOVED. OTHERWISE BUCKETS ARE NOT OBTAINED FOR THIS
author: { terms: { field: "author.keyword", size: 1000 } },
year: { terms: { field: "year", size: 100 } } // ".keyword" HAS TO BE REMOVED. OTHERWISE BUCKETS ARE NOT OBTAINED FOR THIS
},
// CONTABO ================================================================================
// aggs: {
// subjects: { terms: { field: "subjects.keyword", size: 1000 } }, // In SOLR is "subject"!
// language: { terms: { field: "language.keyword" } }, // << ".keyword" HAS TO BE REMOVED. OTHERWISE BUCKETS ARE NOT OBTAINED FOR THIS
// author: { terms: { field: "author.keyword", size: 1000 } },
// year: { terms: { field: "year.keyword", size: 100 } } // << ".keyword" HAS TO BE REMOVED. OTHERWISE BUCKETS ARE NOT OBTAINED FOR THIS
// },
// ===========================================================================================
highlight: {
fields: {
title: {},
author: {},
subjects: {}
}
}
}; };
console.log("body:", body);
const stations = api.get<SolrResponse>(base, q_params); const stations = api.post<OpenSearchResponse>(base, body);
// .pipe(map((res) => res.response.docs));
return stations; return stations;
} }
// /**
// * This method performs a faceted search on a Solr core. Faceted search allows the user to filter search results based on various categories (facets)
// */
// public facetedSearch_SOLR(
// suggestion: Suggestion | string,
// activeFilterCategories: ActiveFilterCategories,
// solrCore: string,
// solrHost: string,
// start?: string, // Starting page
// ): Observable<SolrResponse> {
// // console.log("face:", suggestion);
// // console.log(activeFilterCategories);
// // console.log(solrCore);
// // console.log(solrHost);
// // console.log(start);
// console.log("facetedsearchSOLR > suggestion entered:");
// console.log(suggestion);
// // Construct Solr query parameters
// const host = "https://" + solrHost;
// const path = "/solr/" + solrCore + "/select?";
// const base = host + path;
// const fields = [
// "id",
// "licence",
// "server_date_published",
// "abstract_output",
// "identifier",
// "title_output",
// "title_additional",
// "author",
// "subject",
// "doctype",
// ].toString();
// // Determine search term, query operator, and query fields based on the suggestion type. Depending on whether suggestion is a string or a Suggestion object, it constructs the search term and query fields differently.
// let term, queryOperator, qfFields;
// if (typeof suggestion === "string") { // f suggestion is a string, it appends a wildcard (*) for partial matches.
// term = suggestion + "*";
// queryOperator = "or";
// qfFields = "title^3 author^2 subject^1";
// } else if (suggestion instanceof Suggestion) { // If suggestion is a Suggestion object, it forms a more specific query based on the type and value of the suggestion.
// term = suggestion.type + ':"' + suggestion.value + '"';
// queryOperator = "and";
// qfFields = undefined;
// }
// // Set default value for start if not provided
// if (start === undefined) start = "0";
// // Construct filter fields based on active filter categories
// const filterFields = new Array<string>();
// if (Object.keys(activeFilterCategories).length > 0) {
// /* Declare variable prop with a type that is a key of the activeFilterCategories. The 'keyof typeof' activeFilterCategories type represents all possible keys
// that can exist on the activeFilterCategories --> prop can only be assigned a value that is a key of the activeFilterCategories object */
// let prop: keyof typeof activeFilterCategories;
// for (prop in activeFilterCategories) {
// const filterItems = activeFilterCategories[prop];
// filterItems.forEach(function (value: string) {
// filterFields.push(prop + ':("' + value + '")');
// // e.g. Array [ 'subject:("Vektordaten")', 'author:("GeoSphere Austria, ")' ]
// });
// }
// }
// // https://solr.apache.org/guide/8_4/json-request-api.html
// // Construct Solr query parameters
// const q_params = {
// "0": "fl=" + fields,
// q: term,
// "q.op": queryOperator,
// defType: "edismax",
// qf: qfFields,
// // df: "title",
// indent: "on",
// wt: "json",
// rows: 10,
// // fq: ["subject:Steiermark", "language:de"],
// fq: filterFields,
// start: start,
// sort: "server_date_published desc",
// facet: "on",
// // "facet.field": "language",
// "json.facet.language": '{ type: "terms", field: "language" }',
// "json.facet.subject": '{ type: "terms", field: "subject", limit: -1 }',
// "json.facet.year": '{ type: "terms", field: "year" }',
// "json.facet.author": '{ type: "terms", field: "author_facet", limit: -1 }',
// };
// /* E.g.
// {"0":"fl=id,licence,server_date_published,abstract_output,identifier,title_output,title_additional,author,subject,doctype","q":"*","q.op":"or","defType":"edismax",
// "qf":"title^3 author^2 subject^1",
// "indent":"on","wt":"json","rows":10,
// "fq":["subject:(\"Vektordaten\")","author:(\"GeoSphere Austria, \")"],
// "start":"0","sort":"server_date_published desc","facet":"on",
// "json.facet.language":"{ type: \"terms\", field: \"language\" }",
// "json.facet.subject":"{ type: \"terms\", field: \"subject\", limit: -1 }",
// "json.facet.year":"{ type: \"terms\", field: \"year\" }",
// "json.facet.author":"{ type: \"terms\", field: \"author_facet\", limit: -1 }"}
// */
// // console.log(JSON.stringify(q_params));
// // Make API call to Solr and return the result
// const stations = api.get<SolrResponse>(base, q_params);
// return stations;
// }
// Method to fetch years
public getYears(): Observable<string[]> { public getYears(): Observable<string[]> {
// const heroes = of(HEROES);
// const host = "https:" + VUE_API;
const host = VUE_API; const host = VUE_API;
const path = "/api/years"; const path = "/api/years";
const base = host + path; const base = host + path;
const years = api.get<string[]>(base); const years = api.get<string[]>(base);
// this.messageService.add('HeroService: fetched heroes');
return years; return years;
} }
// Method to fetch documents for a specific year
public getDocuments(year: string): Observable<Array<DbDataset>> { public getDocuments(year: string): Observable<Array<DbDataset>> {
// const host = "https:" + VUE_API;
const host = VUE_API; const host = VUE_API;
const path = "/api/sitelinks/" + year; const path = "/api/sitelinks/" + year;
const base = host + path; const base = host + path;
const documents: Observable<DbDataset[]> = api.get<Array<DbDataset>>(base); const documents: Observable<DbDataset[]> = api.get<Array<DbDataset>>(base);
// this.messageService.add('HeroService: fetched heroes');
return documents; return documents;
} }
// Method to fetch a dataset by its ID
public getDataset(id: number): Observable<DbDataset> { public getDataset(id: number): Observable<DbDataset> {
// const host = "https:" + VUE_API;
const host = VUE_API; const host = VUE_API;
const path = "/api/dataset/" + id; const path = "/api/dataset/" + id;
const apiUrl = host + path; const apiUrl = host + path;
const dataset = api.get<DbDataset>(apiUrl).pipe(map((res) => this.prepareDataset(res))); const dataset = api.get<DbDataset>(apiUrl).pipe(map((res) => this.prepareDataset(res)));
// const dataset = api.get<DbDataset>(apiUrl).pipe(map((res) => this.prepareDataset(res, apiUrl)));
// this.messageService.add('HeroService: fetched heroes');
return dataset; return dataset;
} }
// Method to fetch a dataset by its DOI
public getDatasetByDoi(doi: string): Observable<DbDataset> { public getDatasetByDoi(doi: string): Observable<DbDataset> {
// const host = "https:" + VUE_API;
const host = VUE_API; const host = VUE_API;
const path = "/api/dataset/10.24341/tethys." + doi; const path = "/api/dataset/10.24341/tethys." + doi;
const apiUrl = host + path; const apiUrl = host + path;
const dataset = api.get<DbDataset>(apiUrl).pipe(map((res) => this.prepareDataset(res))); const dataset = api.get<DbDataset>(apiUrl).pipe(map((res) => this.prepareDataset(res)));
// const dataset = api.get<DbDataset>(apiUrl).pipe(map((res) => this.prepareDataset(res, apiUrl)));
// this.messageService.add('HeroService: fetched heroes');
return dataset; return dataset;
} }
// public getOaiDatasets(): Observable<OaiDataset[]> { // Method to prepare dataset object
// const apiUrl = "https://data.tethys.at/oai?verb=ListRecords&metadataPrefix=oai_datacite";
// const oaiDatasets = api.get<string>(apiUrl).pipe(
// map(
// (response: string) => {
// // const parser = new DOMParser();
// // const xmlDoc: XMLDocument = parser.parseFromString(response, "application/xml");
// // const xslDoc = parser.parseFromString(this.xsl, "application/xml");
// // const xsltProcessor = new XSLTProcessor();
// // xsltProcessor.importStylesheet(xslDoc);
// // console.log(xmlDoc);
// // const xmlDom = xsltProcessor.transformToDocument(xmlDoc);
// // const serializer = new XMLSerializer();
// // const html = serializer.serializeToString(xmlDom.documentElement);
// // console.log(html);
// // const arrOai = new Array<OaiDataset>();
// // return arrOai;
// const arrOai = this.parseXML(response);
// return arrOai;
// // .then((data) => {
// // return data;
// // });
// },
// // (error: string) => this.errorHandler(error),
// ),
// );
// // const oaiDatasets = of(OAI_DATASETS);
// // this.messageService.add('HeroService: fetched heroes');
// return oaiDatasets;
// }
// private parseXML(xmlStr: string): Array<OaiDataset> {
// // let k = "";
// const arr: OaiDataset[] = [];
// const domParser = new DOMParser();
// const doc = domParser.parseFromString(xmlStr, "application/xml");
// const records = doc.getElementsByTagName("ListRecords")[0];
// // // const rt = xmlNode.resumptionToken;
// // for (let i = 0; i < records.length; i++) {
// // console.log(records[i].getAttribute("name"));
// // }
// const parser: xml2js.Parser = new xml2js.Parser({
// trim: true,
// explicitArray: false,
// ignoreAttrs: false,
// // mergeAttrs: true,
// });
// parser.parseString(records.outerHTML, function (err: Error | null, result: any) {
// const xmlNode = result.ListRecords;
// // const rt = xmlNode.resumptionToken;
// for (const rNode in xmlNode.record) {
// const item = xmlNode.record[rNode];
// const dc = item.metadata.resource;
// const t = dc.titles.title;
// const id = dc.identifier._;
// const lang = "en"; //dc.titles.title.attributes("xml",True)->lang;
// let title: string;
// if (lang == "en" && t.length > 1) {
// title = t[1]._;
// } else {
// title = t[0]._;
// }
// let creator = "";
// if (dc.creators.creator instanceof Array) {
// dc.creators.creator.forEach((person: OaiPerson) => {
// creator += person.creatorName + "; ";
// });
// } else {
// creator += dc.creators.creator.creatorName._;
// }
// let contributor = "";
// if (dc.contributors) {
// if (dc.contributors.contributor instanceof Array) {
// dc.contributors.contributor.forEach((person: OaiPerson) => {
// contributor += person.contributorName + "; ";
// });
// } else {
// contributor += dc.contributors.contributor.contributorName;
// }
// }
// // ?.map((u: any) => u.creatorName._).join("; ");
// // foreach ($dc->creators->creator as $c) {
// // foreach ($c->creatorName as $d) {
// // if (count(explode(',',$d)) > 1) {
// // $creator .= explode(',',$d)[0] . ', ' . substr(explode(',',$d)[1],1,1) . '; ';
// // } else {
// // $creator .= explode(',',$d)[0];
// // }
// // }
// // }
// const north = dc.geoLocations.geoLocation.geoLocationBox.northBoundLatitude;
// const east = dc.geoLocations.geoLocation.geoLocationBox.eastBoundLongitude;
// const south = dc.geoLocations.geoLocation.geoLocationBox.southBoundLatitude;
// const west = dc.geoLocations.geoLocation.geoLocationBox.westBoundLongitude;
// const subject = dc.subjects.subject.map((u: any) => u._).join(", ");
// const oaiDataset = {
// doi: id,
// title: title,
// creator: creator,
// contributor: contributor,
// subject: subject,
// north: north,
// south: south,
// east: east,
// west: west,
// } as OaiDataset;
// arr.push(oaiDataset);
// }
// // resolve(arr);
// });
// return arr;
// }
// private prepareOAI(xml: any) : Array<OaiDataset> {
// //
// }
// private prepareDataset(datasetObj: DbDataset, apiUrl: string): DbDataset {
private prepareDataset(datasetObj: DbDataset): DbDataset { private prepareDataset(datasetObj: DbDataset): DbDataset {
const dataset = deserialize<DbDataset>(DbDataset, JSON.stringify(datasetObj)); const dataset = deserialize<DbDataset>(DbDataset, JSON.stringify(datasetObj));
dataset.url = document.documentURI; dataset.url = document.documentURI;
// this.internalDatasetId.generateInternalId(dataset);
// if (dataset.seriesParameters) {
// dataset.parameters = dataset.seriesParameters;
// delete dataset.seriesParameters;
// }
return dataset; return dataset;
} }
} }

View File

@ -59,6 +59,8 @@ export default class DatasetDetailComponent extends Vue {
} }
onSearch(suggestion: Suggestion | string): void { onSearch(suggestion: Suggestion | string): void {
console.log("onSearch (dataset-detail.component)");
const host = window.location.host; const host = window.location.host;
const parts = host.split("."); const parts = host.split(".");
if (parts[0] === "doi") { if (parts[0] === "doi") {

View File

@ -11,6 +11,9 @@ import { Suggestion } from "@/models/dataset";
export default class HomeViewComponent extends Vue { export default class HomeViewComponent extends Vue {
public display = ""; public display = "";
/* This method is called when a search suggestion is selected. It takes a parameter suggestion which can be either a Suggestion object or a string.
If it's a string, the method extracts the term and navigates to the "Search" route with the term as a parameter. If it's a Suggestion object, it extracts
the value and type from the suggestion and navigates to the "Search" route with both parameters.*/
onSearch(suggestion: Suggestion | string): void { onSearch(suggestion: Suggestion | string): void {
let term; let term;
if (typeof suggestion === "string") { if (typeof suggestion === "string") {
@ -22,6 +25,7 @@ export default class HomeViewComponent extends Vue {
} }
} }
/* This method is called when the user initiates a search. It navigates to the "Search" route with the display property as a parameter. */
search(): void { search(): void {
this.$router.push({ name: "Search", params: { display: this.display } }); this.$router.push({ name: "Search", params: { display: this.display } });
} }

View File

@ -18,13 +18,6 @@
</a> </a>
</div> --> </div> -->
<!-- <div class="column">
<div class="col text-center py-3">
<h1>Tethys Research Data Repository</h1>
<p class="lead">Data Publisher for Geoscience Austria</p>
<hr class="center-line" />
</div>
</div> -->
<div class="column"> <div class="column">
<div class="col text-center py-3"> <div class="col text-center py-3">
<h1>Tethys Research Data Repository</h1> <h1>Tethys Research Data Repository</h1>

View File

@ -3,16 +3,23 @@ import VsInput from "@/components/vs-input/vs-input.vue";
import VsResult from "@/components/vs-result/vs-result.vue"; import VsResult from "@/components/vs-result/vs-result.vue";
import FacetCategory from "@/components/face-category/facet-category.vue"; import FacetCategory from "@/components/face-category/facet-category.vue";
import ActiveFacetCategory from "@/components/active-facet-category/active-facet-category.vue"; import ActiveFacetCategory from "@/components/active-facet-category/active-facet-category.vue";
import { SolrSettings } from "@/models/solr"; // import { SolrSettings } from "@/models/solr";
import { OpenSettings } from "@/models/solr";
// import { DatasetService } from "@/services/dataset.service"; // import { DatasetService } from "@/services/dataset.service";
import DatasetService from "../../services/dataset.service"; import DatasetService from "../../services/dataset.service";
import { Suggestion, Dataset, SearchType } from "@/models/dataset"; import { Suggestion, Dataset, SearchType } from "@/models/dataset";
import { SolrResponse, FacetFields, FacetItem, FacetResults, FacetInstance } from "@/models/headers"; // import { SolrResponse, FacetFields, FacetItem, FacetResults, FacetInstance } from "@/models/headers";
// import { SolrResponse, FacetFields, FacetItem, FacetResults, FacetInstance, OpenSearchResponse, HitHighlight } from "@/models/headers";
import { FacetFields, FacetItem, FacetResults, FacetInstance, OpenSearchResponse, HitHighlight } from "@/models/headers";
import { ActiveFilterCategories } from "@/models/solr"; import { ActiveFilterCategories } from "@/models/solr";
import { SOLR_HOST, SOLR_CORE } from "@/constants"; // import { SOLR_HOST, SOLR_CORE } from "@/constants";
import { IPagination } from "@/models/pagination"; import { IPagination } from "@/models/pagination";
import PaginationComponent from "@/components/PaginationComponent.vue"; import PaginationComponent from "@/components/PaginationComponent.vue";
import { OPEN_HOST, OPEN_CORE } from "@/constants";
// Decorate the component and define its name and components
@Component({ @Component({
name: "SearchViewComponent", name: "SearchViewComponent",
components: { components: {
@ -23,6 +30,8 @@ import PaginationComponent from "@/components/PaginationComponent.vue";
PaginationComponent, PaginationComponent,
}, },
}) })
// Define the SearchViewComponent class
export default class SearchViewComponent extends Vue { export default class SearchViewComponent extends Vue {
@Prop() @Prop()
display!: string; display!: string;
@ -46,16 +55,22 @@ export default class SearchViewComponent extends Vue {
}; };
loaded = false; loaded = false;
numFound!: number; numFound!: number;
private solr: SolrSettings = { // private solr: SolrSettings = {
core: SOLR_CORE, //"rdr_data", // SOLR.core; // core: SOLR_CORE, //"rdr_data", // SOLR.core;
host: SOLR_HOST, //"tethys.at", // host: SOLR_HOST, //"tethys.at",
// core: "test_data", // SOLR.core; // };
// host: "repository.geologie.ac.at",
private open: OpenSettings = {
core: OPEN_CORE, //"rdr_data", // SOLR.core;
host: OPEN_HOST, //"tethys.at",
}; };
// private rdrAPI!: DatasetService;
private error = ""; private error = "";
// Computed property to get search term as string
get stringSearchTerm(): string { get stringSearchTerm(): string {
// console.log("stringSearchTerm:", this.searchTerm);
if (typeof this.searchTerm === "string") { if (typeof this.searchTerm === "string") {
return this.searchTerm; return this.searchTerm;
} else if (this.searchTerm instanceof Suggestion) { } else if (this.searchTerm instanceof Suggestion) {
@ -65,6 +80,7 @@ export default class SearchViewComponent extends Vue {
} }
} }
// Method to check if a search term is present
hasSearchTerm(): boolean { hasSearchTerm(): boolean {
if (typeof this.searchTerm === "string" && this.searchTerm !== "") { if (typeof this.searchTerm === "string" && this.searchTerm !== "") {
return true; return true;
@ -77,18 +93,25 @@ export default class SearchViewComponent extends Vue {
// getKeyName(value: string) { // getKeyName(value: string) {
// return Object.entries(Suggestion).find(([key, val]) => val === value)?.[0]; // return Object.entries(Suggestion).find(([key, val]) => val === value)?.[0];
// } // }
// Method to get enum key by enum value
getEnumKeyByEnumValue<T extends { [index: string]: string }>(myEnum: T, enumValue: string): keyof T | null { getEnumKeyByEnumValue<T extends { [index: string]: string }>(myEnum: T, enumValue: string): keyof T | null {
const keys = Object.keys(myEnum).filter((x) => myEnum[x] == enumValue); const keys = Object.keys(myEnum).filter((x) => myEnum[x] == enumValue);
return keys.length > 0 ? keys[0] : null; return keys.length > 0 ? keys[0] : null;
// return keys[0]; // return keys[0];
} }
// Lifecycle hook: executed before the component is mounted
beforeMount(): void { beforeMount(): void {
// console.log("beforeMount!");
// this.rdrAPI = new DatasetService(); // this.rdrAPI = new DatasetService();
// Trigger search based on provided display and type props
if (this.display != "" && this.type != undefined) { if (this.display != "" && this.type != undefined) {
const enumKey: "Title" | "Author" | "Subject" | null = this.getEnumKeyByEnumValue(SearchType, this.type); const enumKey: "Title" | "Author" | "Subject" | null = this.getEnumKeyByEnumValue(SearchType, this.type);
if (enumKey) { if (enumKey) {
const suggestion = new Suggestion(this.display, SearchType[enumKey]); const suggestion = new Suggestion(this.display, "NO-IDEA", SearchType[enumKey]);
// const suggestion = new Suggestion(this.display, "" , SearchType[enumKey]);
this.onSearch(suggestion); this.onSearch(suggestion);
} else { } else {
this.onSearch(this.display); this.onSearch(this.display);
@ -100,209 +123,327 @@ export default class SearchViewComponent extends Vue {
} }
} }
// onSearch(term: string): void { // Method to trigger a search
onSearch(suggestion: Suggestion | string): void { onSearch(suggestion: Suggestion | string): void {
// let queryOperator; // console.log("ONSEARCH");
// if (typeof suggestion === "string") {
// suggestion = suggestion + "*"; // Reset active filter categories and facet results
// queryOperator = "or";
// } else if (suggestion instanceof Suggestion) {
// // term = suggestion.value;
// queryOperator = "and";
// }
// if (term) {
// term = term.trim();
// } else {
// term = "*%3A*";
// }
this.activeFilterCategories = new ActiveFilterCategories(); this.activeFilterCategories = new ActiveFilterCategories();
this.facets = new FacetResults(); this.facets = new FacetResults();
// this.facets = {};
this.searchTerm = suggestion; this.searchTerm = suggestion;
DatasetService.facetedSearch(suggestion, this.activeFilterCategories, this.solr.core, this.solr.host, undefined).subscribe({ // console.log("ONSEARCH > suggestion: ", suggestion);
next: (res: SolrResponse) => this.dataHandler(res),
// /* Perform faceted search. The method returns an Observable, and the code subscribes to this Observable to handle the response. If the response is successful, it calls the dataHandler method
// with the Solr response as a parameter. If there is an error, it calls the errorHandler method with the error message as a parameter */
// DatasetService.facetedSearch(suggestion, this.activeFilterCategories, this.solr.core, this.solr.host, undefined).subscribe({
// next: (res: SolrResponse) => this.dataHandler(res),
// error: (error: string) => this.errorHandler(error),
// });
DatasetService.facetedSearchOPEN(suggestion, this.activeFilterCategories, this.open.core, this.open.host, undefined).subscribe({
// next: (res: { datasets: Dataset[], highlights: HitHighlight[] }) => this.dataHandlerOpen(res.datasets, res.highlights),
next: (res: OpenSearchResponse) => this.dataHandlerOPEN(res),
error: (error: string) => this.errorHandler(error), error: (error: string) => this.errorHandler(error),
}); });
} }
private dataHandler(res: SolrResponse, filterItem?: FacetItem): void { // Handle the search results
// this.results = datasets; private dataHandlerOPEN(res: OpenSearchResponse, filterItem?: FacetItem): void {
this.results = res.response.docs; this.results = res.hits.hits.map(hit => hit._source);
this.numFound = res.response.numFound; this.numFound = res.hits.total.value;
// pagination // console.log("dataHandlerOPEN (results, numFound):");
this.pagination["total"] = res.response.numFound; // console.log(this.results);
this.pagination["perPage"] = res.responseHeader.params.rows as number; // console.log(this.numFound);
// this.pagination["currentPage"] = 1; // console.log(res.hits.hits);
this.pagination["data"] = res.response.docs; // console.log(res.hits.total.value); console.log(res);
this.pagination.lastPage = Math.ceil(this.pagination.total / this.pagination.perPage); // console.log("results:");
console.log(res);
// facets
// const facet_fields = res.facet_counts.facet_fields; // for (const key in this.results) {
// for (const prop in facet_fields) { // if (Object.prototype.hasOwnProperty.call(this.results, key)) {
// const facetCategory: FacetCategory<any> = facet_fields[prop]; // const element = this.results[key];
// const facetValues = facetCategory.key.values.map((facet_value: any, i: number) => { // // console.log(element.abstract[0]);
// if (i % 2 === 0 && typeof facet_value == "string") { // // console.log(element.language);
// //var rObj = { value: facet, count: facet_fields[prop][i + 1] }; // console.log(element.server_date_published);
// // FiletrItem with value and count // }
// const rObj = new FilterItem(facet_value, facetCategory.key.values[i + 1]);
// return rObj;
// }
// });
// .filter(function (el: FilterItem) {
// return el != null && el.count > 0;
// });
// //this.facets.push({ filterName: prop, values: facetValues });
// this.facets[prop] = facetValues;
// } // }
const facet_fields: FacetFields = res.facets; this.pagination.total = res.hits.total.value;
let prop: keyof typeof facet_fields; this.pagination.perPage = 10;
for (prop in facet_fields) { this.pagination.data = this.results;
const facetCategory = facet_fields[prop]; this.pagination.lastPage = Math.ceil(this.pagination.total / this.pagination.perPage);
if (facetCategory.buckets) {
const facetItems: Array<FacetItem> = facetCategory.buckets;
let facetValues = facetItems.map((facetItem) => { // if (res.aggregations) {
let rObj: FacetItem; // const facet_fields = res.aggregations;
if (filterItem?.val == facetItem.val) {
rObj = filterItem;
} else if (this.facets[prop]?.some((e) => e.val === facetItem.val)) {
// console.log(facetValue + " is included")
const indexOfFacetValue = this.facets[prop].findIndex((i) => i.val === facetItem.val);
// console.log(indexOfFacetValue);
rObj = this.facets[prop][indexOfFacetValue];
rObj.count = facetItem.count;
// rObj = new FacetItem(val, count);
} else {
rObj = new FacetItem(facetItem.val, facetItem.count);
}
return rObj;
});
facetValues = facetValues.filter(function (el) { // let prop: keyof typeof facet_fields;
return el != null && el.count > 0;
}); // for (prop in facet_fields) {
// this.facets[prop] = facetCategory; // const facetCategory = facet_fields[prop];
this.facets[prop] = facetValues; // if (facetCategory.buckets) {
// const facetItems = facetCategory.buckets.map(bucket => new FacetItem(bucket.key, bucket.doc_count));
// this.facets[prop] = facetItems.filter(el => el.count > 0);
// }
// }
// }
if (res.aggregations) {
const facet_fields = res.aggregations;
let prop: keyof typeof facet_fields;
// Iterate through facet fields
for (prop in facet_fields) {
const facetCategory = facet_fields[prop];
if (facetCategory.buckets) {
const facetItems = facetCategory.buckets.map(bucket => new FacetItem(bucket.key, bucket.doc_count));
let facetValues = facetItems.map((facetItem) => {
let rObj: FacetItem;
// Check if current facet item matches filter item
if (filterItem?.val == facetItem.val) {
rObj = filterItem;
} else if (this.facets[prop]?.some((e) => e.val === facetItem.val)) {
const indexOfFacetValue = this.facets[prop].findIndex((i) => i.val === facetItem.val);
rObj = this.facets[prop][indexOfFacetValue];
rObj.count = facetItem.count;
} else {
// Create new facet item
rObj = new FacetItem(facetItem.val, facetItem.count);
}
return rObj;
});
// Filter out null values and values with count <= 0
facetValues = facetValues.filter(el => el.count > 0);
this.facets[prop] = facetValues;
}
} }
} }
} }
// // Method to handle search response
// private dataHandler(res: SolrResponse, filterItem?: FacetItem): void {
// // console.log("dataHandlerSOLR (docs, numFound):");
// // console.log(res.response.docs);
// // console.log(res.response.numFound);
// // Update results
// this.results = res.response.docs;
// this.numFound = res.response.numFound;
// // Update pagination
// this.pagination["total"] = res.response.numFound;
// this.pagination["perPage"] = res.responseHeader.params.rows as number;
// this.pagination["data"] = res.response.docs;
// this.pagination.lastPage = Math.ceil(this.pagination.total / this.pagination.perPage);
// const facet_fields: FacetFields = res.facets;
// /* This code declares a variable prop with a type of keys of the facet_fields object. The keyof typeof facet_fields type represents the keys of the facet_fields object.
// This means that prop can only hold values that are keys of the facet_fields object. */
// let prop: keyof typeof facet_fields;
// // Iterate through facet fields
// for (prop in facet_fields) {
// const facetCategory = facet_fields[prop];
// if (facetCategory.buckets) {
// const facetItems: Array<FacetItem> = facetCategory.buckets;
// let facetValues = facetItems.map((facetItem) => {
// let rObj: FacetItem;
// // Check if current facet item matches filter item
// if (filterItem?.val == facetItem.val) {
// rObj = filterItem;
// } else if (this.facets[prop]?.some((e) => e.val === facetItem.val)) {
// // console.log(facetValue + " is included")
// // Update existing facet item with new count
// const indexOfFacetValue = this.facets[prop].findIndex((i) => i.val === facetItem.val);
// // console.log(indexOfFacetValue);
// rObj = this.facets[prop][indexOfFacetValue];
// rObj.count = facetItem.count;
// // rObj = new FacetItem(val, count);
// } else {
// // Create new facet item
// rObj = new FacetItem(facetItem.val, facetItem.count);
// }
// return rObj;
// });
// // Filter out null values and values with count <= 0
// facetValues = facetValues.filter(function (el) {
// return el != null && el.count > 0;
// });
// // this.facets[prop] = facetCategory;
// // Update facet values
// this.facets[prop] = facetValues;
// }
// }
// }
// Method to handle search errors
private errorHandler(err: string): void { private errorHandler(err: string): void {
this.error = err; this.error = err;
// this.loading = false;
} }
// Method to handle pagination
onMenuClick(page: number) { onMenuClick(page: number) {
// const test = page; console.log("onMenuClick");
// console.log(test);
this.pagination.currentPage = page; this.pagination.currentPage = page;
const start = page * this.pagination.perPage - this.pagination.perPage; const start = page * this.pagination.perPage - this.pagination.perPage;
DatasetService.facetedSearch(this.searchTerm, this.activeFilterCategories, this.solr.core, this.solr.host, start.toString()).subscribe( // // Trigger new search with updated pagination parameters
(res: SolrResponse) => this.dataHandler(res), // DatasetService.facetedSearch(this.searchTerm, this.activeFilterCategories, this.solr.core, this.solr.host, start.toString()).subscribe(
(error: string) => this.errorHandler(error), // (res: SolrResponse) => this.dataHandler(res),
); // (error: string) => this.errorHandler(error),
// );
DatasetService.facetedSearchOPEN(this.searchTerm, this.activeFilterCategories, this.open.core, this.open.host, start.toString()).subscribe({
next: (res: OpenSearchResponse) => this.dataHandlerOPEN(res),
error: (error: string) => this.errorHandler(error),
});
} }
// Method to handle facet filtering
onFilter(facetItem: FacetItem): void { onFilter(facetItem: FacetItem): void {
console.log("onFilter");
// Reset current page
this.pagination.currentPage = 1; this.pagination.currentPage = 1;
// console.log(facetItem.val); // console.log(facetItem.val);
// console.log(facetItem.category);
// if (!this.activeFilterCategories.hasOwnProperty(facetItem.category)) { // if (!this.activeFilterCategories.hasOwnProperty(facetItem.category)) {
// Check if filter item already exists
if (!Object.prototype.hasOwnProperty.call(this.activeFilterCategories, facetItem.category)) { if (!Object.prototype.hasOwnProperty.call(this.activeFilterCategories, facetItem.category)) {
this.activeFilterCategories[facetItem.category] = new Array<string>(); this.activeFilterCategories[facetItem.category] = new Array<string>();
// console.log(this.activeFilterCategories);
} }
// if (!this.activeFilterCategories[facetItem.category].some((e) => e === facetItem.val)) { // if (!this.activeFilterCategories[facetItem.category].some((e) => e === facetItem.val)) {
// Check if filter item is not already applied
if (!this.activeFilterCategories[facetItem.category].some((e) => e === facetItem.val)) { if (!this.activeFilterCategories[facetItem.category].some((e) => e === facetItem.val)) {
// Add filter item to active filter categories
this.activeFilterCategories[facetItem.category].push(facetItem.val); this.activeFilterCategories[facetItem.category].push(facetItem.val);
// Trigger new search with updated filter
DatasetService.facetedSearch(this.searchTerm, this.activeFilterCategories, this.solr.core, this.solr.host, undefined).subscribe( // DatasetService.facetedSearch(this.searchTerm, this.activeFilterCategories, this.solr.core, this.solr.host, undefined).subscribe(
(res: SolrResponse) => this.dataHandler(res, facetItem), // (res: SolrResponse) => this.dataHandler(res, facetItem),
(error: string) => this.errorHandler(error), // (error: string) => this.errorHandler(error),
); // );
// alert(this.activeFilterCategories[filter.Category]); // console.log(this.activeFilterCategories);
// var res = await rdrApi.search(this.searchTerm, this.activeFilterCategories, this.solrCore, this.solrHost); DatasetService.facetedSearchOPEN(this.searchTerm, this.activeFilterCategories, this.open.core, this.open.host, undefined).subscribe({
// this.results = res.response.docs; next: (res: OpenSearchResponse) => this.dataHandlerOPEN(res, facetItem),
// this.numFound = res.response.numFound; error: (error: string) => this.errorHandler(error),
});
// // pagination
// this.pagination['total'] = res.response.numFound;
// this.pagination['per_page'] = res.responseHeader.params.rows;
// this.pagination['current_page'] = 1;
// this.pagination['data'] = res.response.docs;
// var facet_fields = res.facet_counts.facet_fields;
// for (var prop in facet_fields) {
// var facetValues = facet_fields[prop].map((facetValue, i) => {
// if (i % 2 === 0) {
// // var rObj = { value: facetValue, count: facet_fields[prop][i + 1] };
// var rObj;
// if (filter.value == facetValue) {
// rObj = filter;
// } else if (this.facets[prop].some(e => e.value === facetValue)) {
// // console.log(facetValue + " is included")
// var indexOfFacetValue = this.facets[prop].findIndex(i => i.value === facetValue);
// // console.log(indexOfFacetValue);
// rObj = this.facets[prop][indexOfFacetValue];
// rObj.count = facet_fields[prop][i + 1];
// } else {
// rObj = new FilterItem(facetValue, facet_fields[prop][i + 1]);
// }
// return rObj;
// }
// }).filter(function (el) {
// return el != null && el.count > 0;
// });
// // this.facets.push({ filterName: prop, values: facetValues });
// this.facets[prop] = facetValues;
} }
} }
onClearFacetCategory(categoryName: string): void { // // // Method to clear facet category filter
// alert(categoryName); // onClearFacetCategory(categoryName: string): void {
// console.log("onClearFacetCategory");
// delete this.activeFilterCategories[categoryName];
// // Trigger new search with updated filter
// DatasetService.facetedSearch(this.searchTerm, this.activeFilterCategories, this.solr.core, this.solr.host, undefined).subscribe({
// next: (res: SolrResponse) => {
// this.results = res.response.docs;
// this.numFound = res.response.numFound;
// // pagination
// this.pagination["total"] = res.response.numFound;
// this.pagination["perPage"] = res.responseHeader.params.rows as number;
// this.pagination["currentPage"] = 1;
// this.pagination["data"] = res.response.docs;
// const facet_fields: FacetFields = res.facets;
// let prop: keyof typeof facet_fields;
// for (prop in facet_fields) {
// const facetCategory: FacetInstance = facet_fields[prop];
// if (facetCategory.buckets) {
// const facetItems: Array<FacetItem> = facetCategory.buckets;
// const facetValues = facetItems.map((facetItem) => {
// let rObj: FacetItem;
// if (this.facets[prop]?.some((e) => e.val === facetItem.val)) {
// // console.log(facetValue + " is included")
// // Update existing facet item with new count
// const indexOfFacetValue = this.facets[prop].findIndex((i) => i.val === facetItem.val);
// // console.log(indexOfFacetValue);
// rObj = this.facets[prop][indexOfFacetValue];
// rObj.count = facetItem.count;
// // rObj = new FacetItem(val, count);
// // if facet ccategory is reactivated category, deactivate all filter items
// if (prop == categoryName) {
// rObj.active = false;
// }
// } else {
// // Create new facet item
// rObj = new FacetItem(facetItem.val, facetItem.count);
// }
// return rObj;
// });
// this.facets[prop] = facetValues;
// }
// }
// },
// error: (error: string) => this.errorHandler(error),
// complete: () => console.log("clear facet category completed"),
// });
// }
// Method to clear facet category filter
onClearFacetCategoryOPEN(categoryName: string): void {
// console.log("onClearFacetCategory");
delete this.activeFilterCategories[categoryName]; delete this.activeFilterCategories[categoryName];
DatasetService.facetedSearch(this.searchTerm, this.activeFilterCategories, this.solr.core, this.solr.host, undefined).subscribe({ // Trigger new search with updated filter
next: (res: SolrResponse) => { DatasetService.facetedSearchOPEN(this.searchTerm, this.activeFilterCategories, this.open.core, this.open.host, undefined).subscribe({
this.results = res.response.docs; next: (res: OpenSearchResponse) => {
this.numFound = res.response.numFound; this.results = res.hits.hits.map(hit => hit._source);
this.numFound = res.hits.total.value;
// pagination
this.pagination["total"] = res.response.numFound; // Update pagination
this.pagination["perPage"] = res.responseHeader.params.rows as number; this.pagination.total = res.hits.total.value;
this.pagination["currentPage"] = 1; this.pagination.perPage = 10;
this.pagination["data"] = res.response.docs; this.pagination.currentPage = 1;
this.pagination.data = this.results;
const facet_fields: FacetFields = res.facets; this.pagination.lastPage = Math.ceil(this.pagination.total / this.pagination.perPage);
let prop: keyof typeof facet_fields;
for (prop in facet_fields) { if (res.aggregations) {
const facetCategory: FacetInstance = facet_fields[prop]; const facet_fields = res.aggregations;
if (facetCategory.buckets) {
const facetItems: Array<FacetItem> = facetCategory.buckets; let prop: keyof typeof facet_fields;
const facetValues = facetItems.map((facetItem) => { for (prop in facet_fields) {
let rObj: FacetItem; const facetCategory = facet_fields[prop];
if (this.facets[prop]?.some((e) => e.val === facetItem.val)) { if (facetCategory.buckets) {
// console.log(facetValue + " is included") const facetItems = facetCategory.buckets.map(bucket => new FacetItem(bucket.key, bucket.doc_count));
const indexOfFacetValue = this.facets[prop].findIndex((i) => i.val === facetItem.val);
// console.log(indexOfFacetValue); const facetValues = facetItems.map((facetItem) => {
rObj = this.facets[prop][indexOfFacetValue]; let rObj: FacetItem;
rObj.count = facetItem.count; if (this.facets[prop]?.some((e) => e.val === facetItem.val)) {
// rObj = new FacetItem(val, count); // Update existing facet item with new count
//if facet ccategory is reactivated category, deactivate all filter items const indexOfFacetValue = this.facets[prop].findIndex((i) => i.val === facetItem.val);
if (prop == categoryName) { rObj = this.facets[prop][indexOfFacetValue];
rObj.active = false; rObj.count = facetItem.count;
// if facet category is reactivated category, deactivate all filter items
if (prop === categoryName) {
rObj.active = false;
}
} else {
// Create new facet item
rObj = new FacetItem(facetItem.val, facetItem.count);
} }
} else { return rObj;
rObj = new FacetItem(facetItem.val, facetItem.count); }).filter(el => el.count > 0); // Filter out items with count <= 0
}
return rObj; this.facets[prop] = facetValues;
}); }
this.facets[prop] = facetValues;
} }
} }
}, },
@ -311,5 +452,4 @@ export default class SearchViewComponent extends Vue {
}); });
} }
// onPaging(page: number): void {}
} }

View File

@ -1,31 +1,11 @@
<template> <template>
<div id="page_style" class="rows site-content page__style page__description" autocomplete="off"> <div id="page_style" class="rows site-content page__style page__description" autocomplete="off">
<div class="container-fluid banner mz-5"> <div class="container-fluid banner mz-5">
<vs-input v-bind:propDisplay="searchTerm" v-bind:placeholder="'Enter your search term...'" @search-change="onSearch"></vs-input> <vs-input v-bind:propDisplay="searchTerm" v-bind:placeholder="'Enter your search term...'" @search-change="onSearch"></vs-input>
</div> </div>
<div class="column is-half is-offset-one-quarter" style="padding-top: 0; margin-top: 0"> <div class="column is-half is-offset-one-quarter" style="padding-top: 0; margin-top: 0">
<!-- <div class="tabs is-centered">
<ul id="id-results-tabs">
<li class="search_tab is-active">
<a target="_self">Web</a>
</li>
<li class="search_tab">
<a target="_self">Images</a>
</li>
<li class="search_tab">
<a target="_self">Videos</a>
</li>
<li class="search_tab">
<a target="_self">Homepages</a>
</li>
<li class="search_tab">
<a target="_self">Food</a>
</li>
<li class="search_tab">
<a target="_self">Books</a>
</li>
</ul>
</div> -->
<div v-if="results.length > 0" class="result-list-info"> <div v-if="results.length > 0" class="result-list-info">
<div v-if="hasSearchTerm()" class="resultheader"> <div v-if="hasSearchTerm()" class="resultheader">
Your search term {{ "'" + stringSearchTerm + "'" }} yielded <strong>{{ numFound }}</strong> results: Your search term {{ "'" + stringSearchTerm + "'" }} yielded <strong>{{ numFound }}</strong> results:
@ -54,7 +34,7 @@
<active-facet-category <active-facet-category
v-bind:filterItems="values" v-bind:filterItems="values"
v-bind:categoryName="key" v-bind:categoryName="key"
@clear-facet-category="onClearFacetCategory" @clear-facet-category="onClearFacetCategoryOPEN"
></active-facet-category> ></active-facet-category>
</span> </span>
</div> </div>

View File

@ -89,8 +89,11 @@ module.exports = {
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: "false", __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: "false",
APP_URL: JSON.stringify(process.env.APP_URL), APP_URL: JSON.stringify(process.env.APP_URL),
VUE_API: JSON.stringify(process.env.VUE_API), VUE_API: JSON.stringify(process.env.VUE_API),
SOLR_HOST: JSON.stringify(process.env.SOLR_HOST), // SOLR_HOST: JSON.stringify(process.env.SOLR_HOST),
SOLR_CORE: JSON.stringify(process.env.SOLR_CORE), // SOLR_CORE: JSON.stringify(process.env.SOLR_CORE),
// OPENSEARCH
OPEN_HOST: JSON.stringify(process.env.OPEN_HOST),
OPEN_CORE: JSON.stringify(process.env.OPEN_CORE),
}), }),
// new NodePolyfillPlugin(), // new NodePolyfillPlugin(),
], ],