From 6d1c1b28c37fb329812a5088209a06505690aeee Mon Sep 17 00:00:00 2001 From: frankporras Date: Thu, 6 Jun 2024 17:11:54 +0200 Subject: [PATCH] OpenSearch - some progress in migration --- src/api/api.ts | 35 ++++- src/components/vs-input/vs-input.ts | 39 ++--- src/models/headers.ts | 56 ++++++- src/services/dataset.service.ts | 148 ++++++------------ .../search-view/search-view-component.ts | 2 +- 5 files changed, 143 insertions(+), 137 deletions(-) diff --git a/src/api/api.ts b/src/api/api.ts index df2ca7a..1b0c5f6 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -1,15 +1,34 @@ -import initializeAxios from "./axiosSetup"; -import { axiosRequestConfiguration } from "./config"; -import { map } from "rxjs/operators"; -import { defer, Observable } from "rxjs"; -import { AxiosResponse } from "axios"; -// https://ichi.pro/de/so-wickeln-sie-axios-mit-typescript-und-react-in-rxjs-ein-118892823169891 +// Import the necessary modules and functions +import initializeAxios from "./axiosSetup"; // Function to initialize the Axios instance +import { axiosRequestConfiguration } from "./config"; // Axios configuration settings +import { map } from "rxjs/operators"; // Operator to transform the items emitted by an Observable +import { defer, Observable } from "rxjs"; // RxJS utilities for creating and working with Observables +import { AxiosResponse } from "axios"; // Axios response type +// Initialize the Axios instance with the provided configuration const axiosInstance = initializeAxios(axiosRequestConfiguration); +// Function to make a GET request using Axios wrapped in an Observable // eslint-disable-next-line const get = (url: string, queryParams?: any): Observable => { - return defer(() => axiosInstance.get(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(url, { params: queryParams })) + // Use map to transform the Axios response to extract the data property + .pipe(map((result: AxiosResponse) => result.data)); }; -export default { get }; +// Function to make a POST request using Axios wrapped in an Observable +const post = (url: string, body: any, queryParams?: any): Observable => { + // Use defer to create an Observable that makes the Axios POST request when subscribed to + return defer(() => axiosInstance.post(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 +}; + +// Export the get function as part of the default export +export default { get, post }; \ No newline at end of file diff --git a/src/components/vs-input/vs-input.ts b/src/components/vs-input/vs-input.ts index 8bb0690..f4dba9e 100644 --- a/src/components/vs-input/vs-input.ts +++ b/src/components/vs-input/vs-input.ts @@ -88,7 +88,9 @@ export default class VsInput extends Vue { // }; const suggestions = new Array(); - console.log("Display:", this.display); + console.log("Display:", this.display); + // console.log("results:", this.results ); + this.results.forEach((dataset) => { @@ -106,23 +108,23 @@ export default class VsInput extends Vue { suggestions.push(suggestion); } } - if (this.find(dataset.author, this.display.toLowerCase()) !== "") { - const author = this.find(dataset.author, this.display.toLowerCase()); + // 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); - if (!hasAuthorSuggestion) { - const suggestion = new Suggestion(author, SearchType.Author); - suggestions.push(suggestion); - } - } - if (this.find(dataset.subject, this.display.toLowerCase()) != "") { - const subject = this.find(dataset.subject, 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); - } - } + // 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.subject, this.display.toLowerCase()) != "") { + // const subject = this.find(dataset.subject, 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; @@ -174,7 +176,8 @@ export default class VsInput extends Vue { private request(): void { console.log("request()"); - DatasetService.searchTerm(this.display, this.solr.core, this.solr.host).subscribe({ + // DatasetService.searchTerm(this.display, this.solr.core, this.solr.host).subscribe({ + DatasetService.searchTerm(this.display).subscribe({ next: (res: Dataset[]) => this.dataHandler(res), error: (error: string) => this.errorHandler(error), complete: () => (this.loading = false), diff --git a/src/models/headers.ts b/src/models/headers.ts index 032de97..0de9230 100644 --- a/src/models/headers.ts +++ b/src/models/headers.ts @@ -7,14 +7,6 @@ export interface SolrResponse { // facet_counts: FacetCount; } -// OPENSEARCH -export interface OpenResponse { - responseHeader: ResponseHeader; - response: ResponseContent; - facets: FacetFields; - // facet_counts: FacetCount; -} - export interface ResponseHeader { status: boolean; QTime: number; @@ -79,3 +71,51 @@ export class FacetItem { } } //#endregion + +// OPENSEARCH +// ======================================================================== +export interface OpenSearchResponse { + took: number; + timed_out: boolean; + _shards: Shards; + hits: Hits; + aggregations?: Aggregations; +} + +export interface Shards { + total: number; + successful: number; + skipped: number; + failed: number; +} + +export interface Hits { + total: Total; + max_score: number; + hits: Array; +} + +export interface Total { + value: number; + relation: string; +} + +export interface Hit { + _index: string; + _id: string; + _score: number; + _source: Dataset; +} + +export interface Aggregations { + [key: string]: Aggregation; +} + +export interface Aggregation { + buckets: Array; +} + +export interface Bucket { + key: string; + doc_count: number; +} diff --git a/src/services/dataset.service.ts b/src/services/dataset.service.ts index e510772..9aec58e 100644 --- a/src/services/dataset.service.ts +++ b/src/services/dataset.service.ts @@ -3,44 +3,12 @@ import api from "../api/api"; import { Observable } from "rxjs"; import { map } from "rxjs/operators"; import { Dataset, DbDataset, Suggestion } from "@/models/dataset"; -import { SolrResponse } from "@/models/headers"; +import { OpenSearchResponse, SolrResponse } from "@/models/headers"; import { ActiveFilterCategories } from "@/models/solr"; import { VUE_API } from "@/constants"; import { deserialize } from "class-transformer"; class DatasetService { - // /* Initial test method to fetch and log data from the local OpenSearch endpoint (new backend) */ - // async fetchDataFromOpenSearch(searchTerm: string): Promise { - // const url = "http://192.168.21.18/tethys-records/_search"; - // const headers = { - // "Content-Type": "application/json", - // }; - // const body = { - // query: { - // match: { - // title: searchTerm, - // }, - // }, - // }; - - // try { - // const response = await fetch(url, { - // method: "POST", - // headers: headers, - // body: JSON.stringify(body), - // }); - - // if (!response.ok) { - // throw new Error(`Failed to fetch data from ${url}, status: ${response.status}`); - // } - - // const data = await response.json(); - // console.log("Data from OpenSearch:", data); - // } catch (error) { - // console.error("Error fetching data:", error); - // } - // } - /** * Fetch data from the OpenSearch endpoint with fuzzy search enabled. * This function allows for misspellings in the search term and boosts @@ -50,7 +18,7 @@ class DatasetService { */ async fetchDataFromOpenSearch(searchTerm: string): Promise { // Define the OpenSearch endpoint URL - const url = "http://192.168.21.18/tethys-records/_search"; + const url = "http://opensearch.geoinformation.dev/tethys-records/_search"; // Set the headers for the POST request const headers = { @@ -149,68 +117,6 @@ class DatasetService { } }; - // // Construct the body of the POST request - // const body = { - // query: { - // bool: { - // // The `should` clause specifies that at least one of these conditions must match - // should: [ - // { - // // Match the search term in the title field with a wildcard - // wildcard: { - // title: { - // value: `${searchTerm}*`, // Wildcard search for terms starting with searchTerm - // boost: 3 // Boosting the relevance of title matches - // } - // } - // }, - // { - // // Match the search term in the author field with a wildcard - // wildcard: { - // author: { - // value: `${searchTerm}*`, // Wildcard search for terms starting with searchTerm - // boost: 2 // Boosting the relevance of author matches - // } - // } - // }, - // { - // // Match the search term in the subject field with a wildcard - // wildcard: { - // subject: { - // value: `${searchTerm}*`, // Wildcard search for terms starting with searchTerm - // boost: 1 // Boosting the relevance of subject matches - // } - // } - // } - // ], - // // Ensure that at least one of the `should` clauses must match - // minimum_should_match: 1 - // } - // }, - // // Limit the number of search results to 10 - // size: 10, - // // Start from the first result (pagination) - // from: 0, - // // Sort the results by the `server_date_published` field in descending order - // sort: [ - // { server_date_published: { order: "desc" } } - // ], - // // Aggregations to provide facets for the `language` and `subject` fields - // aggs: { - // language: { - // terms: { - // field: "language.keyword" // Aggregate by the exact values of the `language` field - // } - // }, - // subject: { - // terms: { - // field: "subjects.keyword", // Aggregate by the exact values of the `subjects` field - // size: 10 // Limit the number of aggregation buckets to 10 - // } - // } - // } - // }; - try { // Send the POST request to the OpenSearch endpoint const response = await fetch(url, { @@ -241,10 +147,41 @@ class DatasetService { 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"; + + public searchTerm(term: string): Observable { + const body = { + query: { + bool: { + should: [ + { match: { title: { query: term, fuzziness: "AUTO", boost: 3 } } }, + { match: { author: { query: term, fuzziness: "AUTO", boost: 2 } } }, + { match: { subject: { query: term, fuzziness: "AUTO", boost: 1 } } }, + { wildcard: { title: { value: `${term}*`, boost: 3 } } }, + { wildcard: { author: { value: `${term}*`, boost: 2 } } }, + { wildcard: { subject: { value: `${term}*`, boost: 1 } } } + ], + minimum_should_match: 1 + } + }, + size: 10, + from: 0, + sort: [{ server_date_published: { order: "desc" } }], + aggs: { + language: { terms: { field: "language.keyword" } }, + subject: { terms: { field: "subjects.keyword", size: 10 } } + } + }; + + return api.post(this.openSearchUrl, body).pipe( + map(response => response.hits.hits.map(hit => hit._source)) + ); + } + // For the autocomplete search. Method to perform a search based on a term - public searchTerm(term: string, solrCore: string, solrHost: string): Observable { + public searchTerm_SOLR(term: string, solrCore: string, solrHost: string): Observable { // Calling the test method for - this.fetchDataFromOpenSearch(term); + // this.fetchDataFromOpenSearch(term); // solr endpoint const host = "https://" + solrHost; const path = "/solr/" + solrCore + "/select?"; @@ -276,6 +213,11 @@ class DatasetService { }; // Make API call to Solr and return the result + /** + * When a GET request is made to the Solr server using the api.get 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(base, q_params).pipe(map((res: SolrResponse) => res.response.docs)); return stations; @@ -290,7 +232,9 @@ class DatasetService { &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 */ - // Method to perform a faceted search + /** + * 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( suggestion: Suggestion | string, activeFilterCategories: ActiveFilterCategories, @@ -316,13 +260,13 @@ class DatasetService { "doctype", ].toString(); - // Determine search term, query operator, and query fields based on the suggestion type + // 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") { + 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) { + } 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; diff --git a/src/views/search-view/search-view-component.ts b/src/views/search-view/search-view-component.ts index 676ef47..2bd2bb4 100644 --- a/src/views/search-view/search-view-component.ts +++ b/src/views/search-view/search-view-component.ts @@ -10,7 +10,7 @@ import { OpenSettings } from "@/models/solr"; import DatasetService from "../../services/dataset.service"; import { Suggestion, Dataset, SearchType } from "@/models/dataset"; // import { SolrResponse, FacetFields, FacetItem, FacetResults, FacetInstance } from "@/models/headers"; -import { OpenResponse, SolrResponse, FacetFields, FacetItem, FacetResults, FacetInstance } from "@/models/headers"; +import { SolrResponse, FacetFields, FacetItem, FacetResults, FacetInstance } from "@/models/headers"; import { ActiveFilterCategories } from "@/models/solr"; import { SOLR_HOST, SOLR_CORE } from "@/constants"; import { IPagination } from "@/models/pagination";