Update app.py
Browse files
app.py
CHANGED
|
@@ -8,8 +8,13 @@ import faiss
|
|
| 8 |
from usearch.index import Index
|
| 9 |
|
| 10 |
# Load titles and texts
|
| 11 |
-
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
# Load the int8 and binary indices. Int8 is loaded as a view to save memory, as we never actually perform search with it.
|
| 14 |
int8_view = Index.restore("wikipedia_fr_2022_250K_int8_usearch.index", view=True)
|
| 15 |
binary_index: faiss.IndexBinaryFlat = faiss.read_index_binary("wikipedia_fr_2022_250K_ubinary_faiss.index")
|
|
@@ -52,8 +57,15 @@ def search(query, top_k: int = 20, rescore_multiplier: int = 1, use_approx: bool
|
|
| 52 |
indices = scores.argsort()[::-1][:top_k]
|
| 53 |
top_k_indices = binary_ids[indices]
|
| 54 |
top_k_scores = scores[indices]
|
| 55 |
-
top_k_titles, top_k_texts = zip(*[(
|
| 56 |
-
df = pd.DataFrame({"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
sort_time = time.time() - start_time
|
| 58 |
|
| 59 |
return df, {
|
|
@@ -67,24 +79,34 @@ def search(query, top_k: int = 20, rescore_multiplier: int = 1, use_approx: bool
|
|
| 67 |
}
|
| 68 |
|
| 69 |
|
| 70 |
-
with gr.Blocks(title="Requêter Wikipedia en temps réel") as demo:
|
| 71 |
gr.Markdown(
|
| 72 |
"""
|
| 73 |
-
## Requêter Wikipedia en temps réel
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
Les résultats sont renvoyés en temps réel via
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
-
Détails
|
| 82 |
-
1. La requête est enchâssée en float32 à l'aide du modèle [`OrdalieTech/Solon-embeddings-large-0.1`](https://hf.co/OrdalieTech/Solon-embeddings-large-0.1).
|
| 83 |
2. La requête est quantizée en binaire à l'aide de la fonction `quantize_embeddings` de la bibliothèque [SentenceTransformers](https://sbert.net/).
|
| 84 |
-
3. Un index binaire (
|
| 85 |
-
4. Les *n* textes demandés par l'utilisateur jugés les plus pertinents sont chargés à la volée à partir d'un index int8 sur disque (
|
| 86 |
5. Les *n* textes sont rescorés en utilisant la requête en float32 et les enchâssements en int8.
|
| 87 |
-
6. Les *n* premiers textes sont triés par score et affichés.
|
| 88 |
|
| 89 |
Ce processus est conçu pour être rapide et efficace en termes de mémoire : l'index binaire étant suffisamment petit pour tenir dans la mémoire et l'index int8 étant chargé en tant que vue pour économiser de la mémoire.
|
| 90 |
Au total, ce processus nécessite de conserver 1) le modèle en mémoire, 2) l'index binaire en mémoire et 3) l'index int8 sur le disque.
|
|
@@ -93,30 +115,29 @@ Avec une dimension de 1024, nous avons besoin de `1024 / 8 * num_docs` octets po
|
|
| 93 |
C'est nettement moins cher que de faire le même processus avec des enchâssements en float32 qui nécessiterait `4 * 1024 * num_docs` octets de mémoire/espace disque pour l'index float32, soit 32x plus de mémoire et 4x plus d'espace disque.
|
| 94 |
De plus, l'index binaire est beaucoup plus rapide (jusqu'à 32x) à rechercher que l'index float32, tandis que le rescorage est également extrêmement efficace.
|
| 95 |
En conclusion, ce processus permet une recherche rapide, évolutive, peu coûteuse et efficace en termes de mémoire.
|
| 96 |
-
|
| 97 |
</details>
|
| 98 |
"""
|
| 99 |
)
|
| 100 |
with gr.Row():
|
| 101 |
with gr.Column(scale=75):
|
| 102 |
query = gr.Textbox(
|
| 103 |
-
label="
|
| 104 |
placeholder="Saisissez une requête pour rechercher des textes pertinents dans Wikipédia.",
|
| 105 |
)
|
| 106 |
with gr.Column(scale=25):
|
| 107 |
use_approx = gr.Radio(
|
| 108 |
-
choices=[("
|
| 109 |
value=True,
|
| 110 |
-
label="
|
| 111 |
)
|
| 112 |
|
| 113 |
with gr.Row():
|
| 114 |
with gr.Column(scale=2):
|
| 115 |
top_k = gr.Slider(
|
| 116 |
-
minimum=
|
| 117 |
-
maximum=
|
| 118 |
-
step=
|
| 119 |
-
value=
|
| 120 |
label="Nombre de documents à rechercher",
|
| 121 |
info="Recherche effectué via un bi-encodeur binaire",
|
| 122 |
)
|
|
@@ -127,16 +148,13 @@ En conclusion, ce processus permet une recherche rapide, évolutive, peu coûteu
|
|
| 127 |
step=1,
|
| 128 |
value=1,
|
| 129 |
label="Coefficient de rescorage",
|
| 130 |
-
info="Reranking via le coefficient
|
| 131 |
)
|
| 132 |
|
| 133 |
search_button = gr.Button(value="Search")
|
| 134 |
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
output = gr.Dataframe(headers=["Score", "Titre", "Texte"])
|
| 138 |
-
with gr.Column(scale=1):
|
| 139 |
-
json = gr.JSON()
|
| 140 |
|
| 141 |
query.submit(search, inputs=[query, top_k, rescore_multiplier, use_approx], outputs=[output, json])
|
| 142 |
search_button.click(search, inputs=[query, top_k, rescore_multiplier, use_approx], outputs=[output, json])
|
|
|
|
| 8 |
from usearch.index import Index
|
| 9 |
|
| 10 |
# Load titles and texts
|
| 11 |
+
wikipedia_dataset = load_dataset("bourdoiscatie/wikipedia_fr_2022_250K", split="train", num_proc=4).select_columns(["title", "text", "wiki_id"])
|
| 12 |
|
| 13 |
+
def add_link(example):
|
| 14 |
+
example["title"] = '['+example["title"]+']('+'https://fr.wikipedia.org/wiki?curid='+str(example["wiki_id"])+')'
|
| 15 |
+
return example
|
| 16 |
+
wikipedia_dataset = wikipedia_dataset.map(add_link)
|
| 17 |
+
|
| 18 |
# Load the int8 and binary indices. Int8 is loaded as a view to save memory, as we never actually perform search with it.
|
| 19 |
int8_view = Index.restore("wikipedia_fr_2022_250K_int8_usearch.index", view=True)
|
| 20 |
binary_index: faiss.IndexBinaryFlat = faiss.read_index_binary("wikipedia_fr_2022_250K_ubinary_faiss.index")
|
|
|
|
| 57 |
indices = scores.argsort()[::-1][:top_k]
|
| 58 |
top_k_indices = binary_ids[indices]
|
| 59 |
top_k_scores = scores[indices]
|
| 60 |
+
top_k_titles, top_k_texts = zip(*[(wikipedia_dataset[idx]["title"], wikipedia_dataset[idx]["text"]) for idx in top_k_indices.tolist()])
|
| 61 |
+
df = pd.DataFrame({"Score_ind": [round(value, 2) for value in top_k_scores], "Titre": top_k_titles, "Texte": top_k_texts})
|
| 62 |
+
score_sum = df.groupby('Titre')['Score_ind'].sum().reset_index()
|
| 63 |
+
df = pd.merge(df, score_sum, on='Titre', how='left')
|
| 64 |
+
df.rename(columns={'Score_ind_y': 'Score_sum'}, inplace=True)
|
| 65 |
+
df.rename(columns={'Score_ind_x': 'Score_ind'}, inplace=True)
|
| 66 |
+
df = df[["Score_sum", "Score_ind", "Titre", "Texte"]]
|
| 67 |
+
df = df.sort_values('Score_sum', ascending=False)
|
| 68 |
+
# df = df.groupby('Titre')[['Score', 'Texte']].agg({'Score': 'sum', 'Texte': '\n\n'.join}).reset_index().sort_values('Score', ascending=False)
|
| 69 |
sort_time = time.time() - start_time
|
| 70 |
|
| 71 |
return df, {
|
|
|
|
| 79 |
}
|
| 80 |
|
| 81 |
|
| 82 |
+
with gr.Blocks(title="Requêter Wikipedia en temps réel 🔍") as demo:
|
| 83 |
gr.Markdown(
|
| 84 |
"""
|
| 85 |
+
## Requêter Wikipedia en temps réel 🔍
|
| 86 |
+
|
| 87 |
+
Ce démonstrateur permet de requêter un corpus composé des 250K paragraphes les plus consultés du Wikipédia francophone.
|
| 88 |
+
Les résultats sont renvoyés en temps réel via un pipeline tournant sur un CPU 🚀
|
| 89 |
+
Nous nous sommes grandement inspirés du Space [quantized-retrieval](https://huggingface.co/spaces/sentence-transformers/quantized-retrieval) conçu par [Tom Aarsen](https://huggingface.co/tomaarsen) 🤗.
|
| 90 |
+
Si vous voulez en savoir plus sur le processus complet derrière ce démonstrateur, n'hésitez pas à déplier les liens ci-dessous.
|
| 91 |
+
|
| 92 |
+
<details><summary>1. Détails sur les données</summary>
|
| 93 |
+
Le corpus utilisé correspond au 250 000 premières lignes du jeu de données [wikipedia-22-12-fr-embeddings](https://huggingface.co/datasets/Cohere/wikipedia-22-12-fr-embeddings) mis en ligne par Cohere.
|
| 94 |
+
Comme son nom l'indique il s'agit d'un jeu de données datant de décembre 2022. Cette information est à prendre en compte lorsque vous effectuez votre requête.
|
| 95 |
+
De même il s'agit ici d'un sous-ensemble du jeu de données total, à savoir les 250 000 paragraphes les plus consultés à cette date-là.
|
| 96 |
+
Ainsi, si vous effectuez une recherche pointue sur un sujet peu consulté, ce démonstrateur ne reverra probablement rien de pertinent.
|
| 97 |
+
A noter également que Cohere a effectué un prétraitement sur les données ce qui a conduit à la suppression de dates par exemple.
|
| 98 |
+
Ce jeu de données n'est donc pas optimal. L'idée était de pouvoir proposer quelque chose en peu de temps.
|
| 99 |
+
Dans un deuxième temps, ce démonstrateur sera étendu à l'ensemble du jeu de données *wikipedia-22-12-fr-embeddings* (soit 13M de paragraphes).
|
| 100 |
+
Il n'est pas exclus d'ensuite utiliser une version plus récente de Wikipedia (on peut penser par exemple à [wikimedia/wikipedia](https://huggingface.co/datasets/wikimedia/wikipedia) de Wikimedia) mais qui demandera d'effectuer plusieurs nettoyages.
|
| 101 |
+
</details>
|
| 102 |
|
| 103 |
+
<details><summary>2. Détails le pipeline</summary>
|
| 104 |
+
1. La requête est enchâssée en float32 à l'aide du modèle [`OrdalieTech/Solon-embeddings-large-0.1`](https://hf.co/OrdalieTech/Solon-embeddings-large-0.1) d'Ordalie.
|
| 105 |
2. La requête est quantizée en binaire à l'aide de la fonction `quantize_embeddings` de la bibliothèque [SentenceTransformers](https://sbert.net/).
|
| 106 |
+
3. Un index binaire (250K *embeddings* binaires pesant 32MB de mémoire/espace disque) est requêté (en binaire si l'option approximative est sélectionnée, en int8 si l'option exacte est sélectionnée).
|
| 107 |
+
4. Les *n* textes demandés par l'utilisateur jugés les plus pertinents sont chargés à la volée à partir d'un index int8 sur disque (250K *embeddings* int8 ; 0 bytes de mémoire, 293MB d'espace disque).
|
| 108 |
5. Les *n* textes sont rescorés en utilisant la requête en float32 et les enchâssements en int8.
|
| 109 |
+
6. Les *n* premiers textes sont triés par score et affichés. Le "Score_ind" correspond au score individuel de chaque paragraphe d'être pertinant vis-à-vis de la requête. Le "Score_sum" correspond à la somme de tous les scores individuels des paragraphes issus d'un même article Wikipedia. L'objectif est alors de mettre en avant l'article source plutôt qu'un bout de texte le composant.
|
| 110 |
|
| 111 |
Ce processus est conçu pour être rapide et efficace en termes de mémoire : l'index binaire étant suffisamment petit pour tenir dans la mémoire et l'index int8 étant chargé en tant que vue pour économiser de la mémoire.
|
| 112 |
Au total, ce processus nécessite de conserver 1) le modèle en mémoire, 2) l'index binaire en mémoire et 3) l'index int8 sur le disque.
|
|
|
|
| 115 |
C'est nettement moins cher que de faire le même processus avec des enchâssements en float32 qui nécessiterait `4 * 1024 * num_docs` octets de mémoire/espace disque pour l'index float32, soit 32x plus de mémoire et 4x plus d'espace disque.
|
| 116 |
De plus, l'index binaire est beaucoup plus rapide (jusqu'à 32x) à rechercher que l'index float32, tandis que le rescorage est également extrêmement efficace.
|
| 117 |
En conclusion, ce processus permet une recherche rapide, évolutive, peu coûteuse et efficace en termes de mémoire.
|
|
|
|
| 118 |
</details>
|
| 119 |
"""
|
| 120 |
)
|
| 121 |
with gr.Row():
|
| 122 |
with gr.Column(scale=75):
|
| 123 |
query = gr.Textbox(
|
| 124 |
+
label="Requêter le Wikipédia francophone",
|
| 125 |
placeholder="Saisissez une requête pour rechercher des textes pertinents dans Wikipédia.",
|
| 126 |
)
|
| 127 |
with gr.Column(scale=25):
|
| 128 |
use_approx = gr.Radio(
|
| 129 |
+
choices=[("Exacte", False), ("Approximative", True)],
|
| 130 |
value=True,
|
| 131 |
+
label="Type de recherche",
|
| 132 |
)
|
| 133 |
|
| 134 |
with gr.Row():
|
| 135 |
with gr.Column(scale=2):
|
| 136 |
top_k = gr.Slider(
|
| 137 |
+
minimum=3,
|
| 138 |
+
maximum=40,
|
| 139 |
+
step=1,
|
| 140 |
+
value=15,
|
| 141 |
label="Nombre de documents à rechercher",
|
| 142 |
info="Recherche effectué via un bi-encodeur binaire",
|
| 143 |
)
|
|
|
|
| 148 |
step=1,
|
| 149 |
value=1,
|
| 150 |
label="Coefficient de rescorage",
|
| 151 |
+
info="Reranking via le coefficient",
|
| 152 |
)
|
| 153 |
|
| 154 |
search_button = gr.Button(value="Search")
|
| 155 |
|
| 156 |
+
output = gr.Dataframe(headers=["Score_sum", "Score_ind", "Titre", "Texte"], datatype="markdown")
|
| 157 |
+
json = gr.JSON()
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
query.submit(search, inputs=[query, top_k, rescore_multiplier, use_approx], outputs=[output, json])
|
| 160 |
search_button.click(search, inputs=[query, top_k, rescore_multiplier, use_approx], outputs=[output, json])
|