Loren commited on
Commit
5327a45
·
verified ·
1 Parent(s): 70f9342

Upload 8 files

Browse files
app/database.py CHANGED
@@ -1,11 +1,26 @@
1
  import os
2
- from typing import List, Dict
3
  import duckdb
 
4
  import pandas as pd
5
  from huggingface_hub import hf_hub_download
 
 
 
 
 
 
6
 
7
  # Initialisations
 
 
 
8
  REPO_ID = "Loren/articles_database"
 
 
 
 
 
9
  cache_dir = "/tmp"
10
  os.makedirs(cache_dir, exist_ok=True)
11
  # Rediriger le cache HF globalement
@@ -38,6 +53,32 @@ con.execute(f"CREATE VIEW articles AS SELECT * FROM parquet_scan('{articles_parq
38
  con.execute(f"CREATE VIEW tags AS SELECT * FROM parquet_scan('{tags_parquet}')")
39
  con.execute(f"CREATE VIEW tag_article AS SELECT * FROM parquet_scan('{tag_article_parquet}')")
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  # Fonctions d'accès aux données
42
 
43
  def fetch_tags() -> List[str]:
@@ -108,4 +149,95 @@ def fetch_articles_by_tags(tags: List[str]) -> List[Dict]:
108
  result = con.execute(query, tags).fetchdf()
109
  return {"status": "ok", "result": result.to_dict(orient="records")}
110
  except Exception as e:
111
- return {"status": "error", "code": type(e).__name__, "message": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
+ from typing import List, Dict, Any
3
  import duckdb
4
+ import faiss
5
  import pandas as pd
6
  from huggingface_hub import hf_hub_download
7
+ from sentence_transformers import SentenceTransformer, CrossEncoder
8
+ import torch
9
+ from datasets import load_dataset
10
+ from dotenv import load_dotenv
11
+ import pyarrow as pa
12
+ import pyarrow.compute as pc
13
 
14
  # Initialisations
15
+ load_dotenv()
16
+ HF_TOKEN = os.getenv('HF_TOKEN')
17
+
18
  REPO_ID = "Loren/articles_database"
19
+ FAISS_REPO_ID = "Loren/articles_faiss"
20
+ FAISS_INDEX_FILE = "faiss_index.bin"
21
+ MODEL_NAME = "intfloat/multilingual-e5-small"
22
+ CROSS_ENCODER_NAME = "jinaai/jina-reranker-v2-base-multilingual"
23
+
24
  cache_dir = "/tmp"
25
  os.makedirs(cache_dir, exist_ok=True)
26
  # Rediriger le cache HF globalement
 
53
  con.execute(f"CREATE VIEW tags AS SELECT * FROM parquet_scan('{tags_parquet}')")
54
  con.execute(f"CREATE VIEW tag_article AS SELECT * FROM parquet_scan('{tag_article_parquet}')")
55
 
56
+ # Téléchargement des fichiers de la base faiss depuis le dataset Hugging Face
57
+ hf_faiss_index = hf_hub_download(
58
+ repo_id=FAISS_REPO_ID,
59
+ filename=FAISS_INDEX_FILE,
60
+ repo_type="dataset",
61
+ token=HF_TOKEN,
62
+ cache_dir=cache_dir
63
+ )
64
+
65
+ # Chargement de l’index FAISS
66
+ faiss_index = faiss.read_index(hf_faiss_index)
67
+
68
+ # Téléchargement des metadatas Faiss depuis le dataset Hugging Face
69
+ dataset = load_dataset(FAISS_REPO_ID, split="train")
70
+ arrow_table = dataset.data
71
+
72
+ # Creation du Sentence transformer model
73
+ device = "cuda" if torch.cuda.is_available() else "cpu"
74
+ print(f"*** Device: {device}")
75
+ model = SentenceTransformer(MODEL_NAME, device=device)
76
+
77
+ # Création du cross-encoder
78
+ cross_encoder = CrossEncoder(CROSS_ENCODER_NAME, device=device,
79
+ trust_remote_code=True)
80
+
81
+
82
  # Fonctions d'accès aux données
83
 
84
  def fetch_tags() -> List[str]:
 
149
  result = con.execute(query, tags).fetchdf()
150
  return {"status": "ok", "result": result.to_dict(orient="records")}
151
  except Exception as e:
152
+ return {"status": "error", "code": type(e).__name__, "message": str(e)}
153
+
154
+ def fetch_query_results(query: str, k_model: int = 10, k_cross: int = 5) -> Dict[str, Any]:
155
+ """
156
+ Exécute une requête de recherche sémantique avec FAISS, puis rerank avec un cross-encoder
157
+ et retourne les meilleurs passages enrichis avec des métadonnées provenant de DuckDB.
158
+
159
+ Paramètres
160
+ ----------
161
+ query : str
162
+ La requête texte fournie par l'utilisateur.
163
+ k_model : int, optionnel (défaut = 10)
164
+ Nombre de résultats les plus proches à récupérer depuis l'index FAISS.
165
+ k_cross : int, optionnel (défaut = 5)
166
+ Nombre de résultats finaux à conserver après reranking avec le cross-encoder.
167
+
168
+ Retour
169
+ ------
170
+ Dict[str, Any]
171
+ Un dictionnaire contenant :
172
+ - status : "ok" si succès, sinon "error"
173
+ - result : liste de résultats (si succès)
174
+ - code et message : informations d'erreur (si échec)
175
+ """
176
+ if not query:
177
+ return {"status": "error", "code": "no_query", "message": "Aucun query fourni."}
178
+ try:
179
+ query_vec = model.encode(["query: "+query], convert_to_numpy=True, normalize_embeddings=True)
180
+ distances, indices = faiss_index.search(query_vec, k_model)
181
+
182
+ # Résultats FAISS
183
+ faiss_ids_list = indices[0].tolist()
184
+ distances_list = distances[0].tolist()
185
+
186
+ # Filtrer Arrow sur les IDs trouvés
187
+ filtered_table = arrow_table.filter(
188
+ pc.is_in(arrow_table['faiss_id'],
189
+ value_set=pa.array(faiss_ids_list))
190
+ )
191
+
192
+ # Convertir Arrow → pandas pour ajouter la distance
193
+ df = filtered_table.to_pandas()
194
+
195
+ # Ajouter la distance en gardant l'ordre faiss_ids_list
196
+ distance_map = dict(zip(faiss_ids_list, distances_list))
197
+ df["distance"] = df["faiss_id"].map(distance_map)
198
+
199
+ # Cross-encoder
200
+ top_passages = df["chunk_text"].tolist()
201
+ cross_input = [(query, p) for p in top_passages]
202
+ cross_scores = cross_encoder.predict(cross_input)
203
+
204
+ # Rerank
205
+ df["cross_score"] = cross_scores
206
+ df = df.sort_values(by="cross_score", ascending=False)
207
+
208
+ # Garder top k_cross
209
+ df_top = df.head(k_cross)
210
+
211
+ # Enregistrer dans DuckDB
212
+ con.register("faiss_tmp", df_top)
213
+
214
+ sql = """
215
+ SELECT
216
+ f.faiss_id,
217
+ f.document_id,
218
+ f.distance,
219
+ f.cross_score,
220
+ f.chunk_text,
221
+ a.article_title,
222
+ CASE WHEN a.article_online
223
+ THEN a.article_url
224
+ ELSE 'Article unavailable' END AS url,
225
+ STRING_AGG(t.tag_name, ', ') AS tags
226
+ FROM faiss_tmp f
227
+ JOIN articles a ON f.document_id = a.article_id
228
+ JOIN tag_article ta ON a.article_id = ta.article_id
229
+ JOIN tags t ON ta.tag_id = t.tag_id
230
+ WHERE (LENGTH(article_text) - LENGTH(REPLACE(article_text, ' ', '')) + 1) >= 100
231
+ GROUP BY f.faiss_id, f.document_id, f.distance, f.cross_score, f.chunk_text,
232
+ a.article_title, a.article_online, a.article_url
233
+ ORDER BY AVG(f.cross_score)
234
+ """
235
+
236
+ duck_res = con.execute(sql).fetchdf()
237
+
238
+ # Liste finale de dictionnaires
239
+ list_result = duck_res.to_dict(orient="records")
240
+
241
+ return {"status": "ok", "result": list_result}
242
+ except Exception as e:
243
+ return {"status": "error", "code": type(e).__name__, "message": str(e)}
app/main.py CHANGED
@@ -1,5 +1,5 @@
1
  from fastapi import FastAPI, Query
2
- from typing import List
3
  from app import database
4
  from fastapi.middleware.cors import CORSMiddleware
5
 
@@ -92,3 +92,34 @@ def get_articles_with_tags(tags: List[str] = Query(..., description="Liste des t
92
  return dict_result
93
  except Exception as e:
94
  return {"status": "error", "code": type(e).__name__, "message": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from fastapi import FastAPI, Query
2
+ from typing import List, Optional, Dict, Any
3
  from app import database
4
  from fastapi.middleware.cors import CORSMiddleware
5
 
 
92
  return dict_result
93
  except Exception as e:
94
  return {"status": "error", "code": type(e).__name__, "message": str(e)}
95
+
96
+
97
+ @app.get("/get_query_results")
98
+ def get_query_results(query: str = Query(..., description="Requête de recherche textuelle"),
99
+ k_model: int = Query(10, description="Nombre de candidats retournés par FAISS"),
100
+ k_cross: int = Query(5, description="Nombre de résultats conservés après reranking")
101
+ ) -> Dict[str, Any]:
102
+ """
103
+ Récupère les résultats d'une requête en utilisant deux modèles de recherche.
104
+
105
+ Args:
106
+ query (str): La requête utilisateur pour laquelle récupérer les résultats.
107
+ k_model (int, optional): Nombre de résultats à retourner pour le modèle principal. Par défaut à 10.
108
+ k_cross (int, optional): Nombre de résultats à retourner pour le modèle croisé. Par défaut à 5.
109
+
110
+ Returns:
111
+ Dict[str, Any]: Un dictionnaire contenant soit les résultats de la requête, soit les informations d'erreur.
112
+
113
+ Notes:
114
+ - L'appel de cet endpoint utilise la fonction `fetch_query_result` pour obtenir les résultats.
115
+ - En cas de problème lors du traitement de la requête, un message d'erreur détaillé est retourné.
116
+ """
117
+ try:
118
+ dict_result = database.fetch_query_results(query, k_model, k_cross)
119
+ if dict_result["status"] == "ok":
120
+ return {"status": "ok",
121
+ "results": dict_result["result"]}
122
+ else:
123
+ return dict_result
124
+ except Exception as e:
125
+ return {"status": "error", "code": type(e).__name__, "message": str(e)}
requirements.txt CHANGED
@@ -1,7 +1,13 @@
1
- fastapi==0.109.2
2
- uvicorn[standard]==0.23.2
3
- numpy==1.26.4
4
- pandas==2.1.1
5
- pyarrow==12.0.1
6
- huggingface_hub==0.35.3
7
- duckdb==1.4.0
 
 
 
 
 
 
 
1
+ fastapi==0.109.2
2
+ uvicorn[standard]==0.23.2
3
+ numpy==1.26.4
4
+ pandas==2.1.1
5
+ pyarrow==12.0.1
6
+ huggingface_hub==0.35.3
7
+ duckdb==1.4.0
8
+ faiss-cpu==1.12.0
9
+ langchain==0.3.27
10
+ torch
11
+ sentence-transformers
12
+ pyarrow==22.0.0
13
+ python-dotenv
script/1_create_dataset.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##############################################################################################
2
+ ### Script de création de la base de données articles à partir du fichier parquet,
3
+ ### correspondant au jeu d'essai : https://www.kaggle.com/code/fabiochiusano/medium-articles-simple-data-analysis
4
+ ### Téléchargement du csv puis conversion en Parquet avec compression snappy :
5
+ ### df = pd.read_csv("medium_articles.csv")
6
+ ### df.to_parquet("medium_articles.parquet", engine="pyarrow", compression="snappy")
7
+ ###
8
+ ### Le fichier a été uploadé dans un dataset HF : Loren/articles_db
9
+ ###
10
+ ### Ce script
11
+ ### - crée une base SQLite articles.db constituée des 3 tables : tags, articles, et tag_article
12
+ ### - l'upload dans le dataset HF Loren/articles_db
13
+ ### - crée les fichiers Parquet compressés à partir des tables SQLite
14
+ ### - l'upload dans le dataset HF Loren/articles_database
15
+ ###
16
+ ### 👉 Ils peuvent alors être utilisés par un space Hugging Face
17
+ ##############################################################################################
18
+
19
+ import sqlite3
20
+ import pandas as pd
21
+ import os
22
+ from dotenv import load_dotenv
23
+ import itertools
24
+ import ast
25
+ import uuid
26
+ from huggingface_hub import hf_hub_download, upload_file
27
+ from pathlib import Path
28
+ from collections import Counter
29
+
30
+ # Initialisations
31
+ print("Initialisations ...")
32
+ load_dotenv()
33
+ HF_TOKEN = os.getenv('API_HF_TOKEN')
34
+
35
+ # Constantes
36
+ MIN_COUNT = 5 # nombre minimum d'occurrences pour qu'un tag soit conservé
37
+ DATA_DIR = Path("../../Data") # dossier parent du script
38
+ REPO_ID_DB = "Loren/articles_db" # dataset HF
39
+ REPO_ID = "Loren/articles_database" # dataset HF
40
+ DB_NAME = 'articles.db'
41
+ SQLITE_FILE = DATA_DIR / DB_NAME
42
+ LIST_TABLES = ["articles", "tags", "tag_article"]
43
+ PARQUET_DIR = DATA_DIR / "parquet_tables"
44
+
45
+ # Chargement des données
46
+ parquet_path = hf_hub_download(repo_id=REPO_ID_DB,
47
+ filename="medium_articles.parquet",
48
+ repo_type="dataset")
49
+
50
+ # Créer les dossiers s'ils n'existent pas
51
+ DATA_DIR.mkdir(exist_ok=True)
52
+ PARQUET_DIR.mkdir(exist_ok=True)
53
+
54
+ # Chargement des données
55
+ print("Chargement des données ...")
56
+ df = pd.read_parquet(parquet_path)
57
+
58
+ # Initialisations de la base SQLite
59
+ print("Initialisations de la base SQLite ...")
60
+ conn = sqlite3.connect(SQLITE_FILE)
61
+ cur = conn.cursor()
62
+
63
+ # Suppression des anciennes tables
64
+ cur.execute("DROP TABLE IF EXISTS tag_article")
65
+ cur.execute("DROP TABLE IF EXISTS tags")
66
+ cur.execute("DROP TABLE IF EXISTS articles")
67
+
68
+ # Création des tables Articles, Tags, et de la table d'association articles <-> tags
69
+ cur.execute("""
70
+ CREATE TABLE articles (
71
+ article_id TEXT PRIMARY KEY, -- UUID
72
+ article_title TEXT,
73
+ article_text TEXT,
74
+ article_url TEXT,
75
+ article_authors TEXT,
76
+ article_date TEXT -- YYYY-MM-DD
77
+ )""")
78
+
79
+ cur.execute("""
80
+ CREATE TABLE tags (
81
+ tag_id INTEGER PRIMARY KEY AUTOINCREMENT,
82
+ tag_name TEXT UNIQUE
83
+ )""")
84
+
85
+ cur.execute("""
86
+ CREATE TABLE tag_article (
87
+ tag_article_id INTEGER PRIMARY KEY AUTOINCREMENT,
88
+ article_id TEXT,
89
+ tag_id INTEGER,
90
+ FOREIGN KEY(article_id) REFERENCES articles(article_id),
91
+ FOREIGN KEY(tag_id) REFERENCES tags(tag_id)
92
+ )""")
93
+
94
+ # Extraction des tags en une liste
95
+ print("Extraction des tags en une liste ...")
96
+ df['list_tags'] = df['tags'].apply(lambda x: ast.literal_eval(x) if isinstance(x, str) else [])
97
+ # Extraire tous les tags uniques
98
+ all_tags = list(itertools.chain.from_iterable(df['list_tags']))
99
+ # Comptage du nombre d'occurrences de chaque tag
100
+ tag_counts = Counter(all_tags)
101
+ # On ne va conserver que les tags avec au moins 100 occurrences
102
+ list_tags = [tag for tag, count in tag_counts.items() if count >= MIN_COUNT]
103
+
104
+ # Insertion des tags dans la table
105
+ print("Insertion des tags dans la table ...")
106
+ cur.executemany("INSERT INTO tags (tag_name) VALUES (?)", [(tag,) for tag in list_tags])
107
+
108
+ # Récupération des correspondances tag_name -> tag_id
109
+ print("Récupération des correspondances tag_name -> tag_id ...")
110
+ cur.execute("SELECT tag_id, tag_name FROM tags")
111
+ dict_tag_map = {tag_name: tag_id for tag_id, tag_name in cur.fetchall()}
112
+
113
+ # Insertion des articles et table d'association dans les tables
114
+ print("Insertion des articles et table d'association dans les tables ...")
115
+ for _, row in df.iterrows():
116
+ # Détermination de l'id article
117
+ article_id = str(uuid.uuid4())
118
+
119
+ # Extraction de la date du timestamp
120
+ date_value = None
121
+ if pd.notna(row["timestamp"]):
122
+ try:
123
+ date_value = str(pd.to_datetime(row["timestamp"]).date())
124
+ except Exception:
125
+ date_value = None
126
+
127
+ # Insertion dans la table Articles
128
+ cur.execute("""
129
+ INSERT INTO articles (article_id, article_title, article_text, article_url, article_authors, article_date)
130
+ VALUES (?, ?, ?, ?, ?, ?)""",
131
+ (article_id, row["title"], row["text"], row["url"], row["authors"], date_value))
132
+
133
+ # Association aux tags
134
+ for tag_name in row['list_tags']:
135
+ try:
136
+ tag_id = dict_tag_map[tag_name]
137
+ cur.execute("INSERT INTO tag_article (article_id, tag_id) VALUES (?, ?)",
138
+ (article_id, tag_id))
139
+ except:
140
+ pass
141
+
142
+ print("-> ", len(list_tags), " tags")
143
+ cur.execute("SELECT COUNT(*) FROM tag_article")
144
+ nb_lignes = cur.fetchone()[0]
145
+ print("-> ", nb_lignes, " associations articles <-> tags")
146
+ print("-> ", len(df), " articles")
147
+
148
+ # Commit
149
+ print("Commit ...")
150
+ conn.commit()
151
+
152
+ # Upload dans le dataset hugging face
153
+ print("Upload base Sqlite dans le dataset hugging face ...")
154
+ upload_file(
155
+ path_or_fileobj=SQLITE_FILE,
156
+ path_in_repo=DB_NAME,
157
+
158
+ repo_id=REPO_ID_DB,
159
+ repo_type="dataset",
160
+ token=HF_TOKEN
161
+ )
162
+
163
+ # Création des fichiers Parquet compressés
164
+ print("Création des fichiers Parquet compressés ...")
165
+ parquet_files = []
166
+ for table in LIST_TABLES:
167
+ df = pd.read_sql_query(f"SELECT * FROM {table}", conn)
168
+ parquet_path = PARQUET_DIR / f"{table}.parquet"
169
+ df.to_parquet(parquet_path, engine="pyarrow", index=False, compression="snappy")
170
+ parquet_files.append(parquet_path)
171
+
172
+ # Upload des fichiers Parquet vers HF
173
+ print("Upload des fichiers Parquet dans le dataset hugging face ...")
174
+ for parquet_file in parquet_files:
175
+ print(f"Uploading {parquet_file.name} ...")
176
+ upload_file(
177
+ path_or_fileobj=parquet_file,
178
+ path_in_repo=parquet_file.name,
179
+ repo_id=REPO_ID,
180
+ repo_type="dataset",
181
+ token=HF_TOKEN
182
+ )
183
+
184
+ print("Upload terminé ✅")
185
+
186
+ conn.close()
187
+ print("Traitement terminé.")
script/2_check_dataset.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##############################################################################################
2
+ ### Script de vérification des url des articles
3
+ ###
4
+ ### Ce script
5
+ ### - charge la table articles depuis le dataset HF Loren/articles_database
6
+ ### - check si les url des articles sont des pages actives (status code 200)
7
+ ### - ajoute cette information dans une colonne article_online
8
+ ### - crée les fichiers Parquet compressés à partir de la table créée
9
+ ### - l'upload dans le dataset HF Loren/articles_database
10
+ ###
11
+ ### 👉 Il peut alors être utilisé par un space Hugging Face
12
+ ##############################################################################################
13
+
14
+ import os
15
+ from dotenv import load_dotenv
16
+ from huggingface_hub import hf_hub_download, upload_file
17
+ from pathlib import Path
18
+ from typing import List, Any, Tuple
19
+ import duckdb
20
+ import asyncio
21
+ import aiohttp
22
+ from tqdm.asyncio import tqdm_asyncio
23
+
24
+ # Fonctions
25
+ async def check_url(session: aiohttp.ClientSession, sem: asyncio.Semaphore, url: str) -> bool:
26
+ """
27
+ Vérifie si une URL est accessible (retourne un code HTTP < 400).
28
+
29
+ Args:
30
+ session (aiohttp.ClientSession): Session HTTP réutilisable pour les requêtes.
31
+ sem (asyncio.Semaphore): Sémaphore pour limiter le nombre de requêtes simultanées.
32
+ url (str): L'URL à vérifier.
33
+
34
+ Returns:
35
+ bool: True si l’URL répond avec un code < 400, sinon False.
36
+ """
37
+ if not url:
38
+ return False
39
+
40
+ async with sem: # limite de concurrence
41
+ try:
42
+ async with session.head(url, allow_redirects=True, timeout=TIMEOUT) as resp:
43
+ return resp.status < 400
44
+ except Exception:
45
+ return False
46
+
47
+
48
+ async def process_batch(batch: List[Tuple[Any, ...]]) -> List[bool]:
49
+ """
50
+ Traite un batch d’URLs avec une limite de requêtes simultanées.
51
+
52
+ Args:
53
+ batch (List[Tuple[Any, ...]]): Liste de tuples représentant les lignes d'articles.
54
+ Chaque tuple doit contenir au moins une colonne d’URL à l’index 3.
55
+
56
+ Returns:
57
+ List[bool]: Liste de statuts (True/False) correspondant à l’accessibilité de chaque URL.
58
+ """
59
+ sem = asyncio.Semaphore(MAX_CONCURRENCY)
60
+ async with aiohttp.ClientSession() as session:
61
+ tasks = [
62
+ check_url(session, sem, row[3]) # row[3] = article_url
63
+ for row in batch
64
+ ]
65
+ return await tqdm_asyncio.gather(*tasks)
66
+
67
+
68
+ async def main() -> None:
69
+ """
70
+ Exécute le traitement complet :
71
+ - Récupère les articles par batch depuis la base.
72
+ - Vérifie la disponibilité des URLs.
73
+ - Insère les résultats enrichis dans une table de sortie.
74
+
75
+ Returns:
76
+ None
77
+ """
78
+ total_rows = con.execute("SELECT COUNT(*) FROM articles").fetchone()[0]
79
+ total_batches = (total_rows + BATCH_SIZE - 1) // BATCH_SIZE
80
+
81
+ print(f"🔍 {total_rows} lignes à traiter ({total_batches} batchs de {BATCH_SIZE})")
82
+
83
+ for batch_index in range(total_batches):
84
+ offset = batch_index * BATCH_SIZE
85
+
86
+ # Charger un batch depuis la table
87
+ batch = con.execute(f"""
88
+ SELECT * FROM articles
89
+ LIMIT {BATCH_SIZE} OFFSET {offset}
90
+ """).fetchall()
91
+
92
+ # Vérifier les URLs
93
+ online_statuses = await process_batch(batch)
94
+
95
+ # Préparer les données enrichies
96
+ enriched_rows = [
97
+ (*row, status)
98
+ for row, status in zip(batch, online_statuses)
99
+ ]
100
+
101
+ # Insérer dans la table physique
102
+ con.executemany(f"""
103
+ INSERT INTO {TABLE_OUTPUT} VALUES (?, ?, ?, ?, ?, ?, ?)
104
+ """, enriched_rows)
105
+
106
+ print(f"✅ Batch {batch_index + 1}/{total_batches} traité ({len(batch)} lignes)")
107
+
108
+ print("🎉 Traitement terminé !")
109
+ print(f"Résultat stocké dans la table '{TABLE_OUTPUT}'")
110
+ #
111
+
112
+ if __name__ == "__main__":
113
+ # Initialisations
114
+ print("Initialisations ...")
115
+ load_dotenv()
116
+ HF_TOKEN = os.getenv('API_HF_TOKEN')
117
+
118
+ # Constantes
119
+ DATA_DIR = Path("../../Data") # dossier parent du script
120
+ REPO_ID = "Loren/articles_database" # dataset HF
121
+ PARQUET_DIR = DATA_DIR / "parquet_tables"
122
+ REPO_ID = "Loren/articles_database"
123
+ CACHE_DIR = "/tmp"
124
+ TABLE_OUTPUT = "articles_checked" # Table de sortie
125
+ BATCH_SIZE = 1000
126
+ MAX_CONCURRENCY = 100
127
+ TIMEOUT = 5 # secondes
128
+ parquet_path = PARQUET_DIR / f"{TABLE_OUTPUT}.parquet"
129
+
130
+ os.makedirs(CACHE_DIR, exist_ok=True)
131
+ # Rediriger le cache HF globalement
132
+ os.environ["HF_HOME"] = CACHE_DIR
133
+ os.environ["HF_DATASETS_CACHE"] = CACHE_DIR
134
+ os.environ["TRANSFORMERS_CACHE"] = CACHE_DIR
135
+
136
+ # Téléchargement des fichiers Parquet depuis Hugging Face
137
+ articles_parquet = hf_hub_download(
138
+ repo_id=REPO_ID,
139
+ filename="articles.parquet",
140
+ repo_type="dataset",
141
+ cache_dir=CACHE_DIR)
142
+
143
+ # Connexion DuckDB en mémoire
144
+ con = duckdb.connect()
145
+
146
+ # Créer des tables DuckDB directement à partir des fichiers Parquet
147
+ con.execute(f"CREATE VIEW articles AS SELECT * FROM parquet_scan('{articles_parquet}')")
148
+
149
+ # Créer la table cible
150
+ con.execute(f"""CREATE TABLE {TABLE_OUTPUT} AS
151
+ SELECT *, NULL::BOOLEAN AS article_online
152
+ FROM articles
153
+ WHERE 1=0""")
154
+
155
+ # Traitement principal
156
+ asyncio.run(main())
157
+
158
+ # Sauvegarde du résultat dans un fichier Parquet
159
+ con.execute(f"""COPY {TABLE_OUTPUT} TO '{parquet_path}'
160
+ (FORMAT PARQUET, , COMPRESSION 'SNAPPY')""")
161
+
162
+ print(f"✅ Fichier Parquet créé : {parquet_path}")
163
+
164
+ # Upload des fichiers Parquet vers HF
165
+ print(f"Uploading {parquet_path} ...")
166
+ upload_file(
167
+ path_or_fileobj=parquet_path,
168
+ path_in_repo=f"{TABLE_OUTPUT}.parquet",
169
+ repo_id=REPO_ID,
170
+ repo_type="dataset",
171
+ token=HF_TOKEN
172
+ )
173
+
174
+ print("✅ Traitement terminé.")
script/3_create_faiss_database.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##############################################################################################
2
+ ### Script de création de la base de données FAISS des articles
3
+ ###
4
+ ### Ce script
5
+ ### - charge la table articles depuis le dataset HF Loren/articles_database
6
+ ### - la traite par batch :
7
+ ### - création de chunks de texte
8
+ ### - création des embeddings avec le modèle SentenceTransformer "intfloat/e5-small"
9
+ ### - ajout des embeddings dans un index FAISS
10
+ ### - sauvegarde des métadonnées des chunks dans un fichier parquet
11
+ ### - sauvegarde de l'index FAISS dans un fichier faiss_index.bin
12
+ ### - upload dans le dataset HF Loren/articles_faiss
13
+ ###
14
+ ### 👉 L'index Faiss peut alors être utilisé par un space Hugging Face
15
+ ##############################################################################################
16
+
17
+ import os
18
+ import torch
19
+ import duckdb
20
+ from huggingface_hub import hf_hub_download, upload_file
21
+ from huggingface_hub import HfApi, HfFolder, CommitOperationAdd
22
+ import faiss
23
+ from sentence_transformers import SentenceTransformer
24
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
25
+ from functools import partial
26
+ import pyarrow as pa
27
+ import pyarrow.parquet as pq
28
+ from pathlib import Path
29
+ from dotenv import load_dotenv
30
+
31
+ # Fonctions
32
+ # Batch processing function
33
+ def batch_process(list_articles: list, faiss_id_start: int) -> int:
34
+ """
35
+ Traite un batch d'articles pour générer des embeddings et des métadonnées,
36
+ puis les sauvegarde de manière sécurisée pour garantir la persistance en cas de problème.
37
+
38
+ Étapes réalisées :
39
+ 1. Découpage de chaque article en chunks via le splitter.
40
+ 2. Création d'un dictionnaire de métadonnées pour chaque chunk contenant :
41
+ - faiss_id : identifiant unique aligné avec l'index FAISS
42
+ - document_id : identifiant de l'article
43
+ - chunk_text : texte du chunk
44
+ 3. Calcul des embeddings pour tous les chunks du batch.
45
+ 4. Ajout des embeddings au FAISS index existant (append).
46
+ 5. Écriture immédiate de l'index FAISS sur disque pour assurer la persistance.
47
+ 6. Sauvegarde des métadonnées batch dans un fichier Parquet distinct.
48
+
49
+ Args:
50
+ list_articles (list): Liste de tuples (document_id, document_text) représentant les articles du batch.
51
+ faiss_id_start (int): Identifiant de départ pour le premier chunk du batch,
52
+ utilisé pour aligner FAISS et les métadonnées.
53
+
54
+ Returns:
55
+ int: Identifiant FAISS suivant, à utiliser pour le batch suivant afin de maintenir l'alignement.
56
+
57
+ Notes :
58
+ - Cette fonction est conçue pour être utilisée batch par batch.
59
+ - Les fichiers Parquet et le fichier FAISS sont mis à jour à chaque batch pour éviter toute perte de données.
60
+ """
61
+ global faiss_index
62
+
63
+ try:
64
+ list_chunks = []
65
+ list_metadata = []
66
+
67
+ for doc_id, doc_content in list_articles:
68
+ chunks = splitter.split_text(doc_content)
69
+ for chunk_text in chunks:
70
+ list_chunks.append(chunk_text)
71
+ list_metadata.append({
72
+ "faiss_id": faiss_id_start,
73
+ "document_id": doc_id,
74
+ "chunk_text": chunk_text
75
+ })
76
+ faiss_id_start += 1
77
+
78
+ # Embeddings
79
+ if list_chunks:
80
+ passage_texts = [f"passage: {p}" for p in list_chunks]
81
+ embeddings = model.encode(passage_texts, convert_to_numpy=True,
82
+ normalize_embeddings=True)
83
+ faiss_index.add(embeddings)
84
+ faiss.write_index(faiss_index, str(FAISS_INDEX_FILE))
85
+
86
+ # Sauvegarde batch métadonnées en Parquet
87
+ if list_metadata:
88
+ table = pa.Table.from_pylist(list_metadata)
89
+ batch_file = PARQUET_DIR / f"metadata_batch_{faiss_id_start}.parquet"
90
+ pq.write_table(table, batch_file)
91
+
92
+ return faiss_id_start
93
+ except Exception as e:
94
+ print(f"ERROR in batch_process function : {e}")
95
+ return None
96
+ ##
97
+
98
+ # Initialisations
99
+ global faiss_index
100
+ print("Initialisations ...")
101
+ load_dotenv()
102
+ HF_TOKEN = os.getenv('API_HF_TOKEN')
103
+ REPO_ID = "Loren/articles_database"
104
+ DATA_DIR = Path("../../Data") # dossier parent du script
105
+ CHUNK_SIZE = 250
106
+ CHUNK_OVERLAP = 50
107
+ BATCH_SIZE = 1000
108
+ MODEL_NAME = "intfloat/multilingual-e5-small"
109
+ FAISS_INDEX_FILE = DATA_DIR / "faiss_index.bin"
110
+ PARQUET_DIR = DATA_DIR / "parquet_metadata"
111
+
112
+ CACHE_DIR = "/tmp"
113
+ os.makedirs(CACHE_DIR, exist_ok=True)
114
+ # Rediriger le cache HF globalement
115
+ os.environ["HF_HOME"] = CACHE_DIR
116
+ os.environ["HF_DATASETS_CACHE"] = CACHE_DIR
117
+ os.environ["TRANSFORMERS_CACHE"] = CACHE_DIR
118
+
119
+ # Téléchargement des fichiers Parquet depuis Hugging Face
120
+ print("Téléchargement des fichiers Parquet depuis Hugging Face ...")
121
+ articles_parquet = hf_hub_download(
122
+ repo_id=REPO_ID,
123
+ filename="articles.parquet",
124
+ repo_type="dataset",
125
+ cache_dir=CACHE_DIR)
126
+
127
+ # Connexion DuckDB en mémoire
128
+ con = duckdb.connect()
129
+
130
+ # Créer des tables DuckDB directement à partir des fichiers Parquet
131
+ print("Création des vues DuckDB à partir des fichiers Parquet ...")
132
+ con.execute(f"CREATE VIEW articles AS SELECT * FROM parquet_scan('{articles_parquet}')")
133
+
134
+ # Creating the plitter for chunking document
135
+ print("Initialisation du text splitter ...")
136
+ splitter = RecursiveCharacterTextSplitter(
137
+ chunk_size=CHUNK_SIZE,
138
+ chunk_overlap=CHUNK_OVERLAP,
139
+ keep_separator='end',
140
+ separators=["\n\n", "\n", "."]
141
+ )
142
+
143
+ # Creating the Sentence transformer model
144
+ print("Initialisation du modèle de Sentence Transformer ...")
145
+ device = "cuda" if torch.cuda.is_available() else "cpu"
146
+ print(f"*** Device: {device}")
147
+ model = SentenceTransformer(MODEL_NAME, device=device)
148
+
149
+ # Creating the Faiss index
150
+ embedding_dim = model.get_sentence_embedding_dimension()
151
+ faiss_index = faiss.IndexFlatIP(embedding_dim)
152
+ faiss_id_counter = 0 # compteur global pour lier faiss_id et métadonnées
153
+
154
+ # Traitement par batchs
155
+ print("Création des batches et traitement ...")
156
+ cursor = con.execute("""
157
+ SELECT article_id, article_text
158
+ FROM articles
159
+ WHERE (LENGTH(article_text) - LENGTH(REPLACE(article_text, ' ', '')) + 1) >= 100""")
160
+
161
+ # Création d'un itérateur de batches
162
+ fetch_batch = partial(cursor.fetchmany, BATCH_SIZE)
163
+
164
+ for batch_num, batch in enumerate(iter(fetch_batch, []), start=1):
165
+ print("Traitement batch no ", batch_num, " ...")
166
+ faiss_id_counter = batch_process(batch, faiss_id_counter)
167
+ if not faiss_id_counter:
168
+ print("*** Erreur traitement batch no ", batch_num)
169
+
170
+ print("\n✅ Traitement terminé")
171
+
172
+ # Upload des fichiers vers HF
173
+ # Création du dataset HF
174
+ REPO_ID = "Loren/articles_faiss"
175
+ api = HfApi()
176
+ HfFolder.save_token(HF_TOKEN)
177
+
178
+ # Vérifier si le dataset existe
179
+ try:
180
+ repo_info = api.dataset_info(REPO_ID, token=HF_TOKEN)
181
+ print(f"Dataset {REPO_ID} existe déjà, suppression en cours...")
182
+ api.delete_repo(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
183
+ except Exception as e:
184
+ print(f"Dataset n'existait pas : {e}")
185
+
186
+ # Créer le dataset (privé)
187
+ api.create_repo(repo_id=REPO_ID, repo_type="dataset", exist_ok=True, private=True, token=HF_TOKEN)
188
+ print(f"Dataset {REPO_ID} créé avec succès.")
189
+
190
+ # Récupérer la liste de fichiers parquet
191
+ print("Upload des fichiers metadatas dans le dataset hugging face ", REPO_ID, " ...")
192
+ parquet_files = [
193
+ os.path.join(PARQUET_DIR, f)
194
+ for f in os.listdir(PARQUET_DIR)
195
+ if f.endswith(".parquet")
196
+ ]
197
+
198
+ # Ajouter tous les fichiers
199
+ operations = [
200
+ CommitOperationAdd(
201
+ path_in_repo=f"data/{os.path.basename(f)}",
202
+ path_or_fileobj=f
203
+ )
204
+ for f in parquet_files
205
+ ]
206
+
207
+ api.create_commit(
208
+ repo_id=REPO_ID,
209
+ repo_type="dataset",
210
+ operations=operations,
211
+ commit_message="Upload batch metadata parquet files"
212
+ )
213
+
214
+ print("✅ Upload metadatas terminé !")
215
+
216
+
217
+ print("Upload de l'index Faiss dans le dataset hugging face ", REPO_ID, " ...")
218
+ upload_file(
219
+ path_or_fileobj=FAISS_INDEX_FILE,
220
+ path_in_repo=FAISS_INDEX_FILE.name,
221
+ repo_id=REPO_ID,
222
+ repo_type="dataset",
223
+ token=HF_TOKEN
224
+ )
225
+
226
+ print("✅ Upload faiss index terminé")
227
+
228
+ con.close()
229
+ print("✅ Traitement terminé")
script/rep.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##############################################################################################
2
+ ### Script de création de la base de données FAISS des articles
3
+ ###
4
+ ### Ce script
5
+ ### - charge la table articles depuis le dataset HF Loren/articles_database
6
+ ### - la traite par batch :
7
+ ### - création de chunks de texte
8
+ ### - création des embeddings avec le modèle SentenceTransformer "intfloat/e5-small"
9
+ ### - ajout des embeddings dans un index FAISS
10
+ ### - sauvegarde des métadonnées des chunks dans un fichier parquet
11
+ ### - sauvegarde de l'index FAISS dans un fichier faiss_index.bin
12
+ ### - upload dans le dataset HF Loren/articles_faiss
13
+ ###
14
+ ### 👉 L'index Faiss peut alors être utilisé par un space Hugging Face
15
+ ##############################################################################################
16
+
17
+ import os
18
+ #import torch
19
+ import duckdb
20
+ from huggingface_hub import hf_hub_download, upload_file
21
+ from huggingface_hub import HfApi, HfFolder, CommitOperationAdd
22
+ #import faiss
23
+ #from sentence_transformers import SentenceTransformer
24
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
25
+ from functools import partial
26
+ import pyarrow as pa
27
+ import pyarrow.parquet as pq
28
+ from pathlib import Path
29
+ from dotenv import load_dotenv
30
+
31
+ # Fonctions
32
+ # Batch processing function
33
+ def batch_process(list_articles: list, faiss_id_start: int) -> int:
34
+ """
35
+ Traite un batch d'articles pour générer des embeddings et des métadonnées,
36
+ puis les sauvegarde de manière sécurisée pour garantir la persistance en cas de problème.
37
+
38
+ Étapes réalisées :
39
+ 1. Découpage de chaque article en chunks via le splitter.
40
+ 2. Création d'un dictionnaire de métadonnées pour chaque chunk contenant :
41
+ - faiss_id : identifiant unique aligné avec l'index FAISS
42
+ - document_id : identifiant de l'article
43
+ - chunk_text : texte du chunk
44
+ 3. Calcul des embeddings pour tous les chunks du batch.
45
+ 4. Ajout des embeddings au FAISS index existant (append).
46
+ 5. Écriture immédiate de l'index FAISS sur disque pour assurer la persistance.
47
+ 6. Sauvegarde des métadonnées batch dans un fichier Parquet distinct.
48
+
49
+ Args:
50
+ list_articles (list): Liste de tuples (document_id, document_text) représentant les articles du batch.
51
+ faiss_id_start (int): Identifiant de départ pour le premier chunk du batch,
52
+ utilisé pour aligner FAISS et les métadonnées.
53
+
54
+ Returns:
55
+ int: Identifiant FAISS suivant, à utiliser pour le batch suivant afin de maintenir l'alignement.
56
+
57
+ Notes :
58
+ - Cette fonction est conçue pour être utilisée batch par batch.
59
+ - Les fichiers Parquet et le fichier FAISS sont mis à jour à chaque batch pour éviter toute perte de données.
60
+ """
61
+ global faiss_index
62
+
63
+ try:
64
+ list_chunks = []
65
+ list_metadata = []
66
+
67
+ for doc_id, doc_content in list_articles:
68
+ chunks = splitter.split_text(doc_content)
69
+ for chunk_text in chunks:
70
+ list_chunks.append(chunk_text)
71
+ list_metadata.append({
72
+ "faiss_id": faiss_id_start,
73
+ "document_id": doc_id,
74
+ "chunk_text": chunk_text
75
+ })
76
+ faiss_id_start += 1
77
+
78
+ # Embeddings
79
+ #if list_chunks:
80
+ # embeddings = model.encode(list_chunks, convert_to_numpy=True, normalize_embeddings=True)
81
+ # faiss_index.add(embeddings)
82
+ # faiss.write_index(faiss_index, FAISS_INDEX_FILE)
83
+
84
+ # Sauvegarde batch métadonnées en Parquet
85
+ if list_metadata:
86
+ table = pa.Table.from_pylist(list_metadata)
87
+ batch_file = PARQUET_DIR / f"metadata_batch_{faiss_id_start}.parquet"
88
+ pq.write_table(table, batch_file)
89
+
90
+ return faiss_id_start
91
+ except Exception as e:
92
+ print(f"ERROR in batch_process function : {e}")
93
+ return None
94
+ ##
95
+
96
+ # Initialisations
97
+ global faiss_index
98
+ print("Initialisations ...")
99
+ load_dotenv()
100
+ HF_TOKEN = os.getenv('API_HF_TOKEN')
101
+ REPO_ID = "Loren/articles_database"
102
+ DATA_DIR = Path("../../Data") # dossier parent du script
103
+ CHUNK_SIZE = 250
104
+ CHUNK_OVERLAP = 50
105
+ BATCH_SIZE = 1000
106
+ MODEL_NAME = "intfloat/e5-small"
107
+ FAISS_INDEX_FILE = DATA_DIR / "faiss_index.bin"
108
+ PARQUET_DIR = DATA_DIR / "parquet_metadata"
109
+
110
+ CACHE_DIR = "/tmp"
111
+ os.makedirs(CACHE_DIR, exist_ok=True)
112
+ # Rediriger le cache HF globalement
113
+ os.environ["HF_HOME"] = CACHE_DIR
114
+ os.environ["HF_DATASETS_CACHE"] = CACHE_DIR
115
+ os.environ["TRANSFORMERS_CACHE"] = CACHE_DIR
116
+
117
+ # Téléchargement des fichiers Parquet depuis Hugging Face
118
+ print("Téléchargement des fichiers Parquet depuis Hugging Face ...")
119
+ articles_parquet = hf_hub_download(
120
+ repo_id=REPO_ID,
121
+ filename="articles.parquet",
122
+ repo_type="dataset",
123
+ cache_dir=CACHE_DIR)
124
+
125
+ # Connexion DuckDB en mémoire
126
+ con = duckdb.connect()
127
+
128
+ # Créer des tables DuckDB directement à partir des fichiers Parquet
129
+ print("Création des vues DuckDB à partir des fichiers Parquet ...")
130
+ con.execute(f"CREATE VIEW articles AS SELECT * FROM parquet_scan('{articles_parquet}')")
131
+
132
+ # Creating the plitter for chunking document
133
+ splitter = RecursiveCharacterTextSplitter(
134
+ chunk_size=CHUNK_SIZE,
135
+ chunk_overlap=CHUNK_OVERLAP,
136
+ keep_separator='end',
137
+ separators=["\n\n", "\n", "."]
138
+ )
139
+
140
+ # Creating the Sentence transformer model
141
+ #device = "cuda" if torch.cuda.is_available() else "cpu"
142
+ #print(f"*** Device: {device}")
143
+ #model = SentenceTransformer(MODEL_NAME, device=device)
144
+ #
145
+ ## Creating the Faiss index
146
+ #embedding_dim = model.get_sentence_embedding_dimension()
147
+ #faiss_index = faiss.IndexFlatIP(embedding_dim)
148
+ faiss_id_counter = 0 # compteur global pour lier faiss_id et métadonnées
149
+
150
+ # Traitement par batchs
151
+ print("Création des batches et traitement ...")
152
+ cursor = con.execute("""
153
+ SELECT article_id, article_text
154
+ FROM articles
155
+ WHERE (LENGTH(article_text) - LENGTH(REPLACE(article_text, ' ', '')) + 1) >= 100""")
156
+
157
+ # Création d'un itérateur de batches
158
+ fetch_batch = partial(cursor.fetchmany, BATCH_SIZE)
159
+
160
+ for batch_num, batch in enumerate(iter(fetch_batch, []), start=1):
161
+ print("Traitement batch no ", batch_num, " ...")
162
+ faiss_id_counter = batch_process(batch, faiss_id_counter)
163
+ if not faiss_id_counter:
164
+ print("*** Erreur traitement batch no ", batch_num)
165
+
166
+ print("\n✅ Traitement terminé")
167
+
168
+ # Upload des fichiers vers HF
169
+ # Création du dataset HF
170
+ REPO_ID = "Loren/articles_faiss"
171
+ api = HfApi()
172
+ HfFolder.save_token(HF_TOKEN)
173
+
174
+ # Créer repo si besoin
175
+ api.create_repo(repo_id=REPO_ID, repo_type="dataset", exist_ok=True, private=True)
176
+
177
+ # Récupérer la liste de fichiers parquet
178
+ print("Upload des fichiers metadatas dans le dataset hugging face ", REPO_ID, " ...")
179
+ parquet_files = [
180
+ os.path.join(PARQUET_DIR, f)
181
+ for f in os.listdir(PARQUET_DIR)
182
+ if f.endswith(".parquet")
183
+ ]
184
+
185
+ # Ajouter tous les fichiers
186
+ operations = [
187
+ CommitOperationAdd(
188
+ path_in_repo=f"data/{os.path.basename(f)}",
189
+ path_or_fileobj=f
190
+ )
191
+ for f in parquet_files
192
+ ]
193
+
194
+ api.create_commit(
195
+ repo_id=REPO_ID,
196
+ repo_type="dataset",
197
+ operations=operations,
198
+ commit_message="Upload batch metadata parquet files"
199
+ )
200
+
201
+ print("✅ Upload metadatas terminé !")
202
+
203
+
204
+ print("Upload de l'index Faiss dans le dataset hugging face ", REPO_ID, " ...")
205
+ upload_file(
206
+ path_or_fileobj=FAISS_INDEX_FILE,
207
+ path_in_repo=FAISS_INDEX_FILE.name,
208
+ repo_id=REPO_ID,
209
+ repo_type="dataset",
210
+ token=HF_TOKEN
211
+ )
212
+
213
+ print("✅ Upload faiss index terminé")
214
+
215
+ con.close()
216
+ print("✅ Traitement terminé")