Archie0099 commited on
Commit
0f49634
·
verified ·
1 Parent(s): 97ff337

Speed up knowledge graph builds by extracting pages concurrently

Browse files
Files changed (1) hide show
  1. pipeline/kg.py +42 -7
pipeline/kg.py CHANGED
@@ -44,6 +44,7 @@ import time
44
  import urllib.error
45
  import urllib.request
46
  from collections import deque
 
47
  from dataclasses import dataclass, field
48
  from typing import Optional
49
 
@@ -98,6 +99,19 @@ _RETRY_STATUSES = frozenset({429, 503})
98
  _RETRY_ATTEMPTS = 4
99
  _RETRY_BASE_DELAY = 1.5 # seconds; doubled each attempt
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  # Instruction for triple extraction. We ask for typed entities AND triples and
102
  # force a JSON response via responseSchema for deterministic parsing.
103
  _TRIPLE_PROMPT = (
@@ -906,13 +920,21 @@ def build_graph(
906
  api_key : str
907
  A Gemini API key (required; this is the opt-in online feature).
908
  triple_model, embed_model : str, optional
909
- Model overrides; default to Flash + text-embedding-004.
 
910
  max_pages : int, optional
911
  Cap the number of non-empty pages processed (cost control). None = all.
912
  embed : bool
913
  If True (default) also embed the nodes so semantic search works; if
914
  False the graph supports lexical search only (cheaper).
915
 
 
 
 
 
 
 
 
916
  Returns a graph (possibly empty if the document yields no facts). Raises a
917
  single-line RuntimeError only for a missing key or a hard Gemini failure.
918
  """
@@ -928,15 +950,28 @@ def build_graph(
928
 
929
  graph = KnowledgeGraph()
930
  last_error: Optional[Exception] = None
931
- for page_no, text in usable:
932
- # Isolate each page: a single rate-limited / safety-blocked page (common
933
- # on the free tier) must NOT discard every page already processed (and
934
- # its API spend). Skip it and keep going.
 
 
 
 
 
 
 
 
935
  try:
936
- data = extract_triples(text, api_key=api_key, model=triple_model, timeout=timeout)
937
  except RuntimeError as exc:
938
- graph.pages_failed += 1
939
  last_error = exc
 
 
 
 
 
940
  continue
941
  graph.pages_built += 1
942
  # Register typed entities first so types are known, then triples.
 
44
  import urllib.error
45
  import urllib.request
46
  from collections import deque
47
+ from concurrent.futures import ThreadPoolExecutor
48
  from dataclasses import dataclass, field
49
  from typing import Optional
50
 
 
99
  _RETRY_ATTEMPTS = 4
100
  _RETRY_BASE_DELAY = 1.5 # seconds; doubled each attempt
101
 
102
+ # Build scalability: per-page triple extraction is one (I/O-bound) Gemini call
103
+ # each, so they're fanned across this SHARED, bounded pool instead of running
104
+ # strictly sequentially — a multi-page build is ~Nx faster on a key with real
105
+ # throughput. The pool is module-level on purpose: being shared, it also caps the
106
+ # TOTAL number of concurrent Gemini calls across simultaneous builds/users, so one
107
+ # big document can't blow the rate limit for everyone. Overshoot of the provider's
108
+ # rate limit is absorbed by the 429/503 backoff in _post_json. Graph mutation stays
109
+ # single-threaded (results are collected, then applied in deterministic page order).
110
+ _BUILD_CONCURRENCY = 5
111
+ _EXTRACT_POOL = ThreadPoolExecutor(
112
+ max_workers=_BUILD_CONCURRENCY, thread_name_prefix="kg-extract"
113
+ )
114
+
115
  # Instruction for triple extraction. We ask for typed entities AND triples and
116
  # force a JSON response via responseSchema for deterministic parsing.
117
  _TRIPLE_PROMPT = (
 
920
  api_key : str
921
  A Gemini API key (required; this is the opt-in online feature).
922
  triple_model, embed_model : str, optional
923
+ Model overrides; default to Flash + a discovered embedding model
924
+ (``_pick_embedding_model``, prefers gemini-embedding-001).
925
  max_pages : int, optional
926
  Cap the number of non-empty pages processed (cost control). None = all.
927
  embed : bool
928
  If True (default) also embed the nodes so semantic search works; if
929
  False the graph supports lexical search only (cheaper).
930
 
931
+ Per-page triple extraction is fanned across a shared bounded pool
932
+ (``_EXTRACT_POOL``), so a multi-page build is much faster on a key with real
933
+ throughput; the shared pool also caps total concurrent Gemini calls. Each
934
+ page is isolated — a single failed page is skipped and counted
935
+ (``pages_failed``); only an all-pages failure raises. Embedding failure is
936
+ non-fatal (``embed_error`` set, lexical search retained).
937
+
938
  Returns a graph (possibly empty if the document yields no facts). Raises a
939
  single-line RuntimeError only for a missing key or a hard Gemini failure.
940
  """
 
950
 
951
  graph = KnowledgeGraph()
952
  last_error: Optional[Exception] = None
953
+
954
+ # Fan the per-page Gemini calls across the shared pool (concurrency), then
955
+ # build the graph single-threaded below in PAGE ORDER so node ids stay
956
+ # deterministic and the adjacency dicts are never mutated from two threads.
957
+ def _extract_one(text):
958
+ return extract_triples(text, api_key=api_key, model=triple_model, timeout=timeout)
959
+
960
+ futures = [(page_no, _EXTRACT_POOL.submit(_extract_one, text)) for page_no, text in usable]
961
+ results: dict = {}
962
+ for page_no, fut in futures:
963
+ # Isolate each page: one rate-limited / safety-blocked page (common on the
964
+ # free tier) must NOT discard every other page (and its API spend).
965
  try:
966
+ results[page_no] = fut.result()
967
  except RuntimeError as exc:
968
+ results[page_no] = exc
969
  last_error = exc
970
+
971
+ for page_no, _text in usable:
972
+ data = results.get(page_no)
973
+ if not isinstance(data, dict): # a skipped page (exception) or missing
974
+ graph.pages_failed += 1
975
  continue
976
  graph.pages_built += 1
977
  # Register typed entities first so types are known, then triples.