thebajajra commited on
Commit
f142b83
·
verified ·
1 Parent(s): 6eaec98

Use pre-built FAISS index with IDSelectorBatch filtering (no re-encoding)

Browse files
app.py CHANGED
@@ -136,17 +136,23 @@ def _get_env() -> ShopRLVEEnv:
136
  products = load_catalog(CATALOG_PATH, max_items=CATALOG_MAX_ITEMS, seed=42)
137
  logger.info("Loaded %d products", len(products))
138
 
139
- # NOTE: Do NOT use the pre-built 2M FAISS index here.
140
- # It contains 2M vectors but only ~5K products are loaded,
141
- # so >99% of FAISS results would be IDs not in the catalog → empty results.
142
- # Instead, build a fresh index from the loaded products using real embeddings.
 
143
  config = {
144
  "embedding_model": EMBEDDING_MODEL,
145
- "embedding_debug": False, # use real gte-small embeddings
146
  "embedding_device": EMBEDDING_DEVICE,
147
  }
 
 
148
 
149
- logger.info("Creating ShopRLVEEnv (building index from %d products)...", len(products))
 
 
 
150
  env = ShopRLVEEnv(
151
  collection="C1",
152
  catalog=(products, []),
 
136
  products = load_catalog(CATALOG_PATH, max_items=CATALOG_MAX_ITEMS, seed=42)
137
  logger.info("Loaded %d products", len(products))
138
 
139
+ # Use the pre-built 2M FAISS index if available. openenv.py will
140
+ # automatically call set_allowed_ids() to restrict FAISS search to
141
+ # only the loaded products (IDSelectorBatch), so results are always
142
+ # valid even though the index covers 2M products.
143
+ faiss_path = FAISS_INDEX_DIR if Path(FAISS_INDEX_DIR).exists() else None
144
  config = {
145
  "embedding_model": EMBEDDING_MODEL,
146
+ "embedding_debug": False, # need real model to encode search queries
147
  "embedding_device": EMBEDDING_DEVICE,
148
  }
149
+ if faiss_path:
150
+ config["faiss_index_path"] = faiss_path
151
 
152
+ logger.info(
153
+ "Creating ShopRLVEEnv (faiss=%s, products=%d)...",
154
+ faiss_path or "build-from-scratch", len(products),
155
+ )
156
  env = ShopRLVEEnv(
157
  collection="C1",
158
  catalog=(products, []),
src/shop_rlve/data/__pycache__/index.cpython-311.pyc CHANGED
Binary files a/src/shop_rlve/data/__pycache__/index.cpython-311.pyc and b/src/shop_rlve/data/__pycache__/index.cpython-311.pyc differ
 
src/shop_rlve/data/index.py CHANGED
@@ -61,6 +61,8 @@ class VectorIndex:
61
  self._index = None # faiss.Index, built lazily
62
  self._id_map: list[str] = [] # positional index -> product_id
63
  self._id_to_pos: dict[str, int] = {} # product_id -> positional index
 
 
64
 
65
  def __len__(self) -> int:
66
  """Number of indexed vectors."""
@@ -73,6 +75,36 @@ class VectorIndex:
73
  """Whether the index has been built with embeddings."""
74
  return self._index is not None and self._index.ntotal > 0
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  def build(self, embeddings: np.ndarray, ids: list[str]) -> None:
77
  """Build the FAISS index from embeddings and product IDs.
78
 
@@ -163,14 +195,38 @@ class VectorIndex:
163
  return []
164
 
165
  # Clamp top_k to available vectors
166
- effective_k = min(top_k, self._index.ntotal)
 
167
 
168
  # FAISS expects a 2-D query array
169
  query = np.ascontiguousarray(
170
  query_embedding.reshape(1, -1), dtype=np.float32
171
  )
172
 
173
- distances, indices = self._index.search(query, effective_k)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
  results: list[tuple[str, float]] = []
176
  for dist, idx in zip(distances[0], indices[0]):
 
61
  self._index = None # faiss.Index, built lazily
62
  self._id_map: list[str] = [] # positional index -> product_id
63
  self._id_to_pos: dict[str, int] = {} # product_id -> positional index
64
+ self._allowed_positions: np.ndarray | None = None # for filtered search
65
+ self._id_selector = None # faiss.IDSelectorBatch
66
 
67
  def __len__(self) -> int:
68
  """Number of indexed vectors."""
 
75
  """Whether the index has been built with embeddings."""
76
  return self._index is not None and self._index.ntotal > 0
77
 
78
+ def set_allowed_ids(self, allowed_ids: set[str]) -> None:
79
+ """Restrict future searches to only return results from *allowed_ids*.
80
+
81
+ This is used when a pre-built index covers more products than are
82
+ currently loaded into the catalog. FAISS ``IDSelectorBatch`` is
83
+ used under the hood so the filtering happens inside the FAISS
84
+ search loop — no post-hoc filtering needed.
85
+
86
+ Args:
87
+ allowed_ids: Set of product-ID strings that are allowed.
88
+ """
89
+ import faiss # noqa: F811
90
+
91
+ positions = np.array(
92
+ [pos for pid, pos in self._id_to_pos.items() if pid in allowed_ids],
93
+ dtype=np.int64,
94
+ )
95
+ if len(positions) == 0:
96
+ logger.warning("set_allowed_ids: no overlap between allowed_ids and index")
97
+ self._allowed_positions = None
98
+ self._id_selector = None
99
+ return
100
+
101
+ self._allowed_positions = positions
102
+ self._id_selector = faiss.IDSelectorBatch(positions)
103
+ logger.info(
104
+ "VectorIndex: restricted search to %d / %d indexed products",
105
+ len(positions), len(self._id_map),
106
+ )
107
+
108
  def build(self, embeddings: np.ndarray, ids: list[str]) -> None:
109
  """Build the FAISS index from embeddings and product IDs.
110
 
 
195
  return []
196
 
197
  # Clamp top_k to available vectors
198
+ n_allowed = len(self._allowed_positions) if self._allowed_positions is not None else self._index.ntotal
199
+ effective_k = min(top_k, n_allowed)
200
 
201
  # FAISS expects a 2-D query array
202
  query = np.ascontiguousarray(
203
  query_embedding.reshape(1, -1), dtype=np.float32
204
  )
205
 
206
+ if self._id_selector is not None:
207
+ # Filtered search — restrict results to allowed product IDs
208
+ import faiss # noqa: F811
209
+
210
+ # Detect index type and build appropriate SearchParameters
211
+ raw_index = self._index
212
+ # Unwrap GPU index if needed
213
+ try:
214
+ raw_index = faiss.index_gpu_to_cpu(raw_index)
215
+ except Exception:
216
+ pass
217
+
218
+ if isinstance(raw_index, faiss.IndexHNSWFlat) or "hnsw" in self.index_factory.lower():
219
+ params = faiss.SearchParametersHNSW()
220
+ params.sel = self._id_selector
221
+ # Higher efSearch needed when filtering a sparse subset
222
+ params.efSearch = max(2048, effective_k * 40)
223
+ else:
224
+ params = faiss.SearchParameters()
225
+ params.sel = self._id_selector
226
+
227
+ distances, indices = self._index.search(query, effective_k, params=params)
228
+ else:
229
+ distances, indices = self._index.search(query, effective_k)
230
 
231
  results: list[tuple[str, float]] = []
232
  for dist, idx in zip(distances[0], indices[0]):
src/shop_rlve/server/__pycache__/openenv.cpython-311.pyc CHANGED
Binary files a/src/shop_rlve/server/__pycache__/openenv.cpython-311.pyc and b/src/shop_rlve/server/__pycache__/openenv.cpython-311.pyc differ
 
src/shop_rlve/server/openenv.py CHANGED
@@ -278,6 +278,17 @@ class ShopRLVEEnv:
278
  "ShopRLVEEnv: loaded pre-built FAISS index from %s (%d vectors)",
279
  faiss_index_path, len(self._vector_index),
280
  )
 
 
 
 
 
 
 
 
 
 
 
281
  except (ImportError, FileNotFoundError) as exc:
282
  logger.error("Failed to load FAISS index from %s: %s", faiss_index_path, exc)
283
  raise
 
278
  "ShopRLVEEnv: loaded pre-built FAISS index from %s (%d vectors)",
279
  faiss_index_path, len(self._vector_index),
280
  )
281
+
282
+ # If the index covers more products than loaded, restrict
283
+ # FAISS search to only the loaded product IDs so that
284
+ # results are guaranteed to exist in products_by_id.
285
+ if len(self._vector_index) > len(self._products):
286
+ loaded_ids = {p.id for p in self._products}
287
+ self._vector_index.set_allowed_ids(loaded_ids)
288
+ logger.info(
289
+ "ShopRLVEEnv: restricted FAISS search to %d loaded products",
290
+ len(loaded_ids),
291
+ )
292
  except (ImportError, FileNotFoundError) as exc:
293
  logger.error("Failed to load FAISS index from %s: %s", faiss_index_path, exc)
294
  raise