dbgraph 0.1.0__py3-none-any.whl

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 (42) hide show
  1. dbgraph/__init__.py +21 -0
  2. dbgraph/application.py +70 -0
  3. dbgraph/builder/__init__.py +0 -0
  4. dbgraph/builder/graph_builder.py +22 -0
  5. dbgraph/builder/sql_graph_builder.py +245 -0
  6. dbgraph/descriptor/__init__.py +0 -0
  7. dbgraph/descriptor/graph_descriptor.py +68 -0
  8. dbgraph/descriptor/langchain_graph_descriptor.py +74 -0
  9. dbgraph/descriptor/prompt_templates.py +80 -0
  10. dbgraph/entity/__init__.py +0 -0
  11. dbgraph/entity/aspect.py +153 -0
  12. dbgraph/entity/asset.py +26 -0
  13. dbgraph/entity/asset_type.py +13 -0
  14. dbgraph/entity/dbgraph.py +152 -0
  15. dbgraph/entity/link.py +37 -0
  16. dbgraph/entity/link_type.py +13 -0
  17. dbgraph/entity/rdbgraph.py +76 -0
  18. dbgraph/event/__init__.py +0 -0
  19. dbgraph/event/event.py +20 -0
  20. dbgraph/event/event_bus.py +61 -0
  21. dbgraph/event/event_handler.py +11 -0
  22. dbgraph/io/__init__.py +0 -0
  23. dbgraph/io/graph_loader.py +11 -0
  24. dbgraph/io/graph_writer.py +11 -0
  25. dbgraph/io/json_graph_loader.py +160 -0
  26. dbgraph/io/json_graph_writer.py +17 -0
  27. dbgraph/persistent/__init__.py +0 -0
  28. dbgraph/persistent/graph_persistent.py +98 -0
  29. dbgraph/persistent/models.py +222 -0
  30. dbgraph/persistent/sql_graph_persistent.py +388 -0
  31. dbgraph/render/__init__.py +0 -0
  32. dbgraph/render/graph_renderer.py +11 -0
  33. dbgraph/render/markdown_renderer.py +111 -0
  34. dbgraph/render/text_renderer.py +11 -0
  35. dbgraph/search/__init__.py +0 -0
  36. dbgraph/search/bm25_search_engine.py +33 -0
  37. dbgraph/search/search_engine.py +15 -0
  38. dbgraph/utils/__init__.py +0 -0
  39. dbgraph/utils/singleton.py +37 -0
  40. dbgraph-0.1.0.dist-info/METADATA +165 -0
  41. dbgraph-0.1.0.dist-info/RECORD +42 -0
  42. dbgraph-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,388 @@
1
+ from dataclasses import asdict, dataclass
2
+
3
+ from sqlalchemy import create_engine, delete, select, update
4
+ from sqlalchemy.orm import Session
5
+
6
+ from dbgraph.entity.aspect import (
7
+ Aspect,
8
+ RCategoricalStatistics,
9
+ RColumnSchemaAspect,
10
+ RColumnStatisticsAspect,
11
+ RForeignKeyAspect,
12
+ RNumericalStatistics,
13
+ RTableSchemaAspect,
14
+ RTableStatisticsAspect,
15
+ SemanticAspect,
16
+ )
17
+ from dbgraph.entity.asset import Asset
18
+ from dbgraph.entity.asset_type import AssetType
19
+ from dbgraph.entity.dbgraph import DatabaseGraph
20
+ from dbgraph.entity.link import Link
21
+ from dbgraph.entity.link_type import LinkType
22
+ from dbgraph.persistent.graph_persistent import GraphPersistent
23
+ from dbgraph.persistent.models import (
24
+ AssetAspectModel,
25
+ AssetModel,
26
+ Base,
27
+ GraphModel,
28
+ LinkAspectModel,
29
+ LinkModel,
30
+ )
31
+
32
+
33
+ @dataclass
34
+ class SQLGraphPersistent(GraphPersistent):
35
+ """Implementation of GraphPersistent using SQL databases and SQLAlchemy"""
36
+
37
+ db_uri: str
38
+
39
+ def __post_init__(self):
40
+ self.engine = create_engine(self.db_uri)
41
+ Base.metadata.create_all(bind=self.engine)
42
+
43
+ def create_graph(self, graph_id: str, graph_name: str):
44
+ graph_model = GraphModel(graph_id=graph_id, name=graph_name, completed=False)
45
+ with Session(self.engine) as session:
46
+ session.add(graph_model)
47
+ session.commit()
48
+
49
+ def delete_graph(self, graph_id: str):
50
+ with Session(self.engine) as session:
51
+ session.execute(delete(GraphModel).where(GraphModel.graph_id == graph_id))
52
+ session.commit()
53
+
54
+ def insert_assets(self, assets: list[Asset], graph_id: str):
55
+ """Insert the assets into database"""
56
+ models = [
57
+ AssetModel(
58
+ asset_id=asset.asset_id,
59
+ name=asset.name,
60
+ asset_type=asset.type.value,
61
+ graph_id=graph_id,
62
+ )
63
+ for asset in assets
64
+ ]
65
+ aspects = []
66
+ for asset in assets:
67
+ for aspect_type, aspect in asset.aspects.items():
68
+ aspect = AssetAspectModel(
69
+ asset_id=asset.asset_id,
70
+ name=aspect.name,
71
+ type=aspect_type,
72
+ json_aspect=asdict(aspect),
73
+ )
74
+ aspects.append(aspect)
75
+ with Session(self.engine) as session:
76
+ session.add_all(models)
77
+ session.add_all(aspects)
78
+ session.commit()
79
+
80
+ def insert_asset(self, asset: Asset, graph_id: str):
81
+ """Insert a single asset"""
82
+ model = AssetModel(
83
+ asset_id=asset.asset_id,
84
+ name=asset.name,
85
+ asset_type=asset.type.value,
86
+ graph_id=graph_id,
87
+ )
88
+ with Session(self.engine) as session:
89
+ session.add(model)
90
+ session.commit()
91
+ self.insert_asset_aspects(asset.asset_id, asset.aspects)
92
+
93
+ def insert_links(self, links: list[Link], graph_id: str):
94
+ """Insert links into database"""
95
+ models = [
96
+ LinkModel(
97
+ link_id=link.link_id,
98
+ name=link.name,
99
+ link_type=link.type.value,
100
+ source_id=link.source_id,
101
+ destination_id=link.destination_id,
102
+ graph_id=graph_id,
103
+ )
104
+ for link in links
105
+ ]
106
+ aspects = []
107
+ for link in links:
108
+ for aspect_type, aspect in link.aspects.items():
109
+ link_aspect = LinkAspectModel(
110
+ link_id=link.link_id,
111
+ name=aspect.name,
112
+ type=aspect_type,
113
+ json_aspect=asdict(aspect),
114
+ )
115
+ aspects.append(link_aspect)
116
+ with Session(self.engine) as session:
117
+ session.add_all(models)
118
+ session.add_all(aspects)
119
+ session.commit()
120
+
121
+ def insert_link(self, link: Link, graph_id: str):
122
+ """Insert a single link into the database"""
123
+ model = LinkModel(
124
+ link_id=link.link_id,
125
+ name=link.name,
126
+ link_type=link.type.value,
127
+ source_id=link.source_id,
128
+ destination_id=link.destination_id,
129
+ graph_id=graph_id,
130
+ )
131
+ with Session(self.engine) as session:
132
+ session.add(model)
133
+ session.commit()
134
+ self.insert_link_aspects(link.link_id, link.aspects)
135
+
136
+ def get_link(self, link_id: str, graph_id: str) -> Link:
137
+ with Session(self.engine) as session:
138
+ model = session.scalar(
139
+ select(LinkModel).where(
140
+ LinkModel.link_id == link_id and LinkModel.graph_id == graph_id
141
+ )
142
+ )
143
+ if model is None:
144
+ raise KeyError(f"Link with id={link_id} not found in graph={graph_id}")
145
+ aspects = self.get_aspects_of_link(link_id)
146
+ return Link(
147
+ link_id=link_id,
148
+ name=model.name,
149
+ source_id=model.source_id,
150
+ destination_id=model.destination_id,
151
+ aspects=aspects,
152
+ type=LinkType(model.link_type),
153
+ )
154
+
155
+ def remove_assets(self, assets_ids: list[str], graph_id: str):
156
+ with Session(self.engine) as session:
157
+ session.execute(
158
+ delete(AssetModel).where(AssetModel.asset_id.in_(assets_ids))
159
+ )
160
+ session.commit()
161
+
162
+ def remove_asset(self, asset_id: str, graph_id: str):
163
+ """Remove the asset from the database and return it"""
164
+ with Session(self.engine) as session:
165
+ session.execute(delete(AssetModel).where(AssetModel.asset_id == asset_id))
166
+ session.commit()
167
+
168
+ def get_asset(self, asset_id: str, graph_id: str) -> Asset:
169
+ with Session(self.engine) as session:
170
+ model = session.scalar(
171
+ select(AssetModel).where(
172
+ AssetModel.asset_id == asset_id and AssetModel.graph_id == graph_id
173
+ )
174
+ )
175
+ if model is None:
176
+ raise KeyError(f"Asset with id={asset_id} not found in graph={graph_id}")
177
+ aspects = self.get_aspects_of_asset(asset_id)
178
+ return Asset(
179
+ asset_id=asset_id,
180
+ name=model.name,
181
+ type=AssetType(model.asset_type),
182
+ aspects=aspects,
183
+ )
184
+
185
+ def remove_links(self, links_ids: list[str], graph_id: str):
186
+ """Remove the links and return them"""
187
+ with Session(self.engine) as session:
188
+ session.execute(delete(LinkModel).where(LinkModel.link_id.in_(links_ids)))
189
+ session.commit()
190
+
191
+ def remove_link(self, link_id: str, graph_id: str):
192
+ """Remove the link and return it"""
193
+ with Session(self.engine) as session:
194
+ session.execute(delete(LinkModel).where(LinkModel.link_id == link_id))
195
+ session.commit()
196
+
197
+ def insert_asset_aspects(self, asset_id: str, aspects: dict[str, Aspect]):
198
+ """Insert aspects into asset"""
199
+ models = [
200
+ AssetAspectModel(
201
+ name=aspect.name,
202
+ type=aspect_type,
203
+ asset_id=asset_id,
204
+ json_aspect=asdict(aspect),
205
+ )
206
+ for aspect_type, aspect in aspects.items()
207
+ ]
208
+ with Session(self.engine) as session:
209
+ session.add_all(models)
210
+ session.commit()
211
+
212
+ def insert_link_aspects(self, link_id: str, aspects: dict[str, Aspect]):
213
+ """Insert aspects into link"""
214
+ models = [
215
+ LinkAspectModel(
216
+ name=aspect.name,
217
+ type=aspect_type,
218
+ link_id=link_id,
219
+ json_aspect=asdict(aspect),
220
+ )
221
+ for aspect_type, aspect in aspects.items()
222
+ ]
223
+ with Session(self.engine) as session:
224
+ session.add_all(models)
225
+ session.commit()
226
+
227
+ def _parse_asset_aspect_model(
228
+ self, model: AssetAspectModel, asset_type: AssetType
229
+ ) -> Aspect:
230
+ """Turn SQL Alchemy model to Aspect"""
231
+ json_aspect = model.json_aspect
232
+ match model.type:
233
+ case "schema_properties":
234
+ if asset_type == AssetType.RTABLE:
235
+ aspect = RTableSchemaAspect(**json_aspect)
236
+ elif asset_type == AssetType.RCOLUMN:
237
+ aspect = RColumnSchemaAspect(**json_aspect)
238
+ else:
239
+ raise NotImplementedError()
240
+ case "statistical_properties":
241
+ if asset_type == AssetType.RTABLE:
242
+ aspect = RTableStatisticsAspect(**json_aspect)
243
+ elif asset_type == AssetType.RCOLUMN:
244
+ if json_aspect["numerical_stats"] is not None:
245
+ numerical_aspect = RNumericalStatistics(
246
+ **json_aspect["numerical_stats"]
247
+ )
248
+ categorical_aspect = None
249
+ elif json_aspect["categorical_stats"] is not None:
250
+ categorical_aspect = RCategoricalStatistics(
251
+ **json_aspect["categorical_stats"]
252
+ )
253
+ numerical_aspect = None
254
+ else:
255
+ raise RuntimeError(
256
+ "Neither categorical or numerical stats exists"
257
+ )
258
+ aspect = RColumnStatisticsAspect(
259
+ name=json_aspect["name"],
260
+ non_null_count=json_aspect["non_null_count"],
261
+ null_count=json_aspect["null_count"],
262
+ categorical_stats=categorical_aspect,
263
+ numerical_stats=numerical_aspect,
264
+ )
265
+ else:
266
+ raise NotImplementedError()
267
+ case "semantic_properties":
268
+ aspect = SemanticAspect(**json_aspect)
269
+ case _:
270
+ raise NotImplementedError()
271
+ return aspect
272
+
273
+ def get_aspects_of_asset(self, asset_id: str) -> dict[str, Aspect]:
274
+ """Get the aspects of an asset"""
275
+ with Session(self.engine) as session:
276
+ models = session.scalars(
277
+ select(AssetAspectModel).where(AssetAspectModel.asset_id == asset_id)
278
+ )
279
+ asset = session.scalar(
280
+ select(AssetModel).where(AssetModel.asset_id == asset_id)
281
+ )
282
+ if asset is None:
283
+ raise KeyError(f"Asset with id={asset_id} not found!")
284
+ asset_type = AssetType(asset.asset_type)
285
+ answers: dict[str, Aspect] = {}
286
+ for model in models:
287
+ answers[model.type] = self._parse_asset_aspect_model(model, asset_type)
288
+ return answers
289
+
290
+ def _parse_link_aspect_model(self, model: LinkAspectModel) -> Aspect:
291
+ """Turn SQL Alchemy model to Link Aspect"""
292
+ json_aspect = model.json_aspect
293
+ match model.type:
294
+ case "foreign_key_properties":
295
+ aspect = RForeignKeyAspect(**json_aspect)
296
+ case _:
297
+ raise NotImplementedError()
298
+ return aspect
299
+
300
+ def get_aspects_of_link(self, link_id: str) -> dict[str, Aspect]:
301
+ """Get the aspects of a link"""
302
+ with Session(self.engine) as session:
303
+ models = session.scalars(
304
+ select(LinkAspectModel).where(LinkAspectModel.link_id == link_id)
305
+ )
306
+ link = session.scalar(select(LinkModel).where(LinkModel.link_id == link_id))
307
+ if link is None:
308
+ raise KeyError(f"Link with id={link_id} not found!")
309
+ answers: dict[str, Aspect] = {}
310
+ for model in models:
311
+ answers[model.type] = self._parse_link_aspect_model(model)
312
+ return answers
313
+
314
+ def get_assets(self, graph_id: str) -> list[Asset]:
315
+ with Session(self.engine) as session:
316
+ models = session.scalars(
317
+ select(AssetModel).where(AssetModel.graph_id == graph_id)
318
+ )
319
+ assets = []
320
+ for model in models:
321
+ aspects = {}
322
+ for aspect_model in model.asset_aspects:
323
+ aspects[aspect_model.type] = self._parse_asset_aspect_model(
324
+ aspect_model, AssetType(model.asset_type)
325
+ )
326
+ asset = Asset(
327
+ asset_id=model.asset_id,
328
+ name=model.name,
329
+ type=AssetType(model.asset_type),
330
+ aspects=aspects,
331
+ )
332
+ assets.append(asset)
333
+ return assets
334
+
335
+ def get_links(self, graph_id: str) -> list[Link]:
336
+ with Session(self.engine) as session:
337
+ models = session.scalars(
338
+ select(LinkModel).where(LinkModel.graph_id == graph_id)
339
+ )
340
+ links = []
341
+ for model in models:
342
+ aspects = {}
343
+ for aspect_model in model.link_aspects:
344
+ aspects[aspect_model.type] = self._parse_link_aspect_model(
345
+ aspect_model
346
+ )
347
+ link = Link(
348
+ link_id=model.link_id,
349
+ name=model.name,
350
+ type=LinkType(model.link_type),
351
+ aspects=aspects,
352
+ source_id=model.source_id,
353
+ destination_id=model.destination_id,
354
+ )
355
+ links.append(link)
356
+ return links
357
+
358
+ def load_graph(self, graph_id: str) -> DatabaseGraph:
359
+ """Load the full database graph"""
360
+ links = self.get_links(graph_id)
361
+ assets = self.get_assets(graph_id)
362
+ return DatabaseGraph(assets, links)
363
+
364
+ def save_graph(self, graph: DatabaseGraph, graph_id: str):
365
+ """Save the graph into database"""
366
+
367
+ self.insert_assets(graph.assets, graph_id)
368
+ self.insert_links(graph.links, graph_id)
369
+
370
+ def set_graph_state(self, graph_id: str, completed: bool):
371
+ """Mark the graph as completely built and saved in the database or hasn't been finished building yet"""
372
+ stmt = (
373
+ update(GraphModel)
374
+ .where(GraphModel.graph_id == graph_id)
375
+ .values(completed=completed)
376
+ )
377
+ with Session(self.engine) as session:
378
+ session.execute(stmt)
379
+ session.commit()
380
+
381
+ def get_graph_info(self, graph_id: str) -> tuple[str, str, bool]:
382
+ with Session(self.engine) as session:
383
+ model = session.scalar(
384
+ select(GraphModel).where(GraphModel.graph_id == graph_id)
385
+ )
386
+ if model is None:
387
+ raise KeyError(f"Graph with id={graph_id} not found")
388
+ return model.graph_id, model.name, model.completed
File without changes
@@ -0,0 +1,11 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from dbgraph.entity.dbgraph import DatabaseGraph
4
+
5
+
6
+ class GraphRenderer(ABC):
7
+ """Interface for rendering the graph"""
8
+
9
+ @abstractmethod
10
+ def render(self, graph: DatabaseGraph):
11
+ """Render the graph, store data inside the class"""
@@ -0,0 +1,111 @@
1
+ from dataclasses import dataclass
2
+
3
+ from dbgraph import AssetType, LinkType
4
+ from dbgraph.entity.aspect import (
5
+ RColumnSchemaAspect,
6
+ RColumnStatisticsAspect,
7
+ RForeignKeyAspect,
8
+ RTableSchemaAspect,
9
+ RTableStatisticsAspect,
10
+ SemanticAspect,
11
+ )
12
+ from dbgraph.entity.asset import Asset
13
+ from dbgraph.entity.dbgraph import DatabaseGraph
14
+ from dbgraph.entity.link import Link
15
+ from dbgraph.entity.rdbgraph import RDatabaseGraph
16
+ from dbgraph.render.text_renderer import TextRenderer
17
+
18
+
19
+ @dataclass
20
+ class MarkdownRenderer(TextRenderer):
21
+ """Render the graph to Markdown format"""
22
+
23
+ categorical_value_max_char: int = 50
24
+
25
+ def __post_init__(self):
26
+ self.content = ""
27
+
28
+ def _dump_table(self, asset: Asset) -> str:
29
+ if asset.type != AssetType.RTABLE:
30
+ raise TypeError(f"Asset of type {asset.type} is not table type")
31
+ if "semantic_properties" in asset.aspects and isinstance(
32
+ asset.aspects["semantic_properties"], SemanticAspect
33
+ ):
34
+ answer = f"""# Table `{asset.name}`
35
+ {asset.aspects["semantic_properties"].description}
36
+ - ID: {asset.asset_id}"""
37
+ else:
38
+ answer = f"""# Table `{asset.name}`
39
+ - ID: {asset.asset_id}"""
40
+ for aspect_name, aspect in asset.aspects.items():
41
+ if isinstance(aspect, RTableStatisticsAspect):
42
+ answer += f"\n- `{aspect_name}`: {aspect.num_rows} row(s), {aspect.num_columns} columns"
43
+ elif isinstance(aspect, RTableSchemaAspect):
44
+ answer += f"\n- `{aspect_name}`:"
45
+ answer += f"\n\t- Primary keys: {', '.join(aspect.pks)}"
46
+ return answer
47
+
48
+ def _dump_column(self, asset: Asset) -> str:
49
+ header = f"""## Column `{asset.name}`"""
50
+ if asset.type != AssetType.RCOLUMN:
51
+ raise TypeError(f"Asset of type {asset.type} is not column type")
52
+ if "semantic_properties" in asset.aspects and isinstance(
53
+ asset.aspects["semantic_properties"], SemanticAspect
54
+ ):
55
+ answer = f"""{asset.aspects["semantic_properties"].description}
56
+ - ID: {asset.asset_id}"""
57
+ else:
58
+ answer = f"""- ID: {asset.asset_id}"""
59
+ for aspect_name, aspect in asset.aspects.items():
60
+ if isinstance(aspect, RColumnSchemaAspect):
61
+ answer += f"\n- `{aspect_name}`: dtype={aspect.dtype}, nullable={aspect.is_nullable}"
62
+ if aspect.is_pk:
63
+ header += "(Primary Key)"
64
+ elif isinstance(aspect, RColumnStatisticsAspect):
65
+ if aspect.numerical_stats is not None:
66
+ answer += f"\n- Numerical stats: min={aspect.numerical_stats.min}, max={aspect.numerical_stats.max}, mean={aspect.numerical_stats.mean}"
67
+ elif aspect.categorical_stats is not None:
68
+ answer += "\n- Top 10 Value counts: "
69
+ for key, value in aspect.categorical_stats.value_counts.items():
70
+ if len(key) > self.categorical_value_max_char:
71
+ key = key[: self.categorical_value_max_char] + "..."
72
+ answer += f"\n\t- {key}: {value}"
73
+ return header + "\n" + answer
74
+
75
+ def _dump_fk(self, link: Link, graph: DatabaseGraph) -> str:
76
+ if link.type != LinkType.FOREIGN_KEY:
77
+ raise TypeError(f"Link of type {link.type} is not foreign key type")
78
+ src_name = graph.get_asset(link.source_id).name
79
+ dst_name = graph.get_asset(link.destination_id).name
80
+ if "foreign_key_properties" in link.aspects and isinstance(
81
+ link.aspects["foreign_key_properties"], RForeignKeyAspect
82
+ ):
83
+ aspect = link.aspects["foreign_key_properties"]
84
+ answer = (
85
+ f"- {src_name}.{aspect.from_column} -> {dst_name}.{aspect.to_column}"
86
+ )
87
+ else:
88
+ answer = f"- {src_name} -> {dst_name}"
89
+
90
+ return answer
91
+
92
+ def render(self, graph: DatabaseGraph):
93
+ graph = RDatabaseGraph.from_graph(graph)
94
+ tables = graph.get_tables()
95
+ answer = ""
96
+ for table in tables:
97
+ answer += f"\n{self._dump_table(table)}"
98
+ columns = graph.get_columns(table.asset_id)
99
+ for column in columns:
100
+ answer += f"\n{self._dump_column(column)}"
101
+ foreign_keys = graph.get_foreign_keys()
102
+ if len(foreign_keys) > 0:
103
+ answer += "\n# List of Foreign Key Constraints"
104
+ for fk in foreign_keys:
105
+ answer += f"\n{self._dump_fk(fk, graph)}"
106
+ self.content = answer
107
+
108
+ def get_content(self) -> str:
109
+ if not self.content:
110
+ raise RuntimeError("Content is empty. Please run `render()` first!")
111
+ return self.content
@@ -0,0 +1,11 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from dbgraph.render.graph_renderer import GraphRenderer
4
+
5
+
6
+ class TextRenderer(GraphRenderer, ABC):
7
+ """Render the graph to text-based representation"""
8
+
9
+ @abstractmethod
10
+ def get_content(self) -> str:
11
+ """Get the text data"""
File without changes
@@ -0,0 +1,33 @@
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+
4
+ import bm25s
5
+ import Stemmer
6
+
7
+ from dbgraph.entity.aspect import SemanticAspect
8
+ from dbgraph.search.search_engine import SearchEngine
9
+
10
+
11
+ @dataclass
12
+ class BM25SearchEngine(SearchEngine):
13
+ """Implementation of SearchEngine using BM25"""
14
+
15
+ index_path: Path
16
+
17
+ def search(self, text: str, top_k: int = 10) -> list[str]:
18
+ retriever = bm25s.BM25.load(self.index_path, load_corpus=True)
19
+ stemmer = Stemmer.Stemmer("english")
20
+ tokens = bm25s.tokenize(text, stopwords="en", stemmer=stemmer)
21
+ results, _ = retriever.retrieve(tokens, k=top_k)
22
+ results = results[0]
23
+ return [r["id"] for r in results]
24
+
25
+ def index(self, semantic_aspects: dict[str, SemanticAspect]):
26
+ stemmer = Stemmer.Stemmer("english")
27
+ corpus = [{"id": k, "text": v.description} for k, v in semantic_aspects.items()]
28
+ tokens = bm25s.tokenize(
29
+ [doc["text"] for doc in corpus], stopwords="en", stemmer=stemmer
30
+ )
31
+ retriever = bm25s.BM25(corpus=corpus)
32
+ retriever.index(tokens)
33
+ retriever.save(self.index_path)
@@ -0,0 +1,15 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from dbgraph.entity.aspect import SemanticAspect
4
+
5
+
6
+ class SearchEngine(ABC):
7
+ """Interface for search engines"""
8
+
9
+ @abstractmethod
10
+ def search(self, text: str, top_k: int = 10) -> list[str]:
11
+ """Search relevant resources and return list of IDs"""
12
+
13
+ @abstractmethod
14
+ def index(self, semantic_aspects: dict[str, SemanticAspect]):
15
+ pass
File without changes
@@ -0,0 +1,37 @@
1
+ import typing
2
+ from threading import Lock
3
+
4
+
5
+ class SingletonMeta(type):
6
+ """
7
+ This is a thread-safe implementation of Singleton.
8
+ """
9
+
10
+ _instances: typing.ClassVar = {}
11
+
12
+ _lock: Lock = Lock()
13
+ """
14
+ We now have a lock object that will be used to synchronize threads during
15
+ first access to the Singleton.
16
+ """
17
+
18
+ def __call__(cls, *args, **kwargs):
19
+ """
20
+ Possible changes to the value of the `__init__` argument do not affect
21
+ the returned instance.
22
+ """
23
+ # Now, imagine that the program has just been launched. Since there's no
24
+ # Singleton instance yet, multiple threads can simultaneously pass the
25
+ # previous conditional and reach this point almost at the same time. The
26
+ # first of them will acquire lock and will proceed further, while the
27
+ # rest will wait here.
28
+ with cls._lock:
29
+ # The first thread to acquire the lock, reaches this conditional,
30
+ # goes inside and creates the Singleton instance. Once it leaves the
31
+ # lock block, a thread that might have been waiting for the lock
32
+ # release may then enter this section. But since the Singleton field
33
+ # is already initialized, the thread won't create a new object.
34
+ if cls not in cls._instances:
35
+ instance = super().__call__(*args, **kwargs)
36
+ cls._instances[cls] = instance
37
+ return cls._instances[cls]