flashrag-dev 0.1.4.dev20250715__tar.gz → 0.1.4.dev20250720__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/PKG-INFO +27 -1
  2. flashrag_dev-0.1.4.dev20250715/flashrag_dev.egg-info/PKG-INFO → flashrag_dev-0.1.4.dev20250720/README.md +26 -17
  3. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/config/basic_config.yaml +4 -1
  4. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/retriever/index_builder.py +296 -27
  5. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/retriever/retriever.py +207 -0
  6. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/utils/utils.py +4 -2
  7. flashrag_dev-0.1.4.dev20250720/flashrag/version.py +1 -0
  8. flashrag_dev-0.1.4.dev20250715/README.md → flashrag_dev-0.1.4.dev20250720/flashrag_dev.egg-info/PKG-INFO +43 -0
  9. flashrag_dev-0.1.4.dev20250715/flashrag/version.py +0 -1
  10. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/LICENSE +0 -0
  11. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/__init__.py +0 -0
  12. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/config/__init__.py +0 -0
  13. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/config/config.py +0 -0
  14. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/dataset/__init__.py +0 -0
  15. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/dataset/dataset.py +0 -0
  16. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/dataset/utils.py +0 -0
  17. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/evaluator/__init__.py +0 -0
  18. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/evaluator/_bleu.py +0 -0
  19. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/evaluator/evaluator.py +0 -0
  20. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/evaluator/metrics.py +0 -0
  21. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/evaluator/utils.py +0 -0
  22. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/generator/__init__.py +0 -0
  23. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/generator/fid.py +0 -0
  24. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/generator/generator.py +0 -0
  25. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/generator/multimodal_generator.py +0 -0
  26. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/generator/openai_generator.py +0 -0
  27. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/generator/stop_word_criteria.py +0 -0
  28. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/generator/utils.py +0 -0
  29. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/judger/__init__.py +0 -0
  30. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/judger/judger.py +0 -0
  31. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/pipeline/__init__.py +0 -0
  32. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/pipeline/active_pipeline.py +0 -0
  33. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/pipeline/branching_pipeline.py +0 -0
  34. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/pipeline/mm_pipeline.py +0 -0
  35. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/pipeline/pipeline.py +0 -0
  36. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/pipeline/reasoning_pipeline.py +0 -0
  37. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/pipeline/replug_utils.py +0 -0
  38. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/prompt/__init__.py +0 -0
  39. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/prompt/base_prompt.py +0 -0
  40. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/prompt/mm_prompt.py +0 -0
  41. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/prompt/selfask_examplars.py +0 -0
  42. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/prompt/trace_examplars.py +0 -0
  43. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/refiner/__init__.py +0 -0
  44. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/refiner/kg_refiner.py +0 -0
  45. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/refiner/llmlingua_compressor.py +0 -0
  46. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/refiner/refiner.py +0 -0
  47. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/refiner/selective_context_compressor.py +0 -0
  48. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/retriever/__init__.py +0 -0
  49. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/retriever/__main__.py +0 -0
  50. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/retriever/encoder.py +0 -0
  51. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/retriever/reranker.py +0 -0
  52. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/retriever/utils.py +0 -0
  53. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/utils/__init__.py +0 -0
  54. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/utils/constants.py +0 -0
  55. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag/utils/pred_parse.py +0 -0
  56. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag_dev.egg-info/SOURCES.txt +0 -0
  57. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag_dev.egg-info/dependency_links.txt +0 -0
  58. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag_dev.egg-info/requires.txt +0 -0
  59. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/flashrag_dev.egg-info/top_level.txt +0 -0
  60. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/pyproject.toml +0 -0
  61. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/setup.cfg +0 -0
  62. {flashrag_dev-0.1.4.dev20250715 → flashrag_dev-0.1.4.dev20250720}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: flashrag_dev
3
- Version: 0.1.4.dev20250715
3
+ Version: 0.1.4.dev20250720
4
4
  Summary: A library for efficient Retrieval-Augmented Generation research
5
5
  Home-page: https://github.com/RUC-NLPIR/FlashRAG
6
6
  Author: Jiajie Jin, Yutao Zhu, Chenghao Zhang, Xinyu Yang, Zhicheng Dou
@@ -278,6 +278,32 @@ python -m flashrag.retriever.index_builder \
278
278
  --save_dir indexes/
279
279
  ```
280
280
 
281
+ ### For Sparse Neural Retrieval Methods (SPLADE)
282
+
283
+ ##### Install Seismic Index:
284
+ ```bash
285
+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # Install Rust for compiling
286
+ pip install pyseismic-lsr # Install Seismic
287
+ ```
288
+
289
+ ##### Then build the index with Seismic:
290
+ ```bash
291
+ python -m flashrag.retriever.index_builder \ # builder
292
+ --retrieval_method splade \ # Model name to trigger seismic index (splade only available)
293
+ --model_path retriever/splade-v3 \ # Local path or repository path are both supported.
294
+ --corpus_embedded_path data/ms_marco/ms_marco_embedded_corpus.jsonl \ # Use cached embedded corpus if corpus is already available in seismic expected format
295
+ --corpus_path data/ms_marco/ms_marco_corpus.jsonl \ # Corpus path in format {id, contents} jsonl file to be embedded if not already built
296
+ --save_dir indexes/ \ # save index directory
297
+ --use_fp16 \ # tell to use fp16 for splade model
298
+ --max_length 512 \ # max tokens for each document
299
+ --batch_size 4 \ # batch size for splade model (4-5 seems the best size for Tesla T4 16GB)
300
+ --n_postings 1000 \ # seismic number of posting lists
301
+ --centroid_fraction 0.2 \ # seismic centroids
302
+ --min_cluster_size 2 \ # seismic min cluster
303
+ --summary_energy 0.4 \ # seismic energy
304
+ --batched_indexing 10000000 # seismic batch
305
+ --nknn 32 # Optional parameter. Tell to seismic to use also knn graph. if not present seismic will work without knn graph
306
+ ```
281
307
  ### Using the ready-made pipeline
282
308
 
283
309
  You can use the pipeline class we have already built (as shown in [<u>pipelines</u>](#pipelines)) to implement the RAG process inside. In this case, you just need to configure the config and load the corresponding pipeline.
@@ -1,20 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: flashrag-dev
3
- Version: 0.1.4.dev20250715
4
- Summary: A library for efficient Retrieval-Augmented Generation research
5
- Home-page: https://github.com/RUC-NLPIR/FlashRAG
6
- Author: Jiajie Jin, Yutao Zhu, Chenghao Zhang, Xinyu Yang, Zhicheng Dou
7
- Author-email: jinjiajie@ruc.edu.cn
8
- License: MIT License
9
- Requires-Python: >=3.9
10
- Description-Content-Type: text/markdown
11
- Provides-Extra: core
12
- Provides-Extra: retriever
13
- Provides-Extra: generator
14
- Provides-Extra: multimodal
15
- Provides-Extra: full
16
- License-File: LICENSE
17
-
18
1
  # <div align="center">⚡FlashRAG: A Python Toolkit for Efficient RAG Research<div>
19
2
  \[ English | [中文](README_zh.md) \]
20
3
  <div align="center">
@@ -278,6 +261,32 @@ python -m flashrag.retriever.index_builder \
278
261
  --save_dir indexes/
279
262
  ```
280
263
 
264
+ ### For Sparse Neural Retrieval Methods (SPLADE)
265
+
266
+ ##### Install Seismic Index:
267
+ ```bash
268
+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # Install Rust for compiling
269
+ pip install pyseismic-lsr # Install Seismic
270
+ ```
271
+
272
+ ##### Then build the index with Seismic:
273
+ ```bash
274
+ python -m flashrag.retriever.index_builder \ # builder
275
+ --retrieval_method splade \ # Model name to trigger seismic index (splade only available)
276
+ --model_path retriever/splade-v3 \ # Local path or repository path are both supported.
277
+ --corpus_embedded_path data/ms_marco/ms_marco_embedded_corpus.jsonl \ # Use cached embedded corpus if corpus is already available in seismic expected format
278
+ --corpus_path data/ms_marco/ms_marco_corpus.jsonl \ # Corpus path in format {id, contents} jsonl file to be embedded if not already built
279
+ --save_dir indexes/ \ # save index directory
280
+ --use_fp16 \ # tell to use fp16 for splade model
281
+ --max_length 512 \ # max tokens for each document
282
+ --batch_size 4 \ # batch size for splade model (4-5 seems the best size for Tesla T4 16GB)
283
+ --n_postings 1000 \ # seismic number of posting lists
284
+ --centroid_fraction 0.2 \ # seismic centroids
285
+ --min_cluster_size 2 \ # seismic min cluster
286
+ --summary_energy 0.4 \ # seismic energy
287
+ --batched_indexing 10000000 # seismic batch
288
+ --nknn 32 # Optional parameter. Tell to seismic to use also knn graph. if not present seismic will work without knn graph
289
+ ```
281
290
  ### Using the ready-made pipeline
282
291
 
283
292
  You can use the pipeline class we have already built (as shown in [<u>pipelines</u>](#pipelines)) to implement the RAG process inside. In this case, you just need to configure the config and load the corresponding pipeline.
@@ -68,7 +68,9 @@ bm25_backend: bm25s # pyserini, bm25s
68
68
  use_sentence_transformer: False
69
69
  silent_retrieval: True # whether to silent the retrieval process
70
70
 
71
-
71
+ seismic_query_cut: 10 # parameters for seismic. See seismic paper for full details
72
+ seismic_heap_factor: 0.8 # parameters for seismic. See seismic paper for full details
73
+ # -------------------------------------------------Reranker Settings------------------------------------------------#
72
74
  use_reranker: False # whether to use reranker
73
75
  rerank_model_name: ~ # same as retrieval_method
74
76
  rerank_model_path: ~ # path to reranker model, path will be automatically find in `model2path`
@@ -117,6 +119,7 @@ gpu_memory_utilization: 0.85 # ratio of gpu's memory usage for generator
117
119
  # -------------------------------------------------Evaluation Settings------------------------------------------------#
118
120
  # Metrics to evaluate the result
119
121
  metrics: ["em", "f1", "acc", "precision", "recall", "input_tokens"]
122
+
120
123
  # Specify setting for metric, will be called within certain metrics
121
124
  metric_setting:
122
125
  retrieval_recall_topk: 5
@@ -1,45 +1,62 @@
1
- import os
2
1
  import re
2
+ import time
3
+ from concurrent.futures import ThreadPoolExecutor
4
+
3
5
  import faiss
4
6
  import json
5
7
  import warnings
6
8
  import numpy as np
7
- from typing import cast
9
+ from typing import Dict, List
8
10
  import shutil
9
11
  import subprocess
10
12
  import argparse
11
13
  import datasets
12
14
  import torch
15
+ from seismic import SeismicIndex
13
16
  from tqdm import tqdm
14
17
  from flashrag.retriever.utils import load_model, load_corpus, pooling, set_default_instruction, judge_zh
18
+ from transformers import AutoTokenizer, AutoModelForMaskedLM
19
+
20
+ import os
21
+ import multiprocessing
22
+
23
+ cores = str(multiprocessing.cpu_count())
24
+ os.environ["RAYON_NUM_THREADS"] = cores
15
25
 
16
26
 
17
27
  class Index_Builder:
18
28
  r"""A tool class used to build an index used in retrieval."""
19
29
 
20
30
  def __init__(
21
- self,
22
- retrieval_method,
23
- model_path,
24
- corpus_path,
25
- save_dir,
26
- max_length,
27
- batch_size,
28
- use_fp16,
29
- pooling_method=None,
30
- instruction=None,
31
- faiss_type=None,
32
- embedding_path=None,
33
- save_embedding=False,
34
- faiss_gpu=False,
35
- use_sentence_transformer=False,
36
- bm25_backend="bm25s",
37
- index_modal="all",
31
+ self,
32
+ retrieval_method,
33
+ model_path,
34
+ corpus_path,
35
+ save_dir,
36
+ max_length,
37
+ batch_size,
38
+ use_fp16,
39
+ n_postings=1000,
40
+ centroid_fraction=0.2,
41
+ min_cluster_size=2,
42
+ summary_energy=0.4,
43
+ batched_indexing=10000,
44
+ corpus_embedded_path=None,
45
+ pooling_method=None,
46
+ instruction=None,
47
+ faiss_type=None,
48
+ embedding_path=None,
49
+ save_embedding=False,
50
+ faiss_gpu=False,
51
+ use_sentence_transformer=False,
52
+ bm25_backend="bm25s",
53
+ index_modal="all",
54
+ nknn=0,
38
55
  ):
39
-
40
56
  self.retrieval_method = retrieval_method.lower()
41
57
  self.model_path = model_path
42
58
  self.corpus_path = corpus_path
59
+ self.corpus_embedded_path = corpus_embedded_path
43
60
  self.save_dir = save_dir
44
61
  self.max_length = max_length
45
62
  self.batch_size = batch_size
@@ -52,6 +69,12 @@ class Index_Builder:
52
69
  self.use_sentence_transformer = use_sentence_transformer
53
70
  self.bm25_backend = bm25_backend
54
71
  self.index_modal = index_modal
72
+ self.n_postings = n_postings
73
+ self.centroid_fraction = centroid_fraction
74
+ self.min_cluster_size = min_cluster_size
75
+ self.summary_energy = summary_energy
76
+ self.batched_indexing = batched_indexing
77
+ self.nknn = nknn
55
78
 
56
79
  # judge if the retrieval model is clip
57
80
  self.is_clip = ("clip" in self.retrieval_method) or (self.model_path is not None and "clip" in self.model_path)
@@ -127,9 +150,236 @@ class Index_Builder:
127
150
  self.build_bm25_index_bm25s()
128
151
  else:
129
152
  assert False, "Invalid bm25 backend!"
153
+ elif self.retrieval_method == "splade":
154
+ self.build_seismic_index()
130
155
  else:
131
156
  self.build_dense_index()
132
157
 
158
+ def build_seismic_index(self):
159
+ """Build Seismic index after saving documents in required JSONL format using batch processing."""
160
+
161
+ r"""Full Command:
162
+ # Use "splade" (sparse embedding neural model) as retrieval method to trigger sysmic index costruction.
163
+ python -m flashrag.retriever.index_builder \ # builder
164
+ --retrieval_method splade \ # Model name to trigger seismic index (splade only available)
165
+ --model_path retriever/splade-v3 \ # Local path or repository path are both supported.
166
+ --corpus_embedded_path data/ms_marco/ms_marco_embedded_corpus.jsonl \ # Use cached embedded corpus if corpus is already available ins seismic expected format
167
+ --corpus_path data/ms_marco/ms_marco_corpus.jsonl \ # Corpus path in format {id, contents} jsonl file to be embedded if not already built
168
+ --save_dir indexes/ \ # save index directory
169
+ --use_fp16 \ # tell to use fp16 for splade model
170
+ --max_length 512 \ # max tokens for each document
171
+ --batch_size 4 \ # batch size for splade model (Suggested between 2 and 24 for 16GB VRAM)
172
+ --n_postings 1000 \ # seismic number of posting lists
173
+ --centroid_fraction 0.2 \ # seismic centroids
174
+ --min_cluster_size 2 \ # seismic min cluster
175
+ --summary_energy 0.4 \ # seismic energy
176
+ --batched_indexing 10000 # seismic batch
177
+ --nknn 32
178
+ """
179
+
180
+ if self.pooling_method != 'max':
181
+ print(
182
+ f'Pooling method: {self.pooling_method.upper()} not supported on sparse neural retrieval models. fallback to: MAX.')
183
+ # Load document encoder model (only splade i currently implemented at the moment)
184
+ tokenizer = AutoTokenizer.from_pretrained(self.model_path)
185
+ model = AutoModelForMaskedLM.from_pretrained(self.model_path)
186
+ # Use half precision
187
+ if self.use_fp16:
188
+ model = model.half()
189
+ # Use more devices if available
190
+ if torch.cuda.device_count() > 1:
191
+ print(f"Using {torch.cuda.device_count()} GPUs")
192
+ model = torch.nn.DataParallel(model)
193
+
194
+ # Load to cuda
195
+ model = model.to('cuda' if torch.cuda.is_available() else 'cpu')
196
+ model.eval()
197
+
198
+ # Create file for embedded corpus
199
+ corpus_name = os.path.splitext(os.path.basename(self.corpus_path))[0]
200
+ output_path = os.path.join(self.save_dir, f"{corpus_name}_{self.retrieval_method}_embedded.jsonl")
201
+
202
+ # Init vars
203
+ total_docs = len(self.corpus)
204
+ processed_docs = 0
205
+ start_time = time.time()
206
+ last_update = start_time
207
+
208
+ if self.corpus_embedded_path:
209
+ print("Using cached corpus in seismic format.")
210
+ if self.nknn > 0:
211
+ index = SeismicIndex.build(
212
+ self.corpus_embedded_path,
213
+ n_postings=self.n_postings,
214
+ centroid_fraction=self.centroid_fraction,
215
+ min_cluster_size=self.min_cluster_size,
216
+ summary_energy=self.summary_energy,
217
+ batched_indexing=self.batched_indexing,
218
+ num_threads=int(cores),
219
+ nknn=self.nknn
220
+ )
221
+ else:
222
+ index = SeismicIndex.build(
223
+ self.corpus_embedded_path,
224
+ n_postings=self.n_postings,
225
+ centroid_fraction=self.centroid_fraction,
226
+ min_cluster_size=self.min_cluster_size,
227
+ summary_energy=self.summary_energy,
228
+ batched_indexing=self.batched_indexing,
229
+ num_threads=int(cores)
230
+ )
231
+ index.save(os.path.join(self.save_dir, f"{corpus_name}_{self.retrieval_method}"))
232
+ return index
233
+
234
+
235
+ with open(output_path, 'w') as f_out, ThreadPoolExecutor(max_workers=1) as pool:
236
+ # create progress bar
237
+ progress_bar = tqdm(
238
+ desc="Processing Documents",
239
+ total=total_docs,
240
+ unit="doc",
241
+ bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]"
242
+ )
243
+
244
+ batch = []
245
+ for doc in self.corpus:
246
+ # Load batch
247
+ batch.append(doc)
248
+
249
+ # On batch ready process batch
250
+ if len(batch) >= self.batch_size:
251
+ self._process_batch(batch, f_out, model, tokenizer, pool)
252
+ processed_docs += len(batch)
253
+ batch = []
254
+
255
+ # Progress updates
256
+ current_time = time.time()
257
+ if processed_docs % 100 == 0 or (current_time - last_update) > 5.0:
258
+ elapsed = current_time - start_time
259
+ docs_per_sec = processed_docs / elapsed
260
+ remaining = (total_docs - processed_docs) / docs_per_sec
261
+
262
+ progress_bar.set_postfix({
263
+ 'speed': f"{docs_per_sec:.1f} docs/s",
264
+ 'ETA': f"{remaining / 60:.1f} min"
265
+ })
266
+ progress_bar.update(100 if processed_docs % 100 == 0 else processed_docs % 100)
267
+ last_update = current_time
268
+
269
+ # Handle final leftover batch
270
+ if batch:
271
+ self._process_batch(batch, f_out, model, tokenizer, pool)
272
+ progress_bar.update(len(batch))
273
+
274
+ progress_bar.close()
275
+
276
+ if self.nknn > 0:
277
+ index = SeismicIndex.build(
278
+ output_path,
279
+ n_postings=self.n_postings,
280
+ centroid_fraction=self.centroid_fraction,
281
+ min_cluster_size=self.min_cluster_size,
282
+ summary_energy=self.summary_energy,
283
+ batched_indexing=self.batched_indexing,
284
+ num_threads=int(cores),
285
+ nknn=self.nknn
286
+ )
287
+ else:
288
+ # Create index on embedded corpus file and save it
289
+ index = SeismicIndex.build(
290
+ output_path,
291
+ n_postings=self.n_postings,
292
+ centroid_fraction=self.centroid_fraction,
293
+ min_cluster_size=self.min_cluster_size,
294
+ summary_energy=self.summary_energy,
295
+ batched_indexing=self.batched_indexing,
296
+ num_threads=int(cores)
297
+ )
298
+
299
+ index.save(os.path.join(self.save_dir, f"{corpus_name}_{self.retrieval_method}"))
300
+ return index
301
+
302
+ def _process_batch(self, batch, f_out, model, tokenizer, pool):
303
+ # Get embeddings
304
+ texts = [doc['contents'] for doc in batch]
305
+ vectors = self._get_sparse_embedding(texts, model, tokenizer)
306
+
307
+ # create json for the batch
308
+ def save(docs, embs):
309
+ for doc, vector in zip(docs, embs):
310
+ if 'vector' not in doc:
311
+ doc['vector'] = vector
312
+ f_out.write(json.dumps({
313
+ "id": str(doc['id']),
314
+ "contents": doc['contents'],
315
+ "vector": doc['vector']
316
+ }) + "\n")
317
+ pool.submit(save, batch, vectors)
318
+
319
+ def _get_sparse_embedding(self, texts: List[str], model, tokenizer) -> List[Dict[str, float]]:
320
+ """Generate sparse embeddings for a batch of texts."""
321
+ inputs = tokenizer(
322
+ texts,
323
+ return_tensors="pt",
324
+ truncation=True,
325
+ padding=True,
326
+ max_length=self.max_length,
327
+ add_special_tokens=True
328
+ ).to(model.device)
329
+
330
+ with torch.no_grad():
331
+ logits = model(**inputs).logits # [batch_size, seq_len, vocab_size]
332
+ attention_mask = inputs["attention_mask"].unsqueeze(-1)
333
+
334
+ scores = torch.log1p(torch.relu(logits)) * attention_mask
335
+
336
+ v_repr = torch.max(scores, dim=1)[0] # [batch_size, vocab_size]
337
+
338
+ # Move to CPU (it seems much faster)
339
+ v_repr = v_repr.cpu()
340
+ nonzero_mask = v_repr > 1e-4
341
+
342
+ # Get sparse values and indices in batch
343
+ batch_indices, token_indices = torch.nonzero(nonzero_mask, as_tuple=True)
344
+ token_scores = v_repr[batch_indices, token_indices]
345
+
346
+ # Convert once all token IDs to strings (batched)
347
+ unique_token_ids = torch.unique(token_indices)
348
+ token_id_to_token = {
349
+ idx.item(): tok for idx, tok in zip(
350
+ unique_token_ids, tokenizer.convert_ids_to_tokens(unique_token_ids.tolist())
351
+ )
352
+ }
353
+
354
+ # Build final embeddings
355
+ from collections import defaultdict
356
+ embeddings = defaultdict(dict)
357
+ for b_idx, t_idx, score in zip(batch_indices, token_indices, token_scores):
358
+ embeddings[b_idx.item()][token_id_to_token[t_idx.item()]] = round(score.item(), 4)
359
+
360
+ # Convert to list for each document
361
+ return [embeddings[i] for i in range(len(texts))]
362
+
363
+ @staticmethod
364
+ def get_tokens_and_weights(sparse_embedding, tokenizer):
365
+ token_weight_dict = {}
366
+ for i in range(len(sparse_embedding.indices)):
367
+ token = tokenizer.decode([sparse_embedding.indices[i]])
368
+ weight = sparse_embedding.values[i]
369
+ token_weight_dict[token] = round(weight, 4)
370
+
371
+ # Sort the dictionary by weights
372
+ token_weight_dict = dict(sorted(token_weight_dict.items(), key=lambda item: item[1], reverse=True))
373
+ return token_weight_dict
374
+
375
+ @staticmethod
376
+ def clean_text(self, text: str) -> str:
377
+ """Preprocess the text by removing special characters that seems to cause some problems in seismic index build resulting in a parse error."""
378
+ text = re.sub(r'<[^>]+>', '', text) # Remove HTML tags
379
+ text = re.sub(r'\s+', ' ', text) # Normalize multiple spaces
380
+ text = text.strip() # Remove leading/trailing whitespaces
381
+ return text
382
+
133
383
  def build_bm25_index_pyserini(self):
134
384
  """Building BM25 index based on Pyserini library.
135
385
 
@@ -147,7 +397,7 @@ class Index_Builder:
147
397
  shutil.copyfile(self.corpus_path, temp_file_path)
148
398
  # check if the language is chinese
149
399
  with open(self.corpus_path, 'r', encoding='utf-8') as file:
150
- first_item = json.loads(file.readline())
400
+ first_item = json.loads(file.readline())
151
401
  contents = first_item.get("contents", "") # 获取 contents 字段
152
402
  zh_flag = judge_zh(contents)
153
403
  elif self.corpus_path.endswith(".parquet"):
@@ -204,7 +454,7 @@ class Index_Builder:
204
454
  else:
205
455
  stemmer = Stemmer.Stemmer("english")
206
456
  tokenizer = bm25s.tokenization.Tokenizer(stopwords='en', stemmer=stemmer)
207
-
457
+
208
458
  corpus_text = corpus["contents"]
209
459
  corpus_tokens = tokenizer.tokenize(corpus_text, return_as='tuple')
210
460
  retriever = bm25s.BM25(corpus=corpus, backend="numba")
@@ -313,7 +563,7 @@ class Index_Builder:
313
563
  if self.index_modal == "all":
314
564
  assert all_embeddings.shape[0] % 2 == 0
315
565
  text_embedding = all_embeddings[: len(all_embeddings) // 2, :]
316
- image_embedding = all_embeddings[len(all_embeddings) // 2 :, :]
566
+ image_embedding = all_embeddings[len(all_embeddings) // 2:, :]
317
567
  text_index_save_path = os.path.join(
318
568
  self.save_dir, f"{self.retrieval_method}_{self.faiss_type}_text.index"
319
569
  )
@@ -336,10 +586,10 @@ class Index_Builder:
336
586
  print("Finish!")
337
587
 
338
588
  def save_faiss_index(
339
- self,
340
- all_embeddings,
341
- faiss_type,
342
- index_save_path,
589
+ self,
590
+ all_embeddings,
591
+ faiss_type,
592
+ index_save_path,
343
593
  ):
344
594
  # build index
345
595
  print("Creating index")
@@ -363,6 +613,9 @@ class Index_Builder:
363
613
  faiss.write_index(faiss_index, index_save_path)
364
614
 
365
615
 
616
+ import argparse
617
+
618
+
366
619
  def main():
367
620
  parser = argparse.ArgumentParser(description="Creating index.")
368
621
 
@@ -370,6 +623,7 @@ def main():
370
623
  parser.add_argument("--retrieval_method", type=str)
371
624
  parser.add_argument("--model_path", type=str, default=None)
372
625
  parser.add_argument("--corpus_path", type=str)
626
+ parser.add_argument("--corpus_embedded_path", type=str, default=None)
373
627
  parser.add_argument("--save_dir", default="indexes/", type=str)
374
628
 
375
629
  # Parameters for building dense index
@@ -388,6 +642,14 @@ def main():
388
642
  # Parameters for build multi-modal retriever index
389
643
  parser.add_argument("--index_modal", type=str, default="all", choices=["text", "image", "all"])
390
644
 
645
+ # New arguments for seismic index
646
+ parser.add_argument("--n_postings", type=int, default=1000)
647
+ parser.add_argument("--centroid_fraction", type=float, default=0.2)
648
+ parser.add_argument("--min_cluster_size", type=int, default=2)
649
+ parser.add_argument("--summary_energy", type=float, default=0.4)
650
+ parser.add_argument("--nknn", type=int, default=0)
651
+ parser.add_argument("--batched_indexing", type=int, default=10000)
652
+
391
653
  args = parser.parse_args()
392
654
 
393
655
  index_builder = Index_Builder(
@@ -407,6 +669,13 @@ def main():
407
669
  use_sentence_transformer=args.sentence_transformer,
408
670
  bm25_backend=args.bm25_backend,
409
671
  index_modal=args.index_modal,
672
+ n_postings=args.n_postings,
673
+ centroid_fraction=args.centroid_fraction,
674
+ min_cluster_size=args.min_cluster_size,
675
+ summary_energy=args.summary_energy,
676
+ batched_indexing=args.batched_indexing,
677
+ corpus_embedded_path=args.corpus_embedded_path,
678
+ nknn=args.nknn
410
679
  )
411
680
  index_builder.build_index()
412
681
 
@@ -15,6 +15,7 @@ from flashrag.utils import get_reranker
15
15
  from flashrag.retriever.utils import load_corpus, load_docs, convert_numpy, judge_image, judge_zh
16
16
  from flashrag.retriever.encoder import Encoder, STEncoder, ClipEncoder
17
17
 
18
+ import torch
18
19
 
19
20
  def cache_manager(func):
20
21
  """
@@ -932,3 +933,209 @@ class MultiRetrieverRouter:
932
933
  final_result.append(image_result[image_idx])
933
934
  image_idx += 1
934
935
  return final_result
936
+
937
+
938
+ class SparseRetriever(BaseTextRetriever):
939
+ """Sparse embedding retriever supporting only SPLADE with Seismic backend for now."""
940
+
941
+ def __init__(self, config):
942
+ super().__init__(config)
943
+
944
+ import multiprocessing
945
+ self.cores = str(multiprocessing.cpu_count())
946
+ os.environ["RAYON_NUM_THREADS"] = self.cores
947
+
948
+ self.progress_bar = None
949
+
950
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
951
+ self.corpus = load_corpus(config["corpus_path"])
952
+ self.tokenizer, self.model = self._load_sparse_model()
953
+
954
+ self.id = 0
955
+
956
+ self.update_additional_setting()
957
+
958
+ self.seismic_query_cut = self.config["seismic_query_cut"]
959
+ self.seismic_heap_factor = self.config["seismic_heap_factor"]
960
+ self.index_max_tokens = self.config["seismic_max_tokens_length"]
961
+ self._init_seismic_index()
962
+
963
+ def update_additional_setting(self):
964
+ """Load config shared for all the models supported"""
965
+ self.query_max_length = self._config["retrieval_query_max_length"]
966
+ self.use_fp16 = self._config["retrieval_use_fp16"]
967
+ self.batch_size = self._config["retrieval_batch_size"]
968
+ self.retrieval_model_path = self._config["retrieval_model_path"]
969
+ self.pooling_method = self._config["retrieval_pooling_method"]
970
+
971
+ def _init_seismic_index(self):
972
+ """Initialize Seismic index."""
973
+ from seismic import SeismicIndex # Assuming this is available
974
+ self.seismic_index = SeismicIndex.load(self.index_path)
975
+ self.string_type = f'U{self.index_max_tokens}' # For Seismic string dtype
976
+
977
+ def _load_sparse_model(self):
978
+ """Load tokenizer and model based on sparse type."""
979
+ from transformers import AutoModelForMaskedLM, AutoTokenizer
980
+ # Load model
981
+ tokenizer = AutoTokenizer.from_pretrained(self.retrieval_model_path)
982
+ model = AutoModelForMaskedLM.from_pretrained(self.retrieval_model_path)
983
+
984
+ if self.use_fp16:
985
+ model = model.half()
986
+
987
+ # Use more gpus if available
988
+ if torch.cuda.device_count() > 1:
989
+ model = torch.nn.DataParallel(model, device_ids=self.config['gpu_id'].split(','))
990
+
991
+ model = model.to(self.device)
992
+ model.eval()
993
+ return tokenizer, model
994
+
995
+ def _encode(self, query):
996
+ inputs = self.tokenizer(
997
+ query,
998
+ return_tensors="pt",
999
+ truncation=True,
1000
+ padding=True,
1001
+ max_length=self.query_max_length,
1002
+ add_special_tokens=True
1003
+ ).to(self.model.device)
1004
+
1005
+ with torch.no_grad():
1006
+ logits = self.model(**inputs).logits # [batch_size, seq_len, vocab_size]
1007
+ attention_mask = inputs["attention_mask"].unsqueeze(-1) # [batch, seq_len, 1]
1008
+
1009
+ scores = torch.log1p(torch.relu(logits)) * attention_mask
1010
+ v_repr = torch.max(scores, dim=1)[0] # [batch_size, vocab_size]
1011
+
1012
+ # Move to CPU (it seems much faster)
1013
+ v_repr = v_repr.cpu()
1014
+ nonzero_mask = v_repr > 1e-4
1015
+
1016
+ # Get sparse values and indices in batch
1017
+ batch_indices, token_indices = torch.nonzero(nonzero_mask, as_tuple=True)
1018
+ token_scores = v_repr[batch_indices, token_indices]
1019
+
1020
+ # Convert once all token IDs to strings (batched)
1021
+ unique_token_ids = torch.unique(token_indices)
1022
+ token_id_to_token = {
1023
+ idx.item(): tok for idx, tok in zip(
1024
+ unique_token_ids, self.tokenizer.convert_ids_to_tokens(unique_token_ids.tolist())
1025
+ )
1026
+ }
1027
+
1028
+ # Build final embeddings
1029
+ from collections import defaultdict
1030
+ embeddings = defaultdict(dict)
1031
+ for b_idx, t_idx, score in zip(batch_indices, token_indices, token_scores):
1032
+ embeddings[b_idx.item()][token_id_to_token[t_idx.item()]] = round(score.item(), 4)
1033
+
1034
+ # Convert to list for each document
1035
+ return [embeddings[i] for i in range(len(query))]
1036
+
1037
+ def search(self, query: list, num: int = None, return_score=False) -> (List[Dict], List[float]):
1038
+ """Search using sparse vector."""
1039
+ num = num or self.topk
1040
+
1041
+ query_vec = self._encode(query)
1042
+ results, scores = self._seismic_search(query_vec, num)
1043
+
1044
+ if return_score:
1045
+ return results, scores
1046
+ else:
1047
+ return results
1048
+
1049
+ def batch_search(self, query, num=None, return_score=False):
1050
+ """Search using sparse vector."""
1051
+ if isinstance(query, str):
1052
+ query = [query]
1053
+
1054
+ if self.pooling_method != 'max':
1055
+ print(
1056
+ f'Pooling method: {self.pooling_method.upper()} not supported on sparse neural retrieval models. fallback to: MAX.')
1057
+
1058
+ num = num or self.topk
1059
+
1060
+ embeddings = []
1061
+ batch = []
1062
+
1063
+ # Encode
1064
+ for i in range(len(query)):
1065
+ # Process batch
1066
+ batch.append(query[i])
1067
+ if len(batch) >= self.batch_size:
1068
+ query_vec = self._encode(batch)
1069
+ embeddings.extend(query_vec)
1070
+ batch = []
1071
+
1072
+ if batch:
1073
+ query_vec = self._encode(batch)
1074
+ embeddings.extend(query_vec)
1075
+
1076
+ # Search
1077
+ search_results = self._seismic_batch_search(embeddings, num)
1078
+
1079
+ results = []
1080
+ scores = []
1081
+ for result in sorted(search_results, key=lambda e: int(e[0][0])):
1082
+ tmp_results = []
1083
+ tmp_scores = []
1084
+
1085
+ for query_id, score, doc_id in result:
1086
+ tmp_results.append(self.corpus[int(doc_id)])
1087
+ tmp_scores.append(score)
1088
+
1089
+ results.append(tmp_results)
1090
+ scores.append(tmp_scores)
1091
+
1092
+ if return_score:
1093
+ return results, scores
1094
+ else:
1095
+ return results
1096
+
1097
+ def _seismic_search(self, query_vec: List[Dict[str, float]], k: int) -> (List[Dict], List[float]):
1098
+ """Search using Seismic backend."""
1099
+ # Convert query to Seismic format
1100
+ results, scores = self.index_search(k, query_vec)
1101
+ return results[0], scores[0]
1102
+
1103
+ def _seismic_batch_search(self, query_vecs: List[Dict[str, float]], k: int) -> (
1104
+ List[List[Dict]], List[List[float]]):
1105
+ """Batch search using Seismic backend (one query at a time)."""
1106
+ return self.index_search(k, query_vecs)
1107
+
1108
+ def index_search(self, k, query_vec):
1109
+ max_len = max(len(query) for query in query_vec)
1110
+ pad_token = "" # or whatever default is appropriate
1111
+
1112
+ query_components = []
1113
+ query_values = []
1114
+ ids = []
1115
+
1116
+ for query in query_vec:
1117
+ keys = list(query.keys())
1118
+ values = list(query.values())
1119
+
1120
+ # Pad to max_len
1121
+ padded_keys = keys + [pad_token] * (max_len - len(keys))
1122
+ padded_values = values + [0.0] * (max_len - len(values))
1123
+
1124
+ query_components.append(np.array(padded_keys, dtype='U30'))
1125
+ query_values.append(np.array(padded_values, dtype=np.float32))
1126
+ ids.append(self.id)
1127
+ self.id += 1
1128
+
1129
+ ids = np.array(ids, dtype='U30')
1130
+ # Execute search
1131
+ search_results = self.seismic_index.batch_search(
1132
+ queries_ids=ids, # Placeholder ID
1133
+ query_components=query_components,
1134
+ query_values=query_values,
1135
+ query_cut=self.seismic_query_cut,
1136
+ heap_factor=self.seismic_heap_factor,
1137
+ k=k,
1138
+ sorted=True, # specified even if default value
1139
+ num_threads=int(self.cores)
1140
+ )
1141
+ return search_results
@@ -28,7 +28,7 @@ def get_dataset(config):
28
28
  continue
29
29
  else:
30
30
  print(f"Loading {split} dataset from: {split_path}...")
31
- if split in ["test", "val", "dev"]:
31
+ if split in ["test", "val", "dev", "train"]:
32
32
  split_dict[split] = Dataset(
33
33
  config, split_path, sample_num=config["test_sample_num"], random_sample=config["random_sample"]
34
34
  )
@@ -84,6 +84,8 @@ def get_retriever(config):
84
84
 
85
85
  if config["retrieval_method"] == "bm25":
86
86
  return getattr(importlib.import_module("flashrag.retriever"), "BM25Retriever")(config)
87
+ elif config["retrieval_method"] == "splade":
88
+ return getattr(importlib.import_module("flashrag.retriever"), "SparseRetriever")(config)
87
89
  else:
88
90
  try:
89
91
  model_config = AutoConfig.from_pretrained(config["retrieval_model_path"])
@@ -180,4 +182,4 @@ def extract_between(text: str, start_tag: str, end_tag: str):
180
182
  matches = re.findall(pattern, text, flags=re.DOTALL)
181
183
  if matches:
182
184
  return matches[-1].strip()
183
- return None
185
+ return None
@@ -0,0 +1 @@
1
+ __version__ = "0.1.4dev20250720"
@@ -1,3 +1,20 @@
1
+ Metadata-Version: 2.1
2
+ Name: flashrag-dev
3
+ Version: 0.1.4.dev20250720
4
+ Summary: A library for efficient Retrieval-Augmented Generation research
5
+ Home-page: https://github.com/RUC-NLPIR/FlashRAG
6
+ Author: Jiajie Jin, Yutao Zhu, Chenghao Zhang, Xinyu Yang, Zhicheng Dou
7
+ Author-email: jinjiajie@ruc.edu.cn
8
+ License: MIT License
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ Provides-Extra: core
12
+ Provides-Extra: retriever
13
+ Provides-Extra: generator
14
+ Provides-Extra: multimodal
15
+ Provides-Extra: full
16
+ License-File: LICENSE
17
+
1
18
  # <div align="center">⚡FlashRAG: A Python Toolkit for Efficient RAG Research<div>
2
19
  \[ English | [中文](README_zh.md) \]
3
20
  <div align="center">
@@ -261,6 +278,32 @@ python -m flashrag.retriever.index_builder \
261
278
  --save_dir indexes/
262
279
  ```
263
280
 
281
+ ### For Sparse Neural Retrieval Methods (SPLADE)
282
+
283
+ ##### Install Seismic Index:
284
+ ```bash
285
+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # Install Rust for compiling
286
+ pip install pyseismic-lsr # Install Seismic
287
+ ```
288
+
289
+ ##### Then build the index with Seismic:
290
+ ```bash
291
+ python -m flashrag.retriever.index_builder \ # builder
292
+ --retrieval_method splade \ # Model name to trigger seismic index (splade only available)
293
+ --model_path retriever/splade-v3 \ # Local path or repository path are both supported.
294
+ --corpus_embedded_path data/ms_marco/ms_marco_embedded_corpus.jsonl \ # Use cached embedded corpus if corpus is already available in seismic expected format
295
+ --corpus_path data/ms_marco/ms_marco_corpus.jsonl \ # Corpus path in format {id, contents} jsonl file to be embedded if not already built
296
+ --save_dir indexes/ \ # save index directory
297
+ --use_fp16 \ # tell to use fp16 for splade model
298
+ --max_length 512 \ # max tokens for each document
299
+ --batch_size 4 \ # batch size for splade model (4-5 seems the best size for Tesla T4 16GB)
300
+ --n_postings 1000 \ # seismic number of posting lists
301
+ --centroid_fraction 0.2 \ # seismic centroids
302
+ --min_cluster_size 2 \ # seismic min cluster
303
+ --summary_energy 0.4 \ # seismic energy
304
+ --batched_indexing 10000000 # seismic batch
305
+ --nknn 32 # Optional parameter. Tell to seismic to use also knn graph. if not present seismic will work without knn graph
306
+ ```
264
307
  ### Using the ready-made pipeline
265
308
 
266
309
  You can use the pipeline class we have already built (as shown in [<u>pipelines</u>](#pipelines)) to implement the RAG process inside. In this case, you just need to configure the config and load the corresponding pipeline.
@@ -1 +0,0 @@
1
- __version__ = "0.1.4dev20250715"