Skip to content

Commit

Permalink
Amélioration de la recherche des métadonnées + ajouts (#3296)
Browse files Browse the repository at this point in the history
* reprise de la recherche en decoupant le search sur les espaces
* reprise du fonctionnement du rapidsearch
* feat(metadata): refactor metadata search -> order by number of match found in the af name (resp.ds)
* feat(changelog): update changelog

---------

Co-authored-by: jbrieuclp <[email protected]>
  • Loading branch information
jacquesfize and jbrieuclp authored Dec 31, 2024
1 parent aff66d2 commit 87a6540
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 54 deletions.
62 changes: 47 additions & 15 deletions backend/geonature/core/gn_meta/models/aframework.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import datetime

import sqlalchemy as sa
import re
from flask import g
from geonature.core.gn_permissions.tools import get_scopes_by_action
from geonature.utils.env import DB, db
from pypnnomenclature.models import TNomenclatures
from pypnusershub.db.models import User
from sqlalchemy import ForeignKey, or_
from sqlalchemy import ForeignKey, or_, func
from sqlalchemy.dialects.postgresql import UUID as UUIDType
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship
Expand Down Expand Up @@ -311,28 +312,59 @@ def filter_by_params(cls, params={}, *, _ds_search=True, query=None):

search = params.get("search")
if search:
ors = [
TAcquisitionFramework.acquisition_framework_name.ilike(f"%{search}%"),
sa.cast(TAcquisitionFramework.id_acquisition_framework, sa.String) == search,
]
# enable uuid search only with at least 5 characters
if len(search) >= 5:
ors.append(
search = search.strip()
# Where clauses to include other matching possibilities (id, uuid, date)
where_clauses = []
if search.isdigit(): # ID AF match
where_clauses.append(TAcquisitionFramework.id_acquisition_framework == int(search))

if len(search) >= MIN_LENGTH_UUID_OR_DATE_SEARCH_STRING: # UUID and date match
where_clauses.append(
sa.cast(TAcquisitionFramework.unique_acquisition_framework_id, sa.String).like(
f"{search}%"
)
)
try:
date = datetime.datetime.strptime(search, "%d/%m/%Y").date()
ors.append(TAcquisitionFramework.acquisition_framework_start_date == date)
except ValueError:
pass
try:
date = datetime.datetime.strptime(search, "%d/%m/%Y").date()
where_clauses.append(
TAcquisitionFramework.acquisition_framework_start_date == date
)
except ValueError:
pass

# If name search includes dataset
if _ds_search:
ors.append(
where_clauses.append(
TAcquisitionFramework.datasets.any(
TDatasets.filter_by_params({"search": search}, _af_search=False).whereclause
),
)
query = query.where(sa.or_(*ors))

# Acquisition Framework name matching
search_words_af_cte = select(
func.unnest(func.string_to_array(search, " ")).label("word")
).cte("search_words_cte")
matched_words_af_cte = (
select(
TAcquisitionFramework.id_acquisition_framework,
func.count().label("match_count"),
)
.join(
search_words_af_cte,
sa.or_(
TAcquisitionFramework.acquisition_framework_name.ilike(
func.concat("%", search_words_af_cte.c.word, "%")
),
*where_clauses,
),
)
.group_by(
TAcquisitionFramework.id_acquisition_framework,
)
).cte("matched_words_af_cte")
query = query.where(
matched_words_af_cte.c.id_acquisition_framework
== TAcquisitionFramework.id_acquisition_framework
).order_by(matched_words_af_cte.c.match_count.desc())

return query
2 changes: 2 additions & 0 deletions backend/geonature/core/gn_meta/models/commons.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

from geonature.utils.env import DB, db

MIN_LENGTH_UUID_OR_DATE_SEARCH_STRING = 5


class DateFilterSchema(ma.Schema):
year = ma.fields.Integer()
Expand Down
58 changes: 41 additions & 17 deletions backend/geonature/core/gn_meta/models/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from flask import g
import sqlalchemy as sa
from sqlalchemy import ForeignKey, or_
from sqlalchemy import ForeignKey, or_, func
from sqlalchemy.sql import select, func
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import UUID as UUIDType
Expand Down Expand Up @@ -274,29 +274,53 @@ class DatasetFilterSchema(MetadataFilterSchema):

search = params.get("search")
if search:
ors = [
cls.dataset_name.ilike(f"%{search}%"),
sa.cast(cls.id_dataset, sa.String) == search,
]
# enable uuid search only with at least 5 characters
if len(search) >= 5:
ors.append(sa.cast(cls.unique_dataset_id, sa.String).like(f"{search}%"))
try:
date = datetime.datetime.strptime(search, "%d/%m/%Y").date()
except ValueError:
pass
else:
ors.append(sa.cast(cls.meta_create_date, sa.DATE) == date)
search = search.strip()
# Where clauses to include other matching possibilities (id, uuid)
where_clauses = []
if search.isdigit(): # ID AF match
where_clauses.append(cls.id_dataset == int(search))

if len(search) >= MIN_LENGTH_UUID_OR_DATE_SEARCH_STRING: # UUID match
where_clauses.append(sa.cast(cls.unique_dataset_id, sa.String).ilike(f"{search}%"))

# if name search include acquisition framework
if _af_search:
ors.append(
where_clauses.append(
cls.acquisition_framework.has(
TAcquisitionFramework.filter_by_params(
{"search": search},
_ds_search=False,
).whereclause
)
),
)

# Dataset name matching
search_words_dataset_cte = select(
func.unnest(func.string_to_array(search, " ")).label("word")
).cte("search_words_dataset_cte")
matched_words_dataset_cte = (
select(
cls.id_dataset,
func.count().label("match_count"),
)
query = query.where(or_(*ors))
.join(
search_words_dataset_cte,
sa.or_(
cls.dataset_name.ilike(
func.concat("%", search_words_dataset_cte.c.word, "%")
),
*where_clauses,
),
)
.group_by(
cls.id_dataset,
)
).cte("matched_words_dataset_cte")

query = query.where(cls.id_dataset == matched_words_dataset_cte.c.id_dataset).order_by(
matched_words_dataset_cte.c.match_count.desc()
)

return query

@qfilter(query=True)
Expand Down
2 changes: 1 addition & 1 deletion backend/geonature/tests/test_gn_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from io import StringIO
from unittest.mock import patch


import pytest
from flask import url_for
from geoalchemy2.shape import to_shape
Expand Down Expand Up @@ -824,7 +825,6 @@ def test_get_dataset_search(self, users, datasets, module):
def test_get_dataset_search_uuid(self, users, datasets):
ds = datasets["own_dataset"]
set_logged_user(self.client, users["admin_user"])

response = self.client.get(
url_for("gn_meta.get_datasets"),
json={"search": str(ds.unique_dataset_id)[:5]},
Expand Down
8 changes: 8 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
CHANGELOG
=========

2.15.1 (unreleased)
-------------------

**🐛 Corrections**

- Amélioration de la recherche des métadonnées: ajout d'une recherche par mot-clés. (#3295 par @jbrieuclp)


2.15.0 - Pavo cristatus 🦚 (2025-12-11)
---------------------------------------

Expand Down
28 changes: 16 additions & 12 deletions frontend/src/app/metadataModule/metadata.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { PageEvent, MatPaginator } from '@angular/material/paginator';
import { CruvedStoreService } from '../GN2CommonModule/service/cruved-store.service';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { Observable, combineLatest } from 'rxjs';
import { map, distinctUntilChanged, debounceTime } from 'rxjs/operators';
import { map, distinctUntilChanged, debounceTime, tap, switchMap, startWith } from 'rxjs/operators';
import { omitBy } from 'lodash';

import { DataFormService, ParamsDict } from '@geonature_common/form/data-form.service';
Expand Down Expand Up @@ -82,18 +82,22 @@ export class MetadataComponent implements OnInit {
// rapid search event
//combinaison de la zone de recherche et du chargement des données
this.rapidSearchControl.valueChanges
.pipe(debounceTime(1000), distinctUntilChanged())
.subscribe((term) => {
if (term !== null) {
if (term === '') {
delete this.searchTerms.search;
} else {
this.searchTerms = { ...this.searchTerms, search: this.rapidSearchControl.value };
.pipe(
startWith(''),
debounceTime(500),
distinctUntilChanged(),
tap((term) => {
if (term !== null) {
if (term === '') {
delete this.searchTerms.search;
} else {
this.searchTerms = { ...this.searchTerms, search: this.rapidSearchControl.value };
}
}
this.metadataService.search(this.searchTerms);
this.metadataService.pageIndex.next(0);
}
});
}),
switchMap(() => this.metadataService.search(this.searchTerms))
)
.subscribe(() => {return;});

// format areas filter
this.areaFilters = this.config.METADATA.METADATA_AREA_FILTERS.map((area) => {
Expand Down
19 changes: 10 additions & 9 deletions frontend/src/app/metadataModule/services/metadata.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Injectable } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, UntypedFormControl } from '@angular/forms';
import { NgbDateParserFormatter } from '@ng-bootstrap/ng-bootstrap';
import { BehaviorSubject } from 'rxjs';
import { tap } from 'rxjs/operators';
import { BehaviorSubject, of } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';

import { SyntheseDataService } from '@geonature_common/form/synthese-form/synthese-data.service';
import { DataFormService, ParamsDict } from '@geonature_common/form/data-form.service';
Expand Down Expand Up @@ -46,8 +46,6 @@ export class MetadataService {
person: null,
});

this.getMetadata();

this.config.METADATA.METADATA_AREA_FILTERS.forEach((area) => {
const control_name = 'area_' + area['type_code'].toLowerCase();
this.form.addControl(control_name, new UntypedFormControl(new Array()));
Expand All @@ -59,11 +57,11 @@ export class MetadataService {

// FIXME: remove any!!!
search(formValue: any) {
return this.getMetadataObservable(formValue).subscribe(
(afs) => {
return this.getMetadataObservable(formValue).pipe(
tap((afs) => {
this.acquisitionFrameworks.next(afs);
},
(err) => (this.isLoading = false)
this.pageIndex.next(0);
})
);
}
//recuperation cadres d'acquisition
Expand All @@ -74,7 +72,10 @@ export class MetadataService {
//forkJoin pour lancer les 2 requetes simultanément
return this.dataFormService
.getAcquisitionFrameworksList(selectors, params)
.pipe(tap(() => (this.isLoading = false)));
.pipe(
catchError(() => of([])),
tap(() => (this.isLoading = false)),
);
}

getMetadata(params = {}, selectors = SELECTORS) {
Expand Down

0 comments on commit 87a6540

Please sign in to comment.