Recherche fonctinonelle acheteurs et titulaires #58

This commit is contained in:
Colin Maudry
2025-11-11 12:42:01 +01:00
parent 1e85dbe562
commit e187cf33aa
3 changed files with 169 additions and 36 deletions
+2 -1
View File
@@ -16,7 +16,8 @@ dependencies = [
"xlsxwriter", "xlsxwriter",
"plotly[express]", "plotly[express]",
"httpx", "httpx",
"pandas" # utilisé pour la création de certains graphiques "pandas", # utilisé pour la création de certains graphiques
"unidecode"
] ]
[project.optional-dependencies] [project.optional-dependencies]
+48 -9
View File
@@ -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" name = "Recherche"
@@ -31,11 +37,44 @@ layout = html.Div(
) )
@callback(Output("search_results", "children"), Input("search", "value")) @callback(
def search_results(query): Output("search_results", "children"),
if len(query) >= 3: Input("search", "value"),
# results = [] prevent_initial_call=True,
# dff = dff.select("acheteur_id", "acheteur_nom") )
return html.Div([]) 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: else:
return html.P("Tapez au moins 3 caractères") 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 1 caractère")
+108 -15
View File
@@ -8,6 +8,7 @@ import polars.selectors as cs
from httpx import get from httpx import get
from polars import Schema from polars import Schema
from polars.exceptions import ComputeError from polars.exceptions import ComputeError
from unidecode import unidecode
operators = [ operators = [
["s<", "<"], ["s<", "<"],
@@ -50,29 +51,46 @@ def add_resource_link(dff: pl.DataFrame) -> pl.DataFrame:
return dff return dff
def add_links(dff: pl.DataFrame): 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( dff = dff.with_columns(
pl.when(pl.col("titulaire_typeIdentifiant") == "SIRET") pl.when(
pl.Expr.or_(
pl.col("titulaire_typeIdentifiant").is_null(),
pl.col("titulaire_typeIdentifiant") == "SIRET",
)
)
.then( .then(
'<a href = "/titulaires/' '<a href = "/titulaires/'
+ pl.col("titulaire_id") + pl.col("titulaire_id")
+ '">' + f'" target="{target}">'
+ pl.col("titulaire_id") + pl.col(col)
+ "</a>" + "</a>"
) )
.otherwise(pl.col("titulaire_id")) .otherwise(pl.col(col))
.alias("titulaire_id") .alias(col)
) )
if col.startswith("acheteur_"):
for column, path in [("acheteur_id", "acheteurs"), ("uid", "marches")]:
dff = dff.with_columns( dff = dff.with_columns(
( (
f'<a href = "/{path}/' '<a href = "/acheteurs/'
+ pl.col(column) + pl.col("acheteur_id")
+ '" target="_blank">' + f'" target="{target}">'
+ pl.col(column) + pl.col(col)
+ "</a>" + "</a>"
).alias(column) ).alias(col)
)
if col == "uid":
dff = dff.with_columns(
(
'<a href = "/marches/'
+ pl.col("uid")
+ f'" target="{target}">'
+ pl.col("uid")
+ "</a>"
).alias("uid")
) )
return dff return dff
@@ -211,11 +229,12 @@ def get_decp_data() -> pl.DataFrame:
def get_org_data(dff: pl.DataFrame, org_type: str) -> pl.DataFrame: def get_org_data(dff: pl.DataFrame, org_type: str) -> pl.DataFrame:
lff = dff.lazy() lff = dff.lazy()
lff = lff.select( lff = lff.select(
"uid",
cs.starts_with(org_type).exclude( cs.starts_with(org_type).exclude(
f"{org_type}_latitude", f"{org_type}_longitude" f"{org_type}_latitude", f"{org_type}_longitude"
),
) )
) lff = lff.group_by(cs.starts_with(org_type)).len("len_uid")
lff = lff.unique(f"{org_type}_id")
return lff.collect() return lff.collect()
@@ -375,6 +394,80 @@ def get_data_schema() -> dict:
return new_schema 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: pl.DataFrame = get_decp_data()
df_acheteurs = get_org_data(df, "acheteur") df_acheteurs = get_org_data(df, "acheteur")
df_titulaires = get_org_data(df, "titulaire") df_titulaires = get_org_data(df, "titulaire")