Facetsearch progressed. Pending fixing onClearFacetCategoryOPEN behaviour. Facets menu doesn't work properly.

This commit is contained in:
Porras-Bernardez 2024-06-12 16:51:26 +02:00
parent a70e454cbc
commit da430d6142
6 changed files with 264 additions and 92 deletions

View File

@ -88,7 +88,7 @@ export default class VsInput extends Vue {
const suggestions = new Array<Suggestion>();
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);

View File

@ -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 {

View File

@ -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<FacetItem>;
// subject!: Array<FacetItem>;
[key: string]: Array<FacetItem>;
}
export class FacetFields {
// count: number;
language!: FacetInstance;
subject!: FacetInstance;
// [key: string]: FacetInstance;
}
export interface FacetInstance {
[key: string]: Array<FacetItem>;
// buckets: Array<FacetItem>;
}
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<string>;
}
export interface Aggregations {
export interface Aggregations { // Equivalent SOLR: FacetFields
subjects: Subjects;
language: Language;
}

View File

@ -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<OpenSearchResponse>(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<Dataset[]> {
// 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<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 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<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));
// // 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 = "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<OpenSearchResponse>(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<OpenSearchResponse>(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?";

View File

@ -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"),
});
}
}

View File

@ -34,7 +34,7 @@
<active-facet-category
v-bind:filterItems="values"
v-bind:categoryName="key"
@clear-facet-category="onClearFacetCategory"
@clear-facet-category="onClearFacetCategoryOPEN"
></active-facet-category>
</span>
</div>