diff --git a/pyproject.toml b/pyproject.toml
index 0030ed6..6d49cdb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,7 +16,8 @@ dependencies = [
"xlsxwriter",
"plotly[express]",
"httpx",
- "pandas" # utilisé pour la création de certains graphiques
+ "pandas", # utilisé pour la création de certains graphiques
+ "unidecode"
]
[project.optional-dependencies]
diff --git a/src/pages/recherche.py b/src/pages/recherche.py
index 834dcd5..c6a7224 100644
--- a/src/pages/recherche.py
+++ b/src/pages/recherche.py
@@ -1,6 +1,12 @@
-from dash import Input, Output, callback, dcc, html, register_page
+from dash import Input, Output, callback, dash_table, dcc, html, register_page
-from src.utils import meta_content
+from src.utils import (
+ df_acheteurs,
+ df_titulaires,
+ meta_content,
+ search_org,
+ setup_table_columns,
+)
name = "Recherche"
@@ -31,11 +37,44 @@ layout = html.Div(
)
-@callback(Output("search_results", "children"), Input("search", "value"))
-def search_results(query):
- if len(query) >= 3:
- # results = []
- # dff = dff.select("acheteur_id", "acheteur_nom")
- return html.Div([])
+@callback(
+ Output("search_results", "children"),
+ Input("search", "value"),
+ prevent_initial_call=True,
+)
+def update_search_results(query):
+ if len(query) >= 1:
+ content = []
+
+ for org_type in ["acheteur", "titulaire"]:
+ if org_type == "acheteur":
+ dff = df_acheteurs
+ elif org_type == "titulaire":
+ dff = df_titulaires
+ else:
+ raise ValueError(f"{org_type} is not supported")
+
+ # Search acheteurs and titulaires using the same function
+ results = search_org(dff, query, org_type=org_type)
+ count = results.height
+
+ # Format output
+ columns, tooltip = setup_table_columns(results, hideable=False)
+
+ org_content = [
+ html.H3(f"{org_type.title()}s : {count}"),
+ dash_table.DataTable(
+ columns=columns,
+ data=results.to_dicts(),
+ page_size=5,
+ style_table={"overflowX": "auto"},
+ markdown_options={"html": True},
+ )
+ if count > 0
+ else html.P(f"Aucun {org_type} trouvé."),
+ ]
+ content.extend(org_content)
+
+ return content
else:
- return html.P("Tapez au moins 3 caractères")
+ return html.P("Tapez au moins 1 caractère")
diff --git a/src/utils.py b/src/utils.py
index fd8816a..d2c7bc4 100644
--- a/src/utils.py
+++ b/src/utils.py
@@ -8,6 +8,7 @@ import polars.selectors as cs
from httpx import get
from polars import Schema
from polars.exceptions import ComputeError
+from unidecode import unidecode
operators = [
["s<", "<"],
@@ -50,30 +51,47 @@ def add_resource_link(dff: pl.DataFrame) -> pl.DataFrame:
return dff
-def add_links(dff: pl.DataFrame):
- dff = dff.with_columns(
- pl.when(pl.col("titulaire_typeIdentifiant") == "SIRET")
- .then(
- ''
- + pl.col("titulaire_id")
- + ""
- )
- .otherwise(pl.col("titulaire_id"))
- .alias("titulaire_id")
- )
-
- for column, path in [("acheteur_id", "acheteurs"), ("uid", "marches")]:
- dff = dff.with_columns(
- (
- f''
- + pl.col(column)
- + ""
- ).alias(column)
- )
+def add_links(dff: pl.DataFrame, target: str = "_blank"):
+ for col in ["uid", "acheteur_nom", "titulaire_nom", "acheteur_id", "titulaire_id"]:
+ if col in dff.columns:
+ if col.startswith("titulaire_"):
+ dff = dff.with_columns(
+ pl.when(
+ pl.Expr.or_(
+ pl.col("titulaire_typeIdentifiant").is_null(),
+ pl.col("titulaire_typeIdentifiant") == "SIRET",
+ )
+ )
+ .then(
+ ''
+ + pl.col(col)
+ + ""
+ )
+ .otherwise(pl.col(col))
+ .alias(col)
+ )
+ if col.startswith("acheteur_"):
+ dff = dff.with_columns(
+ (
+ ''
+ + pl.col(col)
+ + ""
+ ).alias(col)
+ )
+ if col == "uid":
+ dff = dff.with_columns(
+ (
+ ''
+ + pl.col("uid")
+ + ""
+ ).alias("uid")
+ )
return dff
@@ -211,11 +229,12 @@ def get_decp_data() -> pl.DataFrame:
def get_org_data(dff: pl.DataFrame, org_type: str) -> pl.DataFrame:
lff = dff.lazy()
lff = lff.select(
+ "uid",
cs.starts_with(org_type).exclude(
f"{org_type}_latitude", f"{org_type}_longitude"
- )
+ ),
)
- lff = lff.unique(f"{org_type}_id")
+ lff = lff.group_by(cs.starts_with(org_type)).len("len_uid")
return lff.collect()
@@ -375,6 +394,80 @@ def get_data_schema() -> dict:
return new_schema
+def search_org(dff: pl.DataFrame, query: str, org_type: str) -> pl.DataFrame:
+ """
+ Search in either 'acheteur' or 'titulaire' DataFrame.
+
+ :param dff: Polars DataFrame with acheteur or titulaire columns
+ :param query: User search string
+ :param org_type: 'acheteur' or 'titulaire'
+ :return: Filtered DataFrame with 'matches' column
+ """
+ if not query.strip():
+ return dff.select(pl.lit(False).alias("matches"))
+
+ # Normalize query
+ normalized_query = unidecode(query.strip()).upper()
+ tokens = [" " + t.strip() for t in normalized_query.split() if t.strip()]
+ print(tokens)
+
+ # Define columns based on entity type
+ cols = [
+ f"{org_type}_id",
+ f"{org_type}_nom",
+ f"{org_type}_departement_nom",
+ f"{org_type}_departement_code",
+ f"{org_type}_commune_nom",
+ ]
+
+ # Concatenate all fields into one string per row
+ org_str = pl.concat_str(pl.lit(" "), pl.col(cols), separator=" ")
+
+ # For each token, create a boolean column: True if token is found
+ token_matches = []
+ for token in tokens:
+ token_match = org_str.str.contains(token).alias(f"token_{token}")
+ token_matches.append(token_match)
+
+ # Count how many tokens match per row
+ match_score = pl.sum_horizontal(token_matches).alias("match_score")
+
+ # For each token, create a boolean column: True if token is found
+ token_matches = []
+ for token in tokens:
+ token_match = org_str.str.contains(token).alias(f"token_{token}")
+ token_matches.append(token_match)
+
+ # Sélection des colonnes
+ if org_type == "acheteur":
+ dff = dff.select(cols + ["len_uid"])
+ if org_type == "titulaire":
+ dff = dff.select(cols + ["len_uid", "titulaire_typeIdentifiant"])
+
+ # Apply and filter
+ dff = (
+ dff.with_columns(token_matches + [match_score])
+ .filter(pl.col("match_score") == len(tokens))
+ .sort("len_uid", descending=True)
+ .drop([f"token_{token}" for token in tokens])
+ )
+
+ # Format result
+ dff = add_links(dff, target="")
+ dff = dff.with_columns(
+ pl.concat_str(
+ pl.col(f"{org_type}_departement_nom"),
+ pl.lit(" ("),
+ pl.col(f"{org_type}_departement_code"),
+ pl.lit(")"),
+ ).alias("Département")
+ )
+
+ dff = dff.select(f"{org_type}_id", f"{org_type}_nom", "Département")
+
+ return dff
+
+
df: pl.DataFrame = get_decp_data()
df_acheteurs = get_org_data(df, "acheteur")
df_titulaires = get_org_data(df, "titulaire")