mirror of
https://github.com/suitenumerique/docs.git
synced 2026-09-13 13:17:53 +02:00
Integrate Find (#1834)
## Purpose integrate Find to Docs ## Proposal - [x] ✨ add a `useSeachDocs` hook in charged of calling the search endpoint. - [x] ✨ add a optional `path` param to the `search` route. This param represents the parent document path in case of a sub-documents (descendants) search. - [x] ⚡️return Indexer results directly without DB calls to retrieve the Document objects. All informations necessary for display are indexed in Find. We can skip the DB calls and improve performance. - [x] ♻️ refactor react `DocSearchContent` components. `DocSearchContent` and `DocSearchSubContent` are now merged a unique component handling all search scenarios and relying on the unique `search` route. - [x] 🔥remove pagination logic in the Indexer. Removing the DB calls also removes the DRF queryset object which handles the pagination. Also we consider pagination not to be necessary for search v1. - [x] 🔥remove the `document/<document_id>/descendants` route. This route is not used anymore. The logic of finding the descendants are moved to the internal `_list_descendants` method. This method is based on the parent `path` instead of the parent `id` which has some consequence about the user access management. Relying on the path prevents the use of the `self.get_object()` method which used to handle the user access logic. - [x] ✨handle fallback logic on DRF based title search in case of non-configured, badly configured or failing at run time indexer. - [x] ✨handle language extension in `title` field. Find returns titles with a language extension (ex: `{ title.fr: "rapport d'activité" }` instead of `{ "title": "rapport d'activité" }`. - [x] 🔧 add a `common.test` file to allow running the tests without docker - [x] ♻️ rename `SearchIndexer` -> `FindDocumentIndexer`. This class has to do with Find in particular and the convention is more coherent with `BaseDocumentIndexer` - [x] ♻️ rename `SEARCH_INDEXER_URL` -> `INDEXING_URL` and `SEARCH_INDEXER_QUERY_URL` -> `SEARCH_URL`. I found the original names very confusing. - [x] 🔧 update the environment variables to activate the FindDocumentIndexer. - [x] ✨automate the generation of encryption key during bootstrap. OIDC_STORE_REFRESH_TOKEN_KEY is a mandatory secret key. We can not push it on Github and we want any contributor to be able to run the app by only running the `make bootstrap`. We chose to generate and wright it into the `common.local` during bootstrap. ## External contributions Thank you for your contribution! 🎉 Please ensure the following items are checked before submitting your pull request: - [x] I have read and followed the [contributing guidelines](https://github.com/suitenumerique/docs/blob/main/CONTRIBUTING.md) - [x] I have read and agreed to the [Code of Conduct](https://github.com/suitenumerique/docs/blob/main/CODE_OF_CONDUCT.md) - [x] I have signed off my commits with `git commit --signoff` (DCO compliance) - [x] I have signed my commits with my SSH or GPG key (`git commit -S`) - [x] My commit messages follow the required format: `<gitmoji>(type) title description` - [x] I have added a changelog entry under `## [Unreleased]` section (if noticeable change) - [x] I have added corresponding tests for new features or bug fixes (if applicable) --------- Signed-off-by: charles <charles.englebert@protonmail.com>
This commit is contained in:
@@ -8,7 +8,6 @@ from functools import cache
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.models import Subquery
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
import requests
|
||||
@@ -78,7 +77,9 @@ def get_visited_document_ids_of(queryset, user):
|
||||
if isinstance(user, AnonymousUser):
|
||||
return []
|
||||
|
||||
qs = models.LinkTrace.objects.filter(user=user)
|
||||
visited_ids = models.LinkTrace.objects.filter(user=user).values_list(
|
||||
"document_id", flat=True
|
||||
)
|
||||
|
||||
docs = (
|
||||
queryset.exclude(accesses__user=user)
|
||||
@@ -86,7 +87,7 @@ def get_visited_document_ids_of(queryset, user):
|
||||
deleted_at__isnull=True,
|
||||
ancestors_deleted_at__isnull=True,
|
||||
)
|
||||
.filter(pk__in=Subquery(qs.values("document_id")))
|
||||
.filter(pk__in=visited_ids)
|
||||
.order_by("pk")
|
||||
.distinct("pk")
|
||||
)
|
||||
@@ -107,15 +108,13 @@ class BaseDocumentIndexer(ABC):
|
||||
Initialize the indexer.
|
||||
"""
|
||||
self.batch_size = settings.SEARCH_INDEXER_BATCH_SIZE
|
||||
self.indexer_url = settings.SEARCH_INDEXER_URL
|
||||
self.indexer_url = settings.INDEXING_URL
|
||||
self.indexer_secret = settings.SEARCH_INDEXER_SECRET
|
||||
self.search_url = settings.SEARCH_INDEXER_QUERY_URL
|
||||
self.search_url = settings.SEARCH_URL
|
||||
self.search_limit = settings.SEARCH_INDEXER_QUERY_LIMIT
|
||||
|
||||
if not self.indexer_url:
|
||||
raise ImproperlyConfigured(
|
||||
"SEARCH_INDEXER_URL must be set in Django settings."
|
||||
)
|
||||
raise ImproperlyConfigured("INDEXING_URL must be set in Django settings.")
|
||||
|
||||
if not self.indexer_secret:
|
||||
raise ImproperlyConfigured(
|
||||
@@ -123,9 +122,7 @@ class BaseDocumentIndexer(ABC):
|
||||
)
|
||||
|
||||
if not self.search_url:
|
||||
raise ImproperlyConfigured(
|
||||
"SEARCH_INDEXER_QUERY_URL must be set in Django settings."
|
||||
)
|
||||
raise ImproperlyConfigured("SEARCH_URL must be set in Django settings.")
|
||||
|
||||
def index(self, queryset=None, batch_size=None):
|
||||
"""
|
||||
@@ -185,7 +182,7 @@ class BaseDocumentIndexer(ABC):
|
||||
"""
|
||||
|
||||
# pylint: disable-next=too-many-arguments,too-many-positional-arguments
|
||||
def search(self, text, token, visited=(), nb_results=None):
|
||||
def search(self, q, token, visited=(), nb_results=None, path=None):
|
||||
"""
|
||||
Search for documents in Find app.
|
||||
Ensure the same default ordering as "Docs" list : -updated_at
|
||||
@@ -193,7 +190,7 @@ class BaseDocumentIndexer(ABC):
|
||||
Returns ids of the documents
|
||||
|
||||
Args:
|
||||
text (str): Text search content.
|
||||
q (str): user query.
|
||||
token (str): OIDC Authentication token.
|
||||
visited (list, optional):
|
||||
List of ids of active public documents with LinkTrace
|
||||
@@ -201,21 +198,24 @@ class BaseDocumentIndexer(ABC):
|
||||
nb_results (int, optional):
|
||||
The number of results to return.
|
||||
Defaults to 50 if not specified.
|
||||
path (str, optional):
|
||||
The parent path to search descendants of.
|
||||
"""
|
||||
nb_results = nb_results or self.search_limit
|
||||
response = self.search_query(
|
||||
results = self.search_query(
|
||||
data={
|
||||
"q": text,
|
||||
"q": q,
|
||||
"visited": visited,
|
||||
"services": ["docs"],
|
||||
"nb_results": nb_results,
|
||||
"order_by": "updated_at",
|
||||
"order_direction": "desc",
|
||||
"path": path,
|
||||
},
|
||||
token=token,
|
||||
)
|
||||
|
||||
return [d["_id"] for d in response]
|
||||
return results
|
||||
|
||||
@abstractmethod
|
||||
def search_query(self, data, token) -> dict:
|
||||
@@ -226,11 +226,57 @@ class BaseDocumentIndexer(ABC):
|
||||
"""
|
||||
|
||||
|
||||
class SearchIndexer(BaseDocumentIndexer):
|
||||
class FindDocumentIndexer(BaseDocumentIndexer):
|
||||
"""
|
||||
Document indexer that pushes documents to La Suite Find app.
|
||||
Document indexer that indexes and searches documents with La Suite Find app.
|
||||
"""
|
||||
|
||||
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
def search(self, q, token, visited=(), nb_results=None, path=None):
|
||||
"""format Find search results"""
|
||||
search_results = super().search(q, token, visited, nb_results, path)
|
||||
return [
|
||||
{
|
||||
**hit["_source"],
|
||||
"id": hit["_id"],
|
||||
"title": self.get_title(hit["_source"]),
|
||||
}
|
||||
for hit in search_results
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_title(source):
|
||||
"""
|
||||
Find returns the titles with an extension depending on the language.
|
||||
This function extracts the title in a generic way.
|
||||
|
||||
Handles multiple cases:
|
||||
- Localized title fields like "title.<some_extension>"
|
||||
- Fallback to plain "title" field if localized version not found
|
||||
- Returns empty string if no title field exists
|
||||
|
||||
Args:
|
||||
source (dict): The _source dictionary from a search hit
|
||||
|
||||
Returns:
|
||||
str: The extracted title or empty string if not found
|
||||
|
||||
Example:
|
||||
>>> get_title({"title.fr": "Bonjour", "id": 1})
|
||||
"Bonjour"
|
||||
>>> get_title({"title": "Hello", "id": 1})
|
||||
"Hello"
|
||||
>>> get_title({"id": 1})
|
||||
""
|
||||
"""
|
||||
titles = utils.get_value_by_pattern(source, r"^title\.")
|
||||
for title in titles:
|
||||
if title:
|
||||
return title
|
||||
if "title" in source:
|
||||
return source["title"]
|
||||
return ""
|
||||
|
||||
def serialize_document(self, document, accesses):
|
||||
"""
|
||||
Convert a Document to the JSON format expected by La Suite Find.
|
||||
|
||||
Reference in New Issue
Block a user