From 644c6faf5780ef855c0695c4dc60e64c29cd88d3 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Tue, 20 Aug 2024 17:03:25 -0400 Subject: [PATCH 01/94] Make views a package --- arches_lingo/views/__init__.py | 2 ++ arches_lingo/views/root.py | 14 ++++++++++++++ arches_lingo/{views.py => views/trees.py} | 11 ----------- 3 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 arches_lingo/views/__init__.py create mode 100644 arches_lingo/views/root.py rename arches_lingo/{views.py => views/trees.py} (94%) diff --git a/arches_lingo/views/__init__.py b/arches_lingo/views/__init__.py new file mode 100644 index 00000000..ea7dc745 --- /dev/null +++ b/arches_lingo/views/__init__.py @@ -0,0 +1,2 @@ +from .root import * +from .trees import * diff --git a/arches_lingo/views/root.py b/arches_lingo/views/root.py new file mode 100644 index 00000000..a85b60f2 --- /dev/null +++ b/arches_lingo/views/root.py @@ -0,0 +1,14 @@ +from django.shortcuts import render +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from django.views.decorators.csrf import ensure_csrf_cookie + +from arches.app.views.base import BaseManagerView + + +class LingoRootView(BaseManagerView): + @method_decorator(ensure_csrf_cookie) + def get(self, request, graphid=None, resourceid=None): + context = self.get_context_data(main_script="views/root") + context["page_title"] = _("Lingo") + return render(request, "arches_lingo/root.htm", context) diff --git a/arches_lingo/views.py b/arches_lingo/views/trees.py similarity index 94% rename from arches_lingo/views.py rename to arches_lingo/views/trees.py index a5d5f534..332a9d9d 100644 --- a/arches_lingo/views.py +++ b/arches_lingo/views/trees.py @@ -3,10 +3,8 @@ from django.contrib.postgres.expressions import ArraySubquery from django.db.models import CharField, F, OuterRef, Subquery, Value from django.db.models.expressions import CombinedExpression, Func -from django.shortcuts import render from django.utils.translation import gettext_lazy as _ from django.utils.decorators import method_decorator -from django.views.decorators.csrf import ensure_csrf_cookie from django.views.generic import View from arches.app.models.models import ( @@ -17,7 +15,6 @@ ) from arches.app.utils.decorators import group_required from arches.app.utils.response import JSONResponse -from arches.app.views.base import BaseManagerView from arches_lingo.const import ( SCHEMES_GRAPH_ID, @@ -204,11 +201,3 @@ def get(self, request): } # Todo: filter by nodegroup permissions return JSONResponse(data) - - -class LingoRootView(BaseManagerView): - @method_decorator(ensure_csrf_cookie) - def get(self, request, graphid=None, resourceid=None): - context = self.get_context_data(main_script="views/root") - context["page_title"] = _("Lingo") - return render(request, "arches_lingo/root.htm", context) From 0278b768eeaead1ed95bd726881e12c8c51059e2 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Tue, 20 Aug 2024 17:05:25 -0400 Subject: [PATCH 02/94] Small view cleanups --- arches_lingo/views/root.py | 2 +- arches_lingo/views/trees.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/arches_lingo/views/root.py b/arches_lingo/views/root.py index a85b60f2..4ec16321 100644 --- a/arches_lingo/views/root.py +++ b/arches_lingo/views/root.py @@ -8,7 +8,7 @@ class LingoRootView(BaseManagerView): @method_decorator(ensure_csrf_cookie) - def get(self, request, graphid=None, resourceid=None): + def get(self, request, *args, **kwargs): context = self.get_context_data(main_script="views/root") context["page_title"] = _("Lingo") return render(request, "arches_lingo/root.htm", context) diff --git a/arches_lingo/views/trees.py b/arches_lingo/views/trees.py index 332a9d9d..68be76bc 100644 --- a/arches_lingo/views/trees.py +++ b/arches_lingo/views/trees.py @@ -50,6 +50,7 @@ class JsonbArrayElements(Func): ) class ConceptTreeView(View): def __init__(self): + super().__init__() self.schemes = ResourceInstance.objects.none() # Maps built during a GET call From b8809b6bfc5844fde6ff2c880d3e13086af1cb34 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 21 Aug 2024 10:49:34 -0400 Subject: [PATCH 03/94] Remove .all() cruft --- arches_lingo/views/trees.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/arches_lingo/views/trees.py b/arches_lingo/views/trees.py index 68be76bc..959f4719 100644 --- a/arches_lingo/views/trees.py +++ b/arches_lingo/views/trees.py @@ -101,8 +101,7 @@ def labels_subquery(label_nodegroup): def language_concepts_map(self): languages = ( - Language.objects.all() - .annotate( + Language.objects.annotate( concept_value=Subquery( ConceptValue.objects.filter( valuetype="prefLabel", value=OuterRef("code") From 826b3e68adbc2da12a6a5bd92d5e31757e7f0c90 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 21 Aug 2024 11:48:59 -0400 Subject: [PATCH 04/94] Add cursor_tuple_fraction optimization for queryset iterators --- arches_lingo/settings.py | 4 +++- tests/test_settings.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/arches_lingo/settings.py b/arches_lingo/settings.py index d21b5a2d..f06645eb 100644 --- a/arches_lingo/settings.py +++ b/arches_lingo/settings.py @@ -110,7 +110,9 @@ "ENGINE": "django.contrib.gis.db.backends.postgis", "HOST": "localhost", "NAME": "arches_lingo", - "OPTIONS": {}, + "OPTIONS": { + "options": "-c cursor_tuple_fraction=1", + }, "PASSWORD": "postgis", "PORT": "5432", "POSTGIS_TEMPLATE": "template_postgis", diff --git a/tests/test_settings.py b/tests/test_settings.py index d901af69..add6a4bd 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -21,7 +21,9 @@ "ENGINE": "django.contrib.gis.db.backends.postgis", "HOST": "localhost", "NAME": "arches_lingo", - "OPTIONS": {}, + "OPTIONS": { + "options": "-c cursor_tuple_fraction=1", + }, "PASSWORD": "postgis", "PORT": "5432", "POSTGIS_TEMPLATE": "template_postgis", From 23319f854e3faa6d550d08c0fe7265e9c9d6fcee Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 21 Aug 2024 18:57:13 -0400 Subject: [PATCH 05/94] Initial commit of search backend #67 --- CHANGELOG.md | 1 + arches_lingo/urls.py | 3 +- arches_lingo/views/trees.py | 101 +++++++++++++++++++++++++++++++----- 3 files changed, 92 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index badbed91..a8306ef0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add login interface [#13](https://github.com/archesproject/arches-lingo/issues/13) - Add front-end router [#11](https://github.com/archesproject/arches-lingo/issues/11) +- Add backend for search [#67](https://github.com/archesproject/arches-lingo/issues/67) ### Fixed diff --git a/arches_lingo/urls.py b/arches_lingo/urls.py index 7a211211..1e5301af 100644 --- a/arches_lingo/urls.py +++ b/arches_lingo/urls.py @@ -3,7 +3,7 @@ from django.conf.urls.i18n import i18n_patterns from django.urls import include, path -from arches_lingo.views import LingoRootView, ConceptTreeView +from arches_lingo.views import LingoRootView, ConceptTreeView, ValueSearchView urlpatterns = [ path("", LingoRootView.as_view(), name="root"), @@ -12,6 +12,7 @@ path("advanced-search", LingoRootView.as_view(), name="advanced-search"), path("schemes", LingoRootView.as_view(), name="schemes"), path("api/concept_trees", ConceptTreeView.as_view(), name="concept_trees"), + path("api/search", ValueSearchView.as_view(), name="api_search"), path("", include("arches_references.urls")), ] diff --git a/arches_lingo/views/trees.py b/arches_lingo/views/trees.py index 959f4719..f7bb1dd0 100644 --- a/arches_lingo/views/trees.py +++ b/arches_lingo/views/trees.py @@ -1,4 +1,5 @@ from collections import defaultdict +from http import HTTPStatus from django.contrib.postgres.expressions import ArraySubquery from django.db.models import CharField, F, OuterRef, Subquery, Value @@ -63,6 +64,12 @@ def __init__(self): # key=resourceid (str) val=list of label dicts self.labels: dict[str : list[dict]] = defaultdict(set) + # Maps representing a reverse (leaf-first) tree + # key=resourceid (str) val=set of concept resourceids (str) + self.broader_concepts: dict[str : set[str]] = defaultdict(set) + # key=resourceid (str) val=set of scheme resourceids (str) + self.schemes_by_top_concept: dict[str : set[str]] = defaultdict(set) + @staticmethod def human_label_type(value_id): if value_id == PREF_LABEL_VALUE_ID: @@ -122,9 +129,11 @@ def top_concepts_map(self): .values("resourceinstance_id", "top_concept_of", "labels") ) for tile in top_concept_of_tiles: - resource_id: str = str(tile["resourceinstance_id"]) - self.top_concepts[tile["top_concept_of"]].add(resource_id) - self.labels[resource_id] = tile["labels"] + scheme_id = tile["top_concept_of"] + top_concept_id = str(tile["resourceinstance_id"]) + self.top_concepts[scheme_id].add(top_concept_id) + self.schemes_by_top_concept[top_concept_id].add(scheme_id) + self.labels[top_concept_id] = tile["labels"] def narrower_concepts_map(self): broader_concept_tiles = ( @@ -134,9 +143,16 @@ def narrower_concepts_map(self): .values("resourceinstance_id", "broader_concept", "labels") ) for tile in broader_concept_tiles.iterator(): - resource_id: str = str(tile["resourceinstance_id"]) - self.narrower_concepts[tile["broader_concept"]].add(resource_id) - self.labels[resource_id] = tile["labels"] + broader_concept_id = tile["broader_concept"] + narrower_concept_id: str = str(tile["resourceinstance_id"]) + self.narrower_concepts[broader_concept_id].add(narrower_concept_id) + self.broader_concepts[narrower_concept_id].add(broader_concept_id) + self.labels[narrower_concept_id] = tile["labels"] + + def populate_schemes(self): + self.schemes = ResourceInstance.objects.filter( + graph_id=SCHEMES_GRAPH_ID + ).annotate(labels=self.labels_subquery(SCHEME_NAME_NODEGROUP)) def serialize_scheme(self, scheme: ResourceInstance): scheme_id: str = str(scheme.pk) @@ -162,8 +178,8 @@ def serialize_scheme_label(self, label_tile: dict): "value": value, } - def serialize_concept(self, conceptid: str): - return { + def serialize_concept(self, conceptid: str, *, parentage=False): + data = { "id": conceptid, "labels": [ self.serialize_concept_label(label) for label in self.labels[conceptid] @@ -173,6 +189,31 @@ def serialize_concept(self, conceptid: str): for conceptid in self.narrower_concepts[conceptid] ], } + if parentage: + # Choose any reverse path back to the scheme (currently indeterminate). + path = self.add_broader_concept_recursive([], conceptid) + scheme_id, concept_ids = path[0], path[1:] + schemes = [scheme for scheme in self.schemes if str(scheme.pk) == scheme_id] + data["parentage"] = [self.serialize_scheme(schemes[0])] + [ + self.serialize_concept(concept_id) for concept_id in concept_ids + ] + + return data + + def add_broader_concept_recursive(self, working_parent_list, conceptid): + broader_concepts = self.broader_concepts[conceptid] + try: + arbitrary_broader_conceptid = next(iter(broader_concepts)) + except StopIteration: + schemes = self.schemes_by_top_concept[conceptid] + arbitrary_scheme = next(iter(schemes)) + working_parent_list.insert(0, arbitrary_scheme) + return working_parent_list + else: + working_parent_list.insert(0, arbitrary_broader_conceptid) + return self.add_broader_concept_recursive( + working_parent_list, arbitrary_broader_conceptid + ) def serialize_concept_label(self, label_tile: dict): lang_code = self.language_concepts[label_tile[CONCEPT_NAME_LANGUAGE_NODE][0]] @@ -191,13 +232,49 @@ def get(self, request): self.language_concepts_map() self.top_concepts_map() self.narrower_concepts_map() - - self.schemes = ResourceInstance.objects.filter( - graph_id=SCHEMES_GRAPH_ID - ).annotate(labels=self.labels_subquery(SCHEME_NAME_NODEGROUP)) + self.populate_schemes() data = { "schemes": [self.serialize_scheme(scheme) for scheme in self.schemes], } # Todo: filter by nodegroup permissions return JSONResponse(data) + + +@method_decorator( + group_required("RDM Administrator", raise_exception=True), name="dispatch" +) +class ValueSearchView(ConceptTreeView): + def get(self, request): + search_term = request.GET.get("search") + if not search_term: + # Treat this as a request to clear & warm the cache. + return JSONResponse(status=HTTPStatus.IM_A_TEAPOT) + + # TODO: cache this + self.language_concepts_map() + self.top_concepts_map() + self.narrower_concepts_map() + self.populate_schemes() + + # TODO: fuzzy match, SEARCH_TERM_SENSITIVITY + concept_ids = ( + TileModel.objects.filter(nodegroup_id=CONCEPT_NAME_NODEGROUP) + .annotate(labels=self.labels_subquery(CONCEPT_NAME_NODEGROUP)) + # TODO: all languages + .filter( + **{ + f"data__{CONCEPT_NAME_CONTENT_NODE}__en__value__icontains": search_term + } + ) + .values_list("resourceinstance_id", flat=True) + ) + deduped = set(concept_ids) + + data = [ + self.serialize_concept(str(concept_uuid), parentage=True) + for concept_uuid in deduped + ] + + # Todo: filter by nodegroup permissions + return JSONResponse(data) From c1b211ada279f4b88766d09cdd817ca2dc0d9756 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 21 Aug 2024 19:17:30 -0400 Subject: [PATCH 06/94] Add caching --- arches_lingo/settings.py | 3 ++ arches_lingo/views/trees.py | 58 ++++++++++++++++++++++++++++--------- tests/test_settings.py | 3 ++ 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/arches_lingo/settings.py b/arches_lingo/settings.py index f06645eb..f9e7711e 100644 --- a/arches_lingo/settings.py +++ b/arches_lingo/settings.py @@ -261,6 +261,9 @@ "default": { "BACKEND": "django.core.cache.backends.dummy.DummyCache", }, + "lingo": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + }, "user_permission": { "BACKEND": "django.core.cache.backends.db.DatabaseCache", "LOCATION": "user_permission_cache", diff --git a/arches_lingo/views/trees.py b/arches_lingo/views/trees.py index f7bb1dd0..1c523f3f 100644 --- a/arches_lingo/views/trees.py +++ b/arches_lingo/views/trees.py @@ -1,6 +1,7 @@ from collections import defaultdict from http import HTTPStatus +from django.core.cache import caches from django.contrib.postgres.expressions import ArraySubquery from django.db.models import CharField, F, OuterRef, Subquery, Value from django.db.models.expressions import CombinedExpression, Func @@ -38,6 +39,8 @@ TOP_CONCEPT_OF_LOOKUP = f"data__{TOP_CONCEPT_OF_NODE_AND_NODEGROUP}" BROADER_LOOKUP = f"data__{CLASSIFICATION_STATUS_ASCRIBED_CLASSIFICATION_NODEID}" +cache = caches["lingo"] + class JsonbArrayElements(Func): """https://forum.djangoproject.com/t/django-4-2-behavior-change-when-using-arrayagg-on-unnested-arrayfield-postgresql-specific/21547/5""" @@ -54,7 +57,6 @@ def __init__(self): super().__init__() self.schemes = ResourceInstance.objects.none() - # Maps built during a GET call # key=concept valueid (str) val=language code self.language_concepts: dict[str:str] = {} # key=scheme resourceid (str) val=set of concept resourceids (str) @@ -70,6 +72,46 @@ def __init__(self): # key=resourceid (str) val=set of scheme resourceids (str) self.schemes_by_top_concept: dict[str : set[str]] = defaultdict(set) + self.read_from_cache() + + def read_from_cache(self): + from_cache = cache.get_many( + [ + "language_concepts", + "top_concepts", + "narrower_concepts", + "schemes", + "labels", + "broader_concepts", + "schemes_by_top_concept", + ] + ) + try: + self.language_concepts = from_cache["language_concepts"] + self.top_concepts = from_cache["top_concepts"] + self.narrower_concepts = from_cache["narrower_concepts"] + self.schemes = from_cache["schemes"] + self.labels = from_cache["labels"] + self.broader_concepts = from_cache["broader_concepts"] + self.schemes_by_top_concepts = from_cache["schemes_by_top_concepts"] + except KeyError: + self.rebuild_cache() + + def rebuild_cache(self): + self.language_concepts_map() + self.top_concepts_map() + self.narrower_concepts_map() + self.populate_schemes() + + cache.set("language_concepts", self.language_concepts) + cache.set("top_concepts", self.top_concepts) + cache.set("narrower_concepts", self.narrower_concepts) + cache.set("schemes", self.schemes) + cache.set("labels", self.labels) + # Reverse trees. + cache.set("broader_concepts", self.broader_concepts) + cache.set("schemes_by_top_concept", self.schemes_by_top_concept) + @staticmethod def human_label_type(value_id): if value_id == PREF_LABEL_VALUE_ID: @@ -229,11 +271,6 @@ def serialize_concept_label(self, label_tile: dict): } def get(self, request): - self.language_concepts_map() - self.top_concepts_map() - self.narrower_concepts_map() - self.populate_schemes() - data = { "schemes": [self.serialize_scheme(scheme) for scheme in self.schemes], } @@ -248,15 +285,10 @@ class ValueSearchView(ConceptTreeView): def get(self, request): search_term = request.GET.get("search") if not search_term: - # Treat this as a request to clear & warm the cache. + # Useful for warming the cache before a search. + self.rebuild_cache() return JSONResponse(status=HTTPStatus.IM_A_TEAPOT) - # TODO: cache this - self.language_concepts_map() - self.top_concepts_map() - self.narrower_concepts_map() - self.populate_schemes() - # TODO: fuzzy match, SEARCH_TERM_SENSITIVITY concept_ids = ( TileModel.objects.filter(nodegroup_id=CONCEPT_NAME_NODEGROUP) diff --git a/tests/test_settings.py b/tests/test_settings.py index add6a4bd..f0e75f32 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -37,6 +37,9 @@ "default": { "BACKEND": "django.core.cache.backends.dummy.DummyCache", }, + "lingo": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + }, "user_permission": { "BACKEND": "django.core.cache.backends.dummy.DummyCache", "LOCATION": "user_permission_cache", From ccedae94d5c4dd59b2fb0b61a3e2c6235b1eab69 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Thu, 22 Aug 2024 10:45:30 -0400 Subject: [PATCH 07/94] Handle all language values --- arches_lingo/migrations/0001_initial.py | 55 +++++++++++++++++++++++++ arches_lingo/models.py | 18 ++++++++ arches_lingo/views/trees.py | 17 +++----- 3 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 arches_lingo/migrations/0001_initial.py create mode 100644 arches_lingo/models.py diff --git a/arches_lingo/migrations/0001_initial.py b/arches_lingo/migrations/0001_initial.py new file mode 100644 index 00000000..ebf5f423 --- /dev/null +++ b/arches_lingo/migrations/0001_initial.py @@ -0,0 +1,55 @@ +# Generated by Django 4.2.15 on 2024-08-22 09:01 +import textwrap + +from django.conf import settings +from django.db import migrations, models + +from arches_lingo.const import CONCEPT_NAME_CONTENT_NODE + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [("models", "11042_update__arches_staging_to_tile")] + + view_name = f"{settings.APP_NAME}__vw_label_values" + + forward = textwrap.dedent( + f""" + CREATE MATERIALIZED VIEW {view_name} AS ( + SELECT + t.resourceinstanceid AS conceptid, + ROW_TO_JSON(JSONB_EACH(t.tiledata -> '{CONCEPT_NAME_CONTENT_NODE}')) + -> 'value' ->> 'value' AS value + FROM + tiles t + ORDER BY + conceptid + );""" + ) + + reverse = f"DROP MATERIALIZED VIEW {view_name};" + + operations = [ + migrations.RunSQL(forward, reverse), + migrations.CreateModel( + name="VwLabelValue", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("value", models.CharField(db_column="value")), + ], + options={ + "db_table": "arches_lingo__vw_label_values", + "managed": False, + }, + ), + ] diff --git a/arches_lingo/models.py b/arches_lingo/models.py new file mode 100644 index 00000000..e239c1cd --- /dev/null +++ b/arches_lingo/models.py @@ -0,0 +1,18 @@ +from django.conf import settings +from django.db import models + +from arches.app.models.models import ResourceInstance + + +class VwLabelValue(models.Model): + concept = models.ForeignKey( + ResourceInstance, + related_name="label_values", + on_delete=models.DO_NOTHING, + db_column="conceptid", + ) + value = models.CharField(db_column="value") + + class Meta: + managed = False + db_table = f"{settings.APP_NAME}__vw_label_values" diff --git a/arches_lingo/views/trees.py b/arches_lingo/views/trees.py index 1c523f3f..9b51d4df 100644 --- a/arches_lingo/views/trees.py +++ b/arches_lingo/views/trees.py @@ -35,6 +35,7 @@ PREF_LABEL_VALUE_ID, ALT_LABEL_VALUE_ID, ) +from arches_lingo.models import VwLabelValue TOP_CONCEPT_OF_LOOKUP = f"data__{TOP_CONCEPT_OF_NODE_AND_NODEGROUP}" BROADER_LOOKUP = f"data__{CLASSIFICATION_STATUS_ASCRIBED_CLASSIFICATION_NODEID}" @@ -45,6 +46,7 @@ class JsonbArrayElements(Func): """https://forum.djangoproject.com/t/django-4-2-behavior-change-when-using-arrayagg-on-unnested-arrayfield-postgresql-specific/21547/5""" + arity = 1 contains_subquery = True function = "JSONB_ARRAY_ELEMENTS" @@ -291,21 +293,14 @@ def get(self, request): # TODO: fuzzy match, SEARCH_TERM_SENSITIVITY concept_ids = ( - TileModel.objects.filter(nodegroup_id=CONCEPT_NAME_NODEGROUP) - .annotate(labels=self.labels_subquery(CONCEPT_NAME_NODEGROUP)) - # TODO: all languages - .filter( - **{ - f"data__{CONCEPT_NAME_CONTENT_NODE}__en__value__icontains": search_term - } - ) - .values_list("resourceinstance_id", flat=True) + VwLabelValue.objects.filter(value__icontains=search_term) + .values_list("concept_id", flat=True) + .distinct() ) - deduped = set(concept_ids) data = [ self.serialize_concept(str(concept_uuid), parentage=True) - for concept_uuid in deduped + for concept_uuid in concept_ids ] # Todo: filter by nodegroup permissions From 5021c69877c4057df6b5403beae85d83e4b093ec Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Thu, 22 Aug 2024 11:40:19 -0400 Subject: [PATCH 08/94] Add fuzzy searching --- arches_lingo/migrations/0001_initial.py | 7 ++++ arches_lingo/views/trees.py | 43 +++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/arches_lingo/migrations/0001_initial.py b/arches_lingo/migrations/0001_initial.py index ebf5f423..54b1b269 100644 --- a/arches_lingo/migrations/0001_initial.py +++ b/arches_lingo/migrations/0001_initial.py @@ -2,11 +2,17 @@ import textwrap from django.conf import settings +from django.contrib.postgres.operations import CreateExtension from django.db import migrations, models from arches_lingo.const import CONCEPT_NAME_CONTENT_NODE +class FuzzyStrMatchExtension(CreateExtension): + def __init__(self): + self.name = "fuzzystrmatch" + + class Migration(migrations.Migration): initial = True @@ -52,4 +58,5 @@ class Migration(migrations.Migration): "managed": False, }, ), + FuzzyStrMatchExtension(), ] diff --git a/arches_lingo/views/trees.py b/arches_lingo/views/trees.py index 9b51d4df..27cc41fa 100644 --- a/arches_lingo/views/trees.py +++ b/arches_lingo/views/trees.py @@ -3,7 +3,14 @@ from django.core.cache import caches from django.contrib.postgres.expressions import ArraySubquery -from django.db.models import CharField, F, OuterRef, Subquery, Value +from django.db.models import ( + CharField, + FloatField, + F, + OuterRef, + Subquery, + Value, +) from django.db.models.expressions import CombinedExpression, Func from django.utils.translation import gettext_lazy as _ from django.utils.decorators import method_decorator @@ -15,6 +22,7 @@ TileModel, Value as ConceptValue, ) +from arches.app.models.system_settings import settings from arches.app.utils.decorators import group_required from arches.app.utils.response import JSONResponse @@ -51,6 +59,11 @@ class JsonbArrayElements(Func): function = "JSONB_ARRAY_ELEMENTS" +class LevenshteinLessEqual(Func): + arity = 3 + function = "LEVENSHTEIN_LESS_EQUAL" + + @method_decorator( group_required("RDM Administrator", raise_exception=True), name="dispatch" ) @@ -286,14 +299,24 @@ def get(self, request): class ValueSearchView(ConceptTreeView): def get(self, request): search_term = request.GET.get("search") + max_edit_distance = request.GET.get( + "maxEditDistance", self.default_sensitivity() + ) if not search_term: # Useful for warming the cache before a search. self.rebuild_cache() return JSONResponse(status=HTTPStatus.IM_A_TEAPOT) - # TODO: fuzzy match, SEARCH_TERM_SENSITIVITY concept_ids = ( - VwLabelValue.objects.filter(value__icontains=search_term) + VwLabelValue.objects.annotate( + edit_distance=LevenshteinLessEqual( + F("value"), + Value(search_term), + Value(max_edit_distance), + output_field=FloatField(), + ) + ) + .filter(edit_distance__lte=max_edit_distance) .values_list("concept_id", flat=True) .distinct() ) @@ -305,3 +328,17 @@ def get(self, request): # Todo: filter by nodegroup permissions return JSONResponse(data) + + @staticmethod + def default_sensitivity(): + """Remains to be seen whether the existing elastic sensitivity setting + should be the fallback, but stub something out for now. + This sensitivity setting is actually inversely related to edit distance, + because it's prefix_length in elastic, not fuzziness, so invert it. + """ + elastic_prefix_length = settings.SEARCH_TERM_SENSITIVITY + if elastic_prefix_length <= 0: + return 5 + if elastic_prefix_length >= 5: + return 0 + return int(5 - elastic_prefix_length) From 8783cdc6801b556eab53323a9d43cb62e176e3e9 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Thu, 22 Aug 2024 16:00:04 -0400 Subject: [PATCH 09/94] Make chosen parentage deterministic --- arches_lingo/views/trees.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/arches_lingo/views/trees.py b/arches_lingo/views/trees.py index 27cc41fa..6f3d3f9a 100644 --- a/arches_lingo/views/trees.py +++ b/arches_lingo/views/trees.py @@ -247,7 +247,6 @@ def serialize_concept(self, conceptid: str, *, parentage=False): ], } if parentage: - # Choose any reverse path back to the scheme (currently indeterminate). path = self.add_broader_concept_recursive([], conceptid) scheme_id, concept_ids = path[0], path[1:] schemes = [scheme for scheme in self.schemes if str(scheme.pk) == scheme_id] @@ -258,18 +257,19 @@ def serialize_concept(self, conceptid: str, *, parentage=False): return data def add_broader_concept_recursive(self, working_parent_list, conceptid): - broader_concepts = self.broader_concepts[conceptid] + # TODO: sort on sortorder at higher stacklevel once captured in original data. + broader_concepts = sorted(self.broader_concepts[conceptid]) try: - arbitrary_broader_conceptid = next(iter(broader_concepts)) - except StopIteration: - schemes = self.schemes_by_top_concept[conceptid] - arbitrary_scheme = next(iter(schemes)) - working_parent_list.insert(0, arbitrary_scheme) + first_broader_conceptid = broader_concepts[0] + except IndexError: + # TODO: sort here too. + schemes = sorted(self.schemes_by_top_concept[conceptid]) + working_parent_list.insert(0, schemes[0]) return working_parent_list else: - working_parent_list.insert(0, arbitrary_broader_conceptid) + working_parent_list.insert(0, first_broader_conceptid) return self.add_broader_concept_recursive( - working_parent_list, arbitrary_broader_conceptid + working_parent_list, first_broader_conceptid ) def serialize_concept_label(self, label_tile: dict): From 760e7423cb0acfb8a709c0d9f231977c7fc6ba0d Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Thu, 22 Aug 2024 16:07:03 -0400 Subject: [PATCH 10/94] Add polyhierarchical flag --- arches_lingo/views/trees.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/arches_lingo/views/trees.py b/arches_lingo/views/trees.py index 6f3d3f9a..c155cbf0 100644 --- a/arches_lingo/views/trees.py +++ b/arches_lingo/views/trees.py @@ -89,6 +89,9 @@ def __init__(self): self.read_from_cache() + # Not currently cached because written to during serialization. + self.polyhierarchical_concepts = set() + def read_from_cache(self): from_cache = cache.get_many( [ @@ -248,12 +251,19 @@ def serialize_concept(self, conceptid: str, *, parentage=False): } if parentage: path = self.add_broader_concept_recursive([], conceptid) - scheme_id, concept_ids = path[0], path[1:] + scheme_id, parent_concept_ids = path[0], path[1:] + if len(parent_concept_ids) > 1: + self.polyhierarchical_concepts.add(conceptid) schemes = [scheme for scheme in self.schemes if str(scheme.pk) == scheme_id] data["parentage"] = [self.serialize_scheme(schemes[0])] + [ - self.serialize_concept(concept_id) for concept_id in concept_ids + self.serialize_concept(parent_id) for parent_id in parent_concept_ids ] + self_and_parent_ids = set([conceptid] + parent_concept_ids) + data["polyhierarchical"] = bool( + self_and_parent_ids.intersection(self.polyhierarchical_concepts) + ) + return data def add_broader_concept_recursive(self, working_parent_list, conceptid): From 66ccf68fa56bd8f01c55f59f970b55d4883da06c Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Thu, 22 Aug 2024 16:23:34 -0400 Subject: [PATCH 11/94] Shorten the serialization of parent path (just show labels) --- arches_lingo/views/trees.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/arches_lingo/views/trees.py b/arches_lingo/views/trees.py index c155cbf0..91c2a298 100644 --- a/arches_lingo/views/trees.py +++ b/arches_lingo/views/trees.py @@ -214,16 +214,18 @@ def populate_schemes(self): graph_id=SCHEMES_GRAPH_ID ).annotate(labels=self.labels_subquery(SCHEME_NAME_NODEGROUP)) - def serialize_scheme(self, scheme: ResourceInstance): + def serialize_scheme(self, scheme: ResourceInstance, *, children=True): scheme_id: str = str(scheme.pk) - return { + data = { "id": scheme_id, "labels": [self.serialize_scheme_label(label) for label in scheme.labels], - "top_concepts": [ + } + if children: + data["top_concepts"] = [ self.serialize_concept(concept_id) for concept_id in self.top_concepts[scheme_id] - ], - } + ] + return data def serialize_scheme_label(self, label_tile: dict): lang_code = self.language_concepts[label_tile[SCHEME_NAME_LANGUAGE_NODE][0]] @@ -238,25 +240,27 @@ def serialize_scheme_label(self, label_tile: dict): "value": value, } - def serialize_concept(self, conceptid: str, *, parentage=False): + def serialize_concept(self, conceptid: str, *, parents=False, children=True): data = { "id": conceptid, "labels": [ self.serialize_concept_label(label) for label in self.labels[conceptid] ], - "narrower": [ + } + if children: + data["narrower"] = [ self.serialize_concept(conceptid) for conceptid in self.narrower_concepts[conceptid] - ], - } - if parentage: + ] + if parents: path = self.add_broader_concept_recursive([], conceptid) scheme_id, parent_concept_ids = path[0], path[1:] if len(parent_concept_ids) > 1: self.polyhierarchical_concepts.add(conceptid) schemes = [scheme for scheme in self.schemes if str(scheme.pk) == scheme_id] - data["parentage"] = [self.serialize_scheme(schemes[0])] + [ - self.serialize_concept(parent_id) for parent_id in parent_concept_ids + data["parents"] = [self.serialize_scheme(schemes[0], children=False)] + [ + self.serialize_concept(parent_id, children=False) + for parent_id in parent_concept_ids ] self_and_parent_ids = set([conceptid] + parent_concept_ids) @@ -332,7 +336,7 @@ def get(self, request): ) data = [ - self.serialize_concept(str(concept_uuid), parentage=True) + self.serialize_concept(str(concept_uuid), parents=True) for concept_uuid in concept_ids ] From 880fc19e9ce55c10a537bf193fde55718b2c4dce Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Thu, 22 Aug 2024 16:41:05 -0400 Subject: [PATCH 12/94] Add pagination --- arches_lingo/views/trees.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/arches_lingo/views/trees.py b/arches_lingo/views/trees.py index 91c2a298..ed692db7 100644 --- a/arches_lingo/views/trees.py +++ b/arches_lingo/views/trees.py @@ -1,8 +1,9 @@ from collections import defaultdict from http import HTTPStatus -from django.core.cache import caches from django.contrib.postgres.expressions import ArraySubquery +from django.core.cache import caches +from django.core.paginator import Paginator from django.db.models import ( CharField, FloatField, @@ -312,16 +313,18 @@ def get(self, request): ) class ValueSearchView(ConceptTreeView): def get(self, request): - search_term = request.GET.get("search") - max_edit_distance = request.GET.get( - "maxEditDistance", self.default_sensitivity() - ) - if not search_term: + if not (search_term := request.GET.get("search")): # Useful for warming the cache before a search. self.rebuild_cache() return JSONResponse(status=HTTPStatus.IM_A_TEAPOT) - concept_ids = ( + max_edit_distance = request.GET.get( + "maxEditDistance", self.default_sensitivity() + ) + page_number = request.GET.get("page", 1) + items_per_page = request.GET.get("items", 25) + + concept_query = ( VwLabelValue.objects.annotate( edit_distance=LevenshteinLessEqual( F("value"), @@ -331,13 +334,17 @@ def get(self, request): ) ) .filter(edit_distance__lte=max_edit_distance) + .order_by("edit_distance") .values_list("concept_id", flat=True) .distinct() ) + paginator = Paginator(concept_query, items_per_page) + page = paginator.get_page(page_number) + data = [ self.serialize_concept(str(concept_uuid), parents=True) - for concept_uuid in concept_ids + for concept_uuid in page ] # Todo: filter by nodegroup permissions From 18c5de6368126d171525805d31ac3303d8f0ccd7 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Thu, 22 Aug 2024 16:43:50 -0400 Subject: [PATCH 13/94] Rename search term param --- arches_lingo/views/trees.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arches_lingo/views/trees.py b/arches_lingo/views/trees.py index ed692db7..6c63b07d 100644 --- a/arches_lingo/views/trees.py +++ b/arches_lingo/views/trees.py @@ -313,7 +313,7 @@ def get(self, request): ) class ValueSearchView(ConceptTreeView): def get(self, request): - if not (search_term := request.GET.get("search")): + if not (term := request.GET.get("term")): # Useful for warming the cache before a search. self.rebuild_cache() return JSONResponse(status=HTTPStatus.IM_A_TEAPOT) @@ -328,7 +328,7 @@ def get(self, request): VwLabelValue.objects.annotate( edit_distance=LevenshteinLessEqual( F("value"), - Value(search_term), + Value(term), Value(max_edit_distance), output_field=FloatField(), ) From fe86962b9a4d2779fdb4642fa0c6f53c8244510c Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Thu, 22 Aug 2024 17:00:16 -0400 Subject: [PATCH 14/94] Avoid empty narrower keys --- arches_lingo/views/trees.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arches_lingo/views/trees.py b/arches_lingo/views/trees.py index 6c63b07d..6c03a0a5 100644 --- a/arches_lingo/views/trees.py +++ b/arches_lingo/views/trees.py @@ -343,7 +343,7 @@ def get(self, request): page = paginator.get_page(page_number) data = [ - self.serialize_concept(str(concept_uuid), parents=True) + self.serialize_concept(str(concept_uuid), parents=True, children=False) for concept_uuid in page ] From 53e0105f1856fa30a43d5632c42231cb8bf58452 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Thu, 22 Aug 2024 20:33:13 -0400 Subject: [PATCH 15/94] Fix typo that broke cache --- arches_lingo/views/trees.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arches_lingo/views/trees.py b/arches_lingo/views/trees.py index 6c03a0a5..f65f9cc7 100644 --- a/arches_lingo/views/trees.py +++ b/arches_lingo/views/trees.py @@ -112,7 +112,7 @@ def read_from_cache(self): self.schemes = from_cache["schemes"] self.labels = from_cache["labels"] self.broader_concepts = from_cache["broader_concepts"] - self.schemes_by_top_concepts = from_cache["schemes_by_top_concepts"] + self.schemes_by_top_concept = from_cache["schemes_by_top_concept"] except KeyError: self.rebuild_cache() From f43f9138e8eb84ae036965babe92d0d8b971b89f Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Thu, 22 Aug 2024 20:43:36 -0400 Subject: [PATCH 16/94] fixup! Add fuzzy --- arches_lingo/migrations/0001_initial.py | 7 ++----- arches_lingo/models.py | 3 +-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/arches_lingo/migrations/0001_initial.py b/arches_lingo/migrations/0001_initial.py index 54b1b269..df67f08a 100644 --- a/arches_lingo/migrations/0001_initial.py +++ b/arches_lingo/migrations/0001_initial.py @@ -1,7 +1,6 @@ # Generated by Django 4.2.15 on 2024-08-22 09:01 import textwrap -from django.conf import settings from django.contrib.postgres.operations import CreateExtension from django.db import migrations, models @@ -19,11 +18,9 @@ class Migration(migrations.Migration): dependencies = [("models", "11042_update__arches_staging_to_tile")] - view_name = f"{settings.APP_NAME}__vw_label_values" - forward = textwrap.dedent( f""" - CREATE MATERIALIZED VIEW {view_name} AS ( + CREATE MATERIALIZED VIEW arches_lingo AS ( SELECT t.resourceinstanceid AS conceptid, ROW_TO_JSON(JSONB_EACH(t.tiledata -> '{CONCEPT_NAME_CONTENT_NODE}')) @@ -35,7 +32,7 @@ class Migration(migrations.Migration): );""" ) - reverse = f"DROP MATERIALIZED VIEW {view_name};" + reverse = "DROP MATERIALIZED VIEW arches_lingo;" operations = [ migrations.RunSQL(forward, reverse), diff --git a/arches_lingo/models.py b/arches_lingo/models.py index e239c1cd..fa87287a 100644 --- a/arches_lingo/models.py +++ b/arches_lingo/models.py @@ -1,4 +1,3 @@ -from django.conf import settings from django.db import models from arches.app.models.models import ResourceInstance @@ -15,4 +14,4 @@ class VwLabelValue(models.Model): class Meta: managed = False - db_table = f"{settings.APP_NAME}__vw_label_values" + db_table = f"arches_lingo__vw_label_values" From 818b51fdc7a5a45e917c65f97f5b0f716a3ad8a4 Mon Sep 17 00:00:00 2001 From: Christopher Byrd Date: Fri, 23 Aug 2024 16:15:22 -0700 Subject: [PATCH 17/94] rough out search bar #19 --- .../basic-search/BasicSearchComponent.vue | 192 ++++++++++++++++++ .../src/arches_lingo/pages/BasicSearch.vue | 11 +- 2 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 arches_lingo/src/arches_lingo/components/basic-search/BasicSearchComponent.vue diff --git a/arches_lingo/src/arches_lingo/components/basic-search/BasicSearchComponent.vue b/arches_lingo/src/arches_lingo/components/basic-search/BasicSearchComponent.vue new file mode 100644 index 00000000..ba56ac0b --- /dev/null +++ b/arches_lingo/src/arches_lingo/components/basic-search/BasicSearchComponent.vue @@ -0,0 +1,192 @@ + + + + + \ No newline at end of file diff --git a/arches_lingo/src/arches_lingo/pages/BasicSearch.vue b/arches_lingo/src/arches_lingo/pages/BasicSearch.vue index fd68eb55..6d1a3081 100644 --- a/arches_lingo/src/arches_lingo/pages/BasicSearch.vue +++ b/arches_lingo/src/arches_lingo/pages/BasicSearch.vue @@ -1 +1,10 @@ - + + + \ No newline at end of file From 60a551f15bf765b2bfe390d9a27cedf2e2ab66c2 Mon Sep 17 00:00:00 2001 From: Christopher Byrd Date: Sun, 25 Aug 2024 17:03:51 -0700 Subject: [PATCH 18/94] nti #19 --- .../basic-search/BasicSearchComponent.vue | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/arches_lingo/src/arches_lingo/components/basic-search/BasicSearchComponent.vue b/arches_lingo/src/arches_lingo/components/basic-search/BasicSearchComponent.vue index ba56ac0b..b0528c10 100644 --- a/arches_lingo/src/arches_lingo/components/basic-search/BasicSearchComponent.vue +++ b/arches_lingo/src/arches_lingo/components/basic-search/BasicSearchComponent.vue @@ -1,5 +1,5 @@ \ No newline at end of file From e5be36a0ade3d68774b2f29b751a7afda13bc2c0 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 26 Aug 2024 08:50:57 -0400 Subject: [PATCH 19/94] Return all results when term is empty --- arches_lingo/views/trees.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/arches_lingo/views/trees.py b/arches_lingo/views/trees.py index f65f9cc7..a0eb7154 100644 --- a/arches_lingo/views/trees.py +++ b/arches_lingo/views/trees.py @@ -1,5 +1,4 @@ from collections import defaultdict -from http import HTTPStatus from django.contrib.postgres.expressions import ArraySubquery from django.core.cache import caches @@ -313,31 +312,30 @@ def get(self, request): ) class ValueSearchView(ConceptTreeView): def get(self, request): - if not (term := request.GET.get("term")): - # Useful for warming the cache before a search. - self.rebuild_cache() - return JSONResponse(status=HTTPStatus.IM_A_TEAPOT) - + term = request.GET.get("term") max_edit_distance = request.GET.get( "maxEditDistance", self.default_sensitivity() ) page_number = request.GET.get("page", 1) items_per_page = request.GET.get("items", 25) - concept_query = ( - VwLabelValue.objects.annotate( - edit_distance=LevenshteinLessEqual( - F("value"), - Value(term), - Value(max_edit_distance), - output_field=FloatField(), + concept_query = VwLabelValue.objects.all() + if term: + concept_query = ( + concept_query.annotate( + edit_distance=LevenshteinLessEqual( + F("value"), + Value(term), + Value(max_edit_distance), + output_field=FloatField(), + ) ) + .filter(edit_distance__lte=max_edit_distance) + .order_by("edit_distance") ) - .filter(edit_distance__lte=max_edit_distance) - .order_by("edit_distance") - .values_list("concept_id", flat=True) - .distinct() - ) + else: + concept_query = concept_query.order_by("concept_id") + concept_query = concept_query.values_list("concept_id", flat=True).distinct() paginator = Paginator(concept_query, items_per_page) page = paginator.get_page(page_number) From b8486421b0924244cb30caafb860fdf41f56e5b1 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 26 Aug 2024 11:23:47 -0400 Subject: [PATCH 20/94] fixup! Add fuzzy --- arches_lingo/migrations/0001_initial.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arches_lingo/migrations/0001_initial.py b/arches_lingo/migrations/0001_initial.py index df67f08a..12679df0 100644 --- a/arches_lingo/migrations/0001_initial.py +++ b/arches_lingo/migrations/0001_initial.py @@ -20,7 +20,7 @@ class Migration(migrations.Migration): forward = textwrap.dedent( f""" - CREATE MATERIALIZED VIEW arches_lingo AS ( + CREATE MATERIALIZED VIEW arches_lingo__vw_label_values AS ( SELECT t.resourceinstanceid AS conceptid, ROW_TO_JSON(JSONB_EACH(t.tiledata -> '{CONCEPT_NAME_CONTENT_NODE}')) @@ -32,7 +32,7 @@ class Migration(migrations.Migration): );""" ) - reverse = "DROP MATERIALIZED VIEW arches_lingo;" + reverse = "DROP MATERIALIZED VIEW arches_lingo__vw_label_values;" operations = [ migrations.RunSQL(forward, reverse), From 0a18d497fe4e41315a3cc8967d83a121544621e3 Mon Sep 17 00:00:00 2001 From: Christopher Byrd Date: Mon, 26 Aug 2024 10:51:50 -0700 Subject: [PATCH 21/94] nit #19 --- .../basic-search/BasicSearchComponent.vue | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/arches_lingo/src/arches_lingo/components/basic-search/BasicSearchComponent.vue b/arches_lingo/src/arches_lingo/components/basic-search/BasicSearchComponent.vue index b0528c10..9bf243f7 100644 --- a/arches_lingo/src/arches_lingo/components/basic-search/BasicSearchComponent.vue +++ b/arches_lingo/src/arches_lingo/components/basic-search/BasicSearchComponent.vue @@ -12,7 +12,8 @@ const delay = 300; const instance = ref(null); const query = ref(null); -const results = ref[]>([]); +const queryString = ref(''); +const results = ref([]); const isLoading = ref(false); const shouldShowClearInputButton = ref(false); @@ -112,20 +113,18 @@ const mockData = () => { }; const fetchData = async () => { + isLoading.value = true; shouldShowClearInputButton.value = false; + instance.value.overlayVisible = false; - if (!query.value) return; - - isLoading.value = true; try { const data = await mockData(); results.value = data; + shouldShowClearInputButton.value = true; } catch (error) { console.error('Error fetching data:', error); - results.value = []; } finally { isLoading.value = false; - shouldShowClearInputButton.value = true; } }; @@ -140,8 +139,9 @@ let keepOpen = false; const openDropdown = () => instance.value?.show() -const selectHandler = (foo) => { - keepOpen = true; +const selectHandler = () => { + query.value = queryString.value; + keepOpen = true; }; const beforeHideHandler = () => { @@ -153,6 +153,14 @@ const beforeHideHandler = () => { } }; +const foo = (value) => { + if (!value) { + shouldShowClearInputButton.value = false; + } + + if (typeof value === 'string') { queryString.value = value;} +} +