From da430d6142b77e242befc9254f7e20b96a5f56ef Mon Sep 17 00:00:00 2001 From: frankporras Date: Wed, 12 Jun 2024 16:51:26 +0200 Subject: [PATCH] Facetsearch progressed. Pending fixing onClearFacetCategoryOPEN behaviour. Facets menu doesn't work properly. --- src/components/vs-input/vs-input.ts | 4 +- src/models/dataset.ts | 6 +- src/models/headers.ts | 26 +-- src/services/dataset.service.ts | 175 +++++++++++++----- .../search-view/search-view-component.ts | 143 ++++++++++++-- .../search-view/search-view-component.vue | 2 +- 6 files changed, 264 insertions(+), 92 deletions(-) diff --git a/src/components/vs-input/vs-input.ts b/src/components/vs-input/vs-input.ts index 68e1cbb..c0f2706 100644 --- a/src/components/vs-input/vs-input.ts +++ b/src/components/vs-input/vs-input.ts @@ -88,7 +88,7 @@ export default class VsInput extends Vue { const suggestions = new Array(); - console.log("Suggestions > Display:", this.display); + console.log("getSuggestions > Display:", this.display); // console.log("results:", this.results ); // console.log("highlights:", this.highlights); @@ -98,7 +98,7 @@ export default class VsInput extends Vue { const highlight = this.highlights[index]; - console.log("get suggestions:id", dataset.id); + // 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); diff --git a/src/models/dataset.ts b/src/models/dataset.ts index 1f8252f..f4b672b 100644 --- a/src/models/dataset.ts +++ b/src/models/dataset.ts @@ -80,9 +80,9 @@ export class Suggestion { // } export enum SearchType { - Title = "Title", - Author = "Author", - Subject = "Subject" + Title = "title", + Author = "author", + Subject = "subject" } export class DbDataset { diff --git a/src/models/headers.ts b/src/models/headers.ts index bdcf21c..c96835e 100644 --- a/src/models/headers.ts +++ b/src/models/headers.ts @@ -18,19 +18,6 @@ export interface ResponseHeaderParams { rows?: number; start?: number; 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 { @@ -40,21 +27,16 @@ export interface ResponseContent { } export class FacetResults { - // language!: Array; - // subject!: Array; [key: string]: Array; } export class FacetFields { - // count: number; language!: FacetInstance; subject!: FacetInstance; - // [key: string]: FacetInstance; } export interface FacetInstance { [key: string]: Array; - // buckets: Array; } export class FacetItem { @@ -78,8 +60,8 @@ export interface OpenSearchResponse { took: number; timed_out: boolean; _shards: Shards; - hits: Hits; - aggregations?: Aggregations; + hits: Hits; // Equivalent SOLR: response > docs + aggregations?: Aggregations; // Equivalent SOLR: facets } export interface Shards { @@ -96,7 +78,7 @@ export interface Hits { } export interface Total { - value: number; + value: number; // Equivalent SOLR: response > numFound relation: string; } @@ -114,7 +96,7 @@ export interface HitHighlight { author?: Array; } -export interface Aggregations { +export interface Aggregations { // Equivalent SOLR: FacetFields subjects: Subjects; language: Language; } diff --git a/src/services/dataset.service.ts b/src/services/dataset.service.ts index 0b201e8..7fc03fb 100644 --- a/src/services/dataset.service.ts +++ b/src/services/dataset.service.ts @@ -76,7 +76,7 @@ class DatasetService { * 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(base, body).pipe( - tap(response => console.log("OpenSearchResponse:", response)), // Log the complete response + // 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 @@ -89,58 +89,140 @@ class DatasetService { ); } - // For the autocomplete search. Method to perform a search based on a term - public searchTerm_SOLR(term: string, solrCore: string, solrHost: string): Observable { - // SOLR endpoint - const host = "https://" + solrHost; - const path = "/solr/" + solrCore + "/select?"; - const base = host + path; + // // For the autocomplete search. Method to perform a search based on a term + // public searchTerm_SOLR(term: string, solrCore: string, solrHost: string): Observable { + // // 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 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 qfFields = "title^3 author^2 subject^1"; - const q_params = { - "0": "fl=" + fields, - q: term + "*", - defType: "edismax", - qf: qfFields, - indent: "on", - wt: "json", - }; + // 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 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)); + // // 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; + // } + + public facetedSearchOPEN( + suggestion: Suggestion | string, + activeFilterCategories: ActiveFilterCategories, + openCore: string, + openHost: string, + start?: string, // Starting page + ): Observable { + // OpenSearch endpoint + const host = "https://" + openHost; + const path = "/" + openCore + "/_search"; + const base = host + path; + + const lowercaseTerm = typeof suggestion === 'string' ? suggestion.toLowerCase() : suggestion.value.toLowerCase(); + + console.log("facetedsearchOPEN > suggestion entered:"); + console.log(suggestion); + + /** + * The query construction depends on whether the suggestion is a string or a Suggestion object. */ + // When suggestion is a string: + const query = 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.toLowerCase()]: { + query: suggestion.value, + operator: 'and' // all the terms in the query must be present in the field + } + } + }; + + // Constructing Filters Based on Active Filter Categories + const filters = Object.entries(activeFilterCategories).map(([category, values]) => ({ + terms: { [`${category}.keyword`]: values } + // terms: { [category]: values } + })); + + const body = { + query: { + bool: { + must: query, // Contains the main query constructed earlier. + filter: filters // Contains the filters constructed from activeFilterCategories. + } + }, + 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 } }, + language: { terms: { field: "language.keyword" } }, + author: { terms: { field: "author.keyword", size: 1000 } }, + year: { terms: { field: "year.keyword", size: 100 } } + }, + highlight: { + fields: { + title: {}, + author: {}, + subjects: {} + } + } + }; + + // return api.post(base, body).pipe( + // // map(response => ({ + // // datasets: response.hits.hits.map(hit => hit._source), + // // highlights: response.hits.hits.map(hit => hit.highlight), + // // // aggregations: response.aggregations + // // })) + // ); + const stations = api.post(base, body); + return stations; } - - - /* E.g. 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 */ - + /** * This method performs a faceted search on a Solr core. Faceted search allows the user to filter search results based on various categories (facets) */ @@ -157,6 +239,9 @@ class DatasetService { // 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?"; diff --git a/src/views/search-view/search-view-component.ts b/src/views/search-view/search-view-component.ts index d87fa9c..a8bdd8c 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 { SolrResponse, FacetFields, FacetItem, FacetResults, FacetInstance } from "@/models/headers"; +import { SolrResponse, FacetFields, FacetItem, FacetResults, FacetInstance, OpenSearchResponse, HitHighlight } from "@/models/headers"; import { ActiveFilterCategories } from "@/models/solr"; import { SOLR_HOST, SOLR_CORE } from "@/constants"; import { IPagination } from "@/models/pagination"; @@ -100,7 +100,7 @@ export default class SearchViewComponent extends Vue { // Lifecycle hook: executed before the component is mounted beforeMount(): void { - console.log("beforeMount!"); + // console.log("beforeMount!"); // this.rdrAPI = new DatasetService(); // Trigger search based on provided display and type props @@ -122,25 +122,66 @@ export default class SearchViewComponent extends Vue { // Method to trigger a search onSearch(suggestion: Suggestion | string): void { - console.log("ONSEARCH"); + // console.log("ONSEARCH"); // Reset active filter categories and facet results this.activeFilterCategories = new ActiveFilterCategories(); this.facets = new FacetResults(); - this.searchTerm = suggestion; - console.log("This.searchterm: ", this.searchTerm); + console.log("ONSEARCH > suggestion: ", suggestion); - /* 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), + // /* 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), }); + + } + + // Handle the search results + private dataHandlerOPEN(res: OpenSearchResponse, filterItem?: FacetItem): void { + // console.log("dataHandlerOPEN (datasets, highlights):"); + // console.log(datasets); + // console.log(highlights); + + this.results = res.hits.hits.map(hit => hit._source); + this.numFound = res.hits.total.value; + + this.pagination.total = res.hits.total.value; + this.pagination.perPage = 10; + this.pagination.data = this.results; + this.pagination.lastPage = Math.ceil(this.pagination.total / this.pagination.perPage); + + if (res.aggregations) { + const facet_fields = res.aggregations; + + let prop: keyof typeof 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)); + + this.facets[prop] = facetItems.filter(el => el.count > 0); + } + } + } + } // 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; @@ -204,11 +245,16 @@ export default class SearchViewComponent extends Vue { this.pagination.currentPage = page; const start = page * this.pagination.perPage - this.pagination.perPage; - // Trigger new search with updated pagination parameters - DatasetService.facetedSearch(this.searchTerm, this.activeFilterCategories, this.solr.core, this.solr.host, start.toString()).subscribe( - (res: SolrResponse) => this.dataHandler(res), - (error: string) => this.errorHandler(error), - ); + // // Trigger new search with updated pagination parameters + // DatasetService.facetedSearch(this.searchTerm, this.activeFilterCategories, this.solr.core, this.solr.host, start.toString()).subscribe( + // (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 @@ -229,14 +275,18 @@ export default class SearchViewComponent extends Vue { // Add filter item to active filter categories 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( - (res: SolrResponse) => this.dataHandler(res, facetItem), - (error: string) => this.errorHandler(error), - ); + // DatasetService.facetedSearch(this.searchTerm, this.activeFilterCategories, this.solr.core, this.solr.host, undefined).subscribe( + // (res: SolrResponse) => this.dataHandler(res, facetItem), + // (error: string) => this.errorHandler(error), + // ); + DatasetService.facetedSearchOPEN(this.searchTerm, this.activeFilterCategories, this.open.core, this.open.host, undefined).subscribe({ + next: (res: OpenSearchResponse) => this.dataHandlerOPEN(res, facetItem), + error: (error: string) => this.errorHandler(error), + }); } } - // Method to clear facet category filter + // // Method to clear facet category filter onClearFacetCategory(categoryName: string): void { delete this.activeFilterCategories[categoryName]; @@ -288,4 +338,59 @@ export default class SearchViewComponent extends Vue { }); } + // Method to clear facet category filter + onClearFacetCategoryOPEN(categoryName: string): void { + delete this.activeFilterCategories[categoryName]; + + // Trigger new search with updated filter + DatasetService.facetedSearchOPEN(this.searchTerm, this.activeFilterCategories, this.open.core, this.open.host, undefined).subscribe({ + next: (res: OpenSearchResponse) => { + this.results = res.hits.hits.map(hit => hit._source); + this.numFound = res.hits.total.value; + + // Update pagination + this.pagination.total = res.hits.total.value; + this.pagination.perPage = 10; + this.pagination.currentPage = 1; + this.pagination.data = this.results; + this.pagination.lastPage = Math.ceil(this.pagination.total / this.pagination.perPage); + + if (res.aggregations) { + const facet_fields = res.aggregations; + + let prop: keyof typeof 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)); + + const facetValues = facetItems.map((facetItem) => { + let rObj: FacetItem; + if (this.facets[prop]?.some((e) => e.val === facetItem.val)) { + // Update existing facet item with new count + const indexOfFacetValue = this.facets[prop].findIndex((i) => i.val === facetItem.val); + rObj = this.facets[prop][indexOfFacetValue]; + 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); + } + return rObj; + }).filter(el => el.count > 0); // Filter out items with count <= 0 + + this.facets[prop] = facetValues; + } + } + } + }, + error: (error: string) => this.errorHandler(error), + complete: () => console.log("clear facet category completed"), + }); + } + } diff --git a/src/views/search-view/search-view-component.vue b/src/views/search-view/search-view-component.vue index bced80b..f42f226 100644 --- a/src/views/search-view/search-view-component.vue +++ b/src/views/search-view/search-view-component.vue @@ -34,7 +34,7 @@