feat(coins): add sample-query endpoint and resolve Freebase MIDs to names
Browse filesAdd GET /coins/datasets/{id}/sample-query that produces structurally
valid KG queries for all 7 query structures by delegating to
Query.instantiate() from the research code rather than hand-rolling
graph walks. Returns anchors, variables, relations, and a known target
entity — guaranteed to form a real path/intersection in the training KG.
Also resolve Freebase MID identifiers (e.g. /m/06rf7) to human-readable
names (e.g. Schleswig Holstein) using the ent2name.txt lookup table:
- Entity and relation search endpoints now return a `label` field with
cleaned display names alongside the raw `name`
- Search filtering matches against both raw names and cleaned labels,
so searching "Chicago" finds /m/01yjl (Chicago Cubs)
- sample-query and sample-triples responses carry resolved labels
- docs/api.yaml +96 -0
- docs/postman/collection.json +18 -0
- src/backend/README.md +1 -0
- src/backend/api/services/registry.py +120 -2
- src/backend/api/urls.py +2 -0
- src/backend/api/utils.py +26 -1
- src/backend/api/views/coins.py +33 -2
- src/research/COINs-KGGeneration/data/FB15k-237/ent2name.txt +0 -0
|
@@ -190,6 +190,58 @@ paths:
|
|
| 190 |
"404":
|
| 191 |
$ref: "#/components/responses/NotFound"
|
| 192 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
/coins/models:
|
| 194 |
get:
|
| 195 |
operationId: getCoinsModels
|
|
@@ -914,6 +966,50 @@ components:
|
|
| 914 |
tail:
|
| 915 |
$ref: "#/components/schemas/CoinsEntity"
|
| 916 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 917 |
CoinsModelsResponse:
|
| 918 |
type: object
|
| 919 |
required: [models]
|
|
|
|
| 190 |
"404":
|
| 191 |
$ref: "#/components/responses/NotFound"
|
| 192 |
|
| 193 |
+
/coins/datasets/{dataset_id}/sample-query:
|
| 194 |
+
get:
|
| 195 |
+
operationId: getCoinsSampleQuery
|
| 196 |
+
tags: [coins]
|
| 197 |
+
summary: Sample a valid KG query
|
| 198 |
+
description: |
|
| 199 |
+
Walks the training knowledge graph to produce a structurally valid query
|
| 200 |
+
for the given query structure. Each returned query contains anchor entities,
|
| 201 |
+
relations, and a known target entity — guaranteed to form a real path or
|
| 202 |
+
intersection in the KG. This is the recommended way to populate the query
|
| 203 |
+
builder UI with meaningful defaults. For simple 1p (single-hop) queries the
|
| 204 |
+
result is equivalent to a random triple; for multi-hop and intersection
|
| 205 |
+
structures (2p, 3p, 2i, 3i, ip, pi) the sampler walks backward from a
|
| 206 |
+
random target through actual KG edges.
|
| 207 |
+
parameters:
|
| 208 |
+
- $ref: "#/components/parameters/CoinsDatasetId"
|
| 209 |
+
- name: query_structure
|
| 210 |
+
in: query
|
| 211 |
+
required: true
|
| 212 |
+
schema:
|
| 213 |
+
$ref: "#/components/schemas/CoinsQueryStructureEnum"
|
| 214 |
+
description: The query structure to sample for.
|
| 215 |
+
- name: count
|
| 216 |
+
in: query
|
| 217 |
+
schema:
|
| 218 |
+
type: integer
|
| 219 |
+
minimum: 1
|
| 220 |
+
maximum: 10
|
| 221 |
+
default: 1
|
| 222 |
+
description: Number of sample queries to return.
|
| 223 |
+
- name: seed
|
| 224 |
+
in: query
|
| 225 |
+
required: false
|
| 226 |
+
schema:
|
| 227 |
+
type: string
|
| 228 |
+
description: |
|
| 229 |
+
Optional sampling seed for deterministic results. Same
|
| 230 |
+
`(dataset_id, query_structure, count, seed)` always yields the
|
| 231 |
+
same queries. Useful for day-stable prefills (e.g. seed by ISO date).
|
| 232 |
+
example: "2026-04-17"
|
| 233 |
+
responses:
|
| 234 |
+
"200":
|
| 235 |
+
description: Sampled queries
|
| 236 |
+
content:
|
| 237 |
+
application/json:
|
| 238 |
+
schema:
|
| 239 |
+
$ref: "#/components/schemas/CoinsSampleQueryResponse"
|
| 240 |
+
"400":
|
| 241 |
+
$ref: "#/components/responses/InvalidRequest"
|
| 242 |
+
"404":
|
| 243 |
+
$ref: "#/components/responses/NotFound"
|
| 244 |
+
|
| 245 |
/coins/models:
|
| 246 |
get:
|
| 247 |
operationId: getCoinsModels
|
|
|
|
| 966 |
tail:
|
| 967 |
$ref: "#/components/schemas/CoinsEntity"
|
| 968 |
|
| 969 |
+
CoinsSampleQueryResponse:
|
| 970 |
+
type: object
|
| 971 |
+
required: [dataset_id, query_structure, queries]
|
| 972 |
+
properties:
|
| 973 |
+
dataset_id:
|
| 974 |
+
$ref: "#/components/schemas/CoinsDatasetIdEnum"
|
| 975 |
+
query_structure:
|
| 976 |
+
$ref: "#/components/schemas/CoinsQueryStructureEnum"
|
| 977 |
+
queries:
|
| 978 |
+
type: array
|
| 979 |
+
description: |
|
| 980 |
+
Sampled queries. Each query is a valid walk through the training KG.
|
| 981 |
+
May return fewer than `count` if the graph is too sparse for the
|
| 982 |
+
requested structure (e.g. 3i requires a target with at least 3 in-edges).
|
| 983 |
+
items:
|
| 984 |
+
$ref: "#/components/schemas/CoinsSampledQuery"
|
| 985 |
+
|
| 986 |
+
CoinsSampledQuery:
|
| 987 |
+
type: object
|
| 988 |
+
required: [anchors, relations, target]
|
| 989 |
+
description: |
|
| 990 |
+
A structurally valid query instance. `anchors` and `relations` keys match the
|
| 991 |
+
node/edge IDs from `GET /coins/query-structures` for the given structure.
|
| 992 |
+
The `target` is the known answer entity.
|
| 993 |
+
properties:
|
| 994 |
+
anchors:
|
| 995 |
+
type: object
|
| 996 |
+
additionalProperties:
|
| 997 |
+
$ref: "#/components/schemas/CoinsEntity"
|
| 998 |
+
description: "Anchor entities keyed by node ID (e.g. `a`, `a1`, `a2`, `a3`)."
|
| 999 |
+
example:
|
| 1000 |
+
a1: { id: 123, name: "/m/06thjt", label: "06thjt" }
|
| 1001 |
+
a2: { id: 456, name: "/m/0d_kd", label: "0d_kd" }
|
| 1002 |
+
relations:
|
| 1003 |
+
type: object
|
| 1004 |
+
additionalProperties:
|
| 1005 |
+
$ref: "#/components/schemas/CoinsRelation"
|
| 1006 |
+
description: "Relations keyed by edge ID (e.g. `r1`, `r2`, `r3`)."
|
| 1007 |
+
example:
|
| 1008 |
+
r1: { id: 7, name: "/people/person/nationality", label: "person nationality" }
|
| 1009 |
+
r2: { id: 12, name: "/location/country/capital", label: "country capital" }
|
| 1010 |
+
target:
|
| 1011 |
+
$ref: "#/components/schemas/CoinsEntity"
|
| 1012 |
+
|
| 1013 |
CoinsModelsResponse:
|
| 1014 |
type: object
|
| 1015 |
required: [models]
|
|
@@ -146,6 +146,24 @@
|
|
| 146 |
"description": "Random sample triples from the dataset. Each triple has head/relation/tail entries with { id, name, label }; `label` is a dataset-specific display-friendly form of `name` (NELL strips `concept:` prefixes, Freebase strips `/m/`, WordNet drops the POS suffix). Pass an optional `seed` (any string) for deterministic sampling — e.g. seed by today's ISO date for a day-stable 'fact of the day' widget."
|
| 147 |
}
|
| 148 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
{
|
| 150 |
"name": "GET /coins/models",
|
| 151 |
"request": {
|
|
|
|
| 146 |
"description": "Random sample triples from the dataset. Each triple has head/relation/tail entries with { id, name, label }; `label` is a dataset-specific display-friendly form of `name` (NELL strips `concept:` prefixes, Freebase strips `/m/`, WordNet drops the POS suffix). Pass an optional `seed` (any string) for deterministic sampling — e.g. seed by today's ISO date for a day-stable 'fact of the day' widget."
|
| 147 |
}
|
| 148 |
},
|
| 149 |
+
{
|
| 150 |
+
"name": "GET /coins/datasets/{id}/sample-query",
|
| 151 |
+
"request": {
|
| 152 |
+
"method": "GET",
|
| 153 |
+
"header": [],
|
| 154 |
+
"url": {
|
| 155 |
+
"raw": "{{base_url}}/coins/datasets/nell/sample-query?query_structure=2i&count=1&seed=2026-04-17",
|
| 156 |
+
"host": ["{{base_url}}"],
|
| 157 |
+
"path": ["coins", "datasets", "nell", "sample-query"],
|
| 158 |
+
"query": [
|
| 159 |
+
{ "key": "query_structure", "value": "2i", "description": "Required. One of: 1p, 2p, 3p, 2i, 3i, ip, pi." },
|
| 160 |
+
{ "key": "count", "value": "1", "description": "Number of sample queries (max 10)." },
|
| 161 |
+
{ "key": "seed", "value": "2026-04-17", "description": "Optional. Deterministic sampling when provided." }
|
| 162 |
+
]
|
| 163 |
+
},
|
| 164 |
+
"description": "Sample a structurally valid KG query by walking the training graph. Returns anchors, relations, and a known target entity that form a real path/intersection. Keys in `anchors` and `relations` match the node/edge IDs from GET /coins/query-structures. Preferred over sample-triples for multi-hop and intersection query prefills."
|
| 165 |
+
}
|
| 166 |
+
},
|
| 167 |
{
|
| 168 |
"name": "GET /coins/models",
|
| 169 |
"request": {
|
|
@@ -78,6 +78,7 @@ All endpoints are prefixed with `/api/v1/`.
|
|
| 78 |
| `GET` | `/coins/datasets/{id}/entities` | Paginated entity search (`?q=&page=&page_size=`) |
|
| 79 |
| `GET` | `/coins/datasets/{id}/relations` | Paginated relation search (`?q=&page=&page_size=`) |
|
| 80 |
| `GET` | `/coins/datasets/{id}/sample-triples` | Random training triples (`?count=10&seed=...`); optional `seed` makes sampling deterministic (same `seed+count` ⇒ same triples, e.g. seed by ISO date for a day-stable widget). Head/relation/tail each carry a dataset-cleaned `label` alongside `id`, `name` |
|
|
|
|
| 81 |
| `GET` | `/coins/models` | Available algorithms + supported query structures |
|
| 82 |
| `GET` | `/coins/query-structures` | Query graph templates for frontend rendering |
|
| 83 |
| `POST` | `/coins/predict` | Run link prediction / query answering |
|
|
|
|
| 78 |
| `GET` | `/coins/datasets/{id}/entities` | Paginated entity search (`?q=&page=&page_size=`) |
|
| 79 |
| `GET` | `/coins/datasets/{id}/relations` | Paginated relation search (`?q=&page=&page_size=`) |
|
| 80 |
| `GET` | `/coins/datasets/{id}/sample-triples` | Random training triples (`?count=10&seed=...`); optional `seed` makes sampling deterministic (same `seed+count` ⇒ same triples, e.g. seed by ISO date for a day-stable widget). Head/relation/tail each carry a dataset-cleaned `label` alongside `id`, `name` |
|
| 81 |
+
| `GET` | `/coins/datasets/{id}/sample-query` | Sample a structurally valid KG query (`?query_structure=2i&count=1&seed=...`). Walks the training graph to produce real paths/intersections. Returns `{anchors, relations, target}` keyed by node/edge IDs from `/coins/query-structures`. Preferred over `sample-triples` for multi-hop/intersection prefills |
|
| 82 |
| `GET` | `/coins/models` | Available algorithms + supported query structures |
|
| 83 |
| `GET` | `/coins/query-structures` | Query graph templates for frontend rendering |
|
| 84 |
| `POST` | `/coins/predict` | Run link prediction / query answering |
|
|
@@ -263,6 +263,30 @@ class SubgraphInfo:
|
|
| 263 |
self.subgraphs = subgraphs
|
| 264 |
|
| 265 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
class ModelRegistry:
|
| 267 |
_instance = None
|
| 268 |
|
|
@@ -523,7 +547,10 @@ class ModelRegistry:
|
|
| 523 |
items = [(int(idx), str(name)) for idx, name in inv_nodes.items()]
|
| 524 |
if query:
|
| 525 |
q = query.lower()
|
| 526 |
-
items = [
|
|
|
|
|
|
|
|
|
|
| 527 |
total = len(items)
|
| 528 |
start = (max(1, page) - 1) * page_size
|
| 529 |
return items[start:start + page_size], total
|
|
@@ -537,7 +564,10 @@ class ModelRegistry:
|
|
| 537 |
items = [(int(idx), str(name)) for idx, name in inv_relations.items()]
|
| 538 |
if query:
|
| 539 |
q = query.lower()
|
| 540 |
-
items = [
|
|
|
|
|
|
|
|
|
|
| 541 |
total = len(items)
|
| 542 |
start = (max(1, page) - 1) * page_size
|
| 543 |
return items[start:start + page_size], total
|
|
@@ -573,6 +603,94 @@ class ModelRegistry:
|
|
| 573 |
})
|
| 574 |
return result
|
| 575 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 576 |
# ---- Sample subgraph generation ------------------------------------
|
| 577 |
|
| 578 |
def _generate_sample_subgraphs(self):
|
|
|
|
| 263 |
self.subgraphs = subgraphs
|
| 264 |
|
| 265 |
|
| 266 |
+
# ---------------------------------------------------------------------------
|
| 267 |
+
# Query sampling — delegates to Query.instantiate() from the research code.
|
| 268 |
+
#
|
| 269 |
+
# _STRUCTURE_INFO maps frontend query structure IDs to:
|
| 270 |
+
# - research code structure string (e.g. "ip" -> "2i1p")
|
| 271 |
+
# - anchor_map: {query_tree_node_index: frontend_anchor_id}
|
| 272 |
+
# - variable_map: {query_tree_node_index: frontend_variable_id}
|
| 273 |
+
# - relation_map: {mapped_tree_edge_index: frontend_relation_id} for "p" edges
|
| 274 |
+
#
|
| 275 |
+
# After Query.instantiate() + map_to_tree(), entities live on vertices
|
| 276 |
+
# and relation strings (like "p5") live on edges indexed by child node index.
|
| 277 |
+
# ---------------------------------------------------------------------------
|
| 278 |
+
# (structure, anchor_map, variable_map, relation_map)
|
| 279 |
+
_STRUCTURE_INFO = {
|
| 280 |
+
"1p": ("1p", {0: "a"}, {}, {0: "r1"}),
|
| 281 |
+
"2p": ("2p", {0: "a"}, {1: "v1"}, {0: "r1", 1: "r2"}),
|
| 282 |
+
"3p": ("3p", {0: "a"}, {1: "v1", 2: "v2"}, {0: "r1", 1: "r2", 2: "r3"}),
|
| 283 |
+
"2i": ("2i", {0: "a1", 2: "a2"}, {}, {0: "r1", 2: "r2"}),
|
| 284 |
+
"3i": ("3i", {0: "a1", 2: "a2", 4: "a3"}, {}, {0: "r1", 2: "r2", 4: "r3"}),
|
| 285 |
+
"ip": ("2i1p", {0: "a1", 2: "a2"}, {4: "v1"}, {0: "r1", 2: "r2", 4: "r3"}),
|
| 286 |
+
"pi": ("1p2i", {0: "a1", 3: "a2"}, {1: "v1"}, {0: "r1", 1: "r2", 3: "r3"}),
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
|
| 290 |
class ModelRegistry:
|
| 291 |
_instance = None
|
| 292 |
|
|
|
|
| 547 |
items = [(int(idx), str(name)) for idx, name in inv_nodes.items()]
|
| 548 |
if query:
|
| 549 |
q = query.lower()
|
| 550 |
+
items = [
|
| 551 |
+
(eid, name) for eid, name in items
|
| 552 |
+
if q in name.lower() or q in clean_entity_name(name, dataset_id).lower()
|
| 553 |
+
]
|
| 554 |
total = len(items)
|
| 555 |
start = (max(1, page) - 1) * page_size
|
| 556 |
return items[start:start + page_size], total
|
|
|
|
| 564 |
items = [(int(idx), str(name)) for idx, name in inv_relations.items()]
|
| 565 |
if query:
|
| 566 |
q = query.lower()
|
| 567 |
+
items = [
|
| 568 |
+
(rid, name) for rid, name in items
|
| 569 |
+
if q in name.lower() or q in clean_relation_name(name, dataset_id).lower()
|
| 570 |
+
]
|
| 571 |
total = len(items)
|
| 572 |
start = (max(1, page) - 1) * page_size
|
| 573 |
return items[start:start + page_size], total
|
|
|
|
| 603 |
})
|
| 604 |
return result
|
| 605 |
|
| 606 |
+
# ---- Query sampling ---------------------------------------------------
|
| 607 |
+
|
| 608 |
+
def sample_query(self, dataset_id, query_structure, count=1, seed=None):
|
| 609 |
+
"""Sample structurally valid queries using Query.instantiate() from the research code.
|
| 610 |
+
|
| 611 |
+
Picks random answer entities, walks the training graph backward via
|
| 612 |
+
adj_t_to_s to produce fully-instantiated query trees, then extracts
|
| 613 |
+
anchor entities and relation IDs mapped to frontend slot names.
|
| 614 |
+
"""
|
| 615 |
+
import numpy as np
|
| 616 |
+
|
| 617 |
+
loader = self.loaders.get(dataset_id)
|
| 618 |
+
if loader is None:
|
| 619 |
+
return []
|
| 620 |
+
|
| 621 |
+
info = _STRUCTURE_INFO.get(query_structure)
|
| 622 |
+
if info is None:
|
| 623 |
+
return []
|
| 624 |
+
structure_str, anchor_map, variable_map, relation_map = info
|
| 625 |
+
|
| 626 |
+
adj_t_to_s = loader.graph_indexes[2]
|
| 627 |
+
inv_nodes, _, inv_relations = loader.dataset.get_inverted_name_maps()
|
| 628 |
+
|
| 629 |
+
coins_root = str(Path(settings.COINS_DATA_DIR).parent)
|
| 630 |
+
original_cwd = os.getcwd()
|
| 631 |
+
try:
|
| 632 |
+
os.chdir(coins_root)
|
| 633 |
+
from graph_completion.graphs.queries import Query, query_edge_r_to_int
|
| 634 |
+
finally:
|
| 635 |
+
os.chdir(original_cwd)
|
| 636 |
+
|
| 637 |
+
query = Query(structure_str)
|
| 638 |
+
query.build_query_tree()
|
| 639 |
+
|
| 640 |
+
answer_candidates = list(adj_t_to_s.keys())
|
| 641 |
+
rng = random.Random(seed) if seed is not None else random
|
| 642 |
+
|
| 643 |
+
np_state = np.random.get_state()
|
| 644 |
+
if seed is not None:
|
| 645 |
+
np.random.seed(hash(seed) % (2**32))
|
| 646 |
+
|
| 647 |
+
def ent(eid):
|
| 648 |
+
name = str(inv_nodes.get(eid, eid))
|
| 649 |
+
return {"id": eid, "name": name, "label": clean_entity_name(name, dataset_id)}
|
| 650 |
+
|
| 651 |
+
def rel(rid):
|
| 652 |
+
name = str(inv_relations.get(rid, rid))
|
| 653 |
+
return {"id": rid, "name": name, "label": clean_relation_name(name, dataset_id)}
|
| 654 |
+
|
| 655 |
+
results = []
|
| 656 |
+
max_attempts = count * 200
|
| 657 |
+
try:
|
| 658 |
+
for _ in range(max_attempts):
|
| 659 |
+
if len(results) >= count:
|
| 660 |
+
break
|
| 661 |
+
answer = rng.choice(answer_candidates)
|
| 662 |
+
qi = next(
|
| 663 |
+
query.instantiate(adj_t_to_s, loader.num_nodes, loader.num_relations, answer, sample=True),
|
| 664 |
+
None,
|
| 665 |
+
)
|
| 666 |
+
if qi is None:
|
| 667 |
+
continue
|
| 668 |
+
|
| 669 |
+
qi_mapped = qi.map_to_tree(query.query_tree)
|
| 670 |
+
|
| 671 |
+
anchors = {}
|
| 672 |
+
for tree_idx, frontend_id in anchor_map.items():
|
| 673 |
+
anchors[frontend_id] = ent(int(qi_mapped.vs[tree_idx]["e"]))
|
| 674 |
+
|
| 675 |
+
variables = {}
|
| 676 |
+
for tree_idx, frontend_id in variable_map.items():
|
| 677 |
+
variables[frontend_id] = ent(int(qi_mapped.vs[tree_idx]["e"]))
|
| 678 |
+
|
| 679 |
+
relations = {}
|
| 680 |
+
for edge_idx, frontend_id in relation_map.items():
|
| 681 |
+
rel_id = query_edge_r_to_int(qi_mapped.es[edge_idx]["r"])
|
| 682 |
+
relations[frontend_id] = rel(rel_id)
|
| 683 |
+
|
| 684 |
+
target_id = int(qi_mapped.vs[query.query_answer]["e"])
|
| 685 |
+
q = {"anchors": anchors, "relations": relations, "target": ent(target_id)}
|
| 686 |
+
if variables:
|
| 687 |
+
q["variables"] = variables
|
| 688 |
+
results.append(q)
|
| 689 |
+
finally:
|
| 690 |
+
np.random.set_state(np_state)
|
| 691 |
+
|
| 692 |
+
return results
|
| 693 |
+
|
| 694 |
# ---- Sample subgraph generation ------------------------------------
|
| 695 |
|
| 696 |
def _generate_sample_subgraphs(self):
|
|
@@ -7,6 +7,7 @@ from api.views.coins import (
|
|
| 7 |
CoinsPredictView,
|
| 8 |
CoinsQueryStructuresView,
|
| 9 |
CoinsRelationsView,
|
|
|
|
| 10 |
CoinsSampleTriplesView,
|
| 11 |
)
|
| 12 |
from api.views.graph_generation import (
|
|
@@ -31,6 +32,7 @@ urlpatterns = [
|
|
| 31 |
path("coins/datasets/<str:dataset_id>/entities", CoinsEntitiesView.as_view()),
|
| 32 |
path("coins/datasets/<str:dataset_id>/relations", CoinsRelationsView.as_view()),
|
| 33 |
path("coins/datasets/<str:dataset_id>/sample-triples", CoinsSampleTriplesView.as_view()),
|
|
|
|
| 34 |
path("coins/models", CoinsModelsView.as_view()),
|
| 35 |
path("coins/query-structures", CoinsQueryStructuresView.as_view()),
|
| 36 |
path("coins/predict", CoinsPredictView.as_view()),
|
|
|
|
| 7 |
CoinsPredictView,
|
| 8 |
CoinsQueryStructuresView,
|
| 9 |
CoinsRelationsView,
|
| 10 |
+
CoinsSampleQueryView,
|
| 11 |
CoinsSampleTriplesView,
|
| 12 |
)
|
| 13 |
from api.views.graph_generation import (
|
|
|
|
| 32 |
path("coins/datasets/<str:dataset_id>/entities", CoinsEntitiesView.as_view()),
|
| 33 |
path("coins/datasets/<str:dataset_id>/relations", CoinsRelationsView.as_view()),
|
| 34 |
path("coins/datasets/<str:dataset_id>/sample-triples", CoinsSampleTriplesView.as_view()),
|
| 35 |
+
path("coins/datasets/<str:dataset_id>/sample-query", CoinsSampleQueryView.as_view()),
|
| 36 |
path("coins/models", CoinsModelsView.as_view()),
|
| 37 |
path("coins/query-structures", CoinsQueryStructuresView.as_view()),
|
| 38 |
path("coins/predict", CoinsPredictView.as_view()),
|
|
@@ -1,6 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
def clean_entity_name(name: str, dataset_id: str) -> str:
|
| 2 |
-
"""Apply dataset-specific entity name cleaning for display
|
| 3 |
if dataset_id == "freebase":
|
|
|
|
|
|
|
|
|
|
| 4 |
return name.replace("/m/", "")
|
| 5 |
elif dataset_id == "wordnet":
|
| 6 |
return name.split(".")[0]
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
|
| 3 |
+
from django.conf import settings
|
| 4 |
+
|
| 5 |
+
_freebase_ent2name = None
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def _get_freebase_ent2name():
|
| 9 |
+
global _freebase_ent2name
|
| 10 |
+
if _freebase_ent2name is None:
|
| 11 |
+
path = Path(settings.COINS_DATA_DIR) / "FB15k-237" / "ent2name.txt"
|
| 12 |
+
mapping = {}
|
| 13 |
+
if path.exists():
|
| 14 |
+
with open(path, encoding="utf-8") as f:
|
| 15 |
+
for line in f:
|
| 16 |
+
parts = line.rstrip("\n").split(None, 1)
|
| 17 |
+
if len(parts) == 2:
|
| 18 |
+
mapping[parts[0]] = parts[1].replace("_", " ")
|
| 19 |
+
_freebase_ent2name = mapping
|
| 20 |
+
return _freebase_ent2name
|
| 21 |
+
|
| 22 |
+
|
| 23 |
def clean_entity_name(name: str, dataset_id: str) -> str:
|
| 24 |
+
"""Apply dataset-specific entity name cleaning for display."""
|
| 25 |
if dataset_id == "freebase":
|
| 26 |
+
resolved = _get_freebase_ent2name().get(name)
|
| 27 |
+
if resolved:
|
| 28 |
+
return resolved
|
| 29 |
return name.replace("/m/", "")
|
| 30 |
elif dataset_id == "wordnet":
|
| 31 |
return name.split(".")[0]
|
|
@@ -7,6 +7,7 @@ from api.services.constants import (
|
|
| 7 |
QUERY_STRUCTURE_INTERNAL, QUERY_TREE_MAPPINGS,
|
| 8 |
)
|
| 9 |
from api.services.registry import ModelRegistry
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
def _require_loader(dataset_id):
|
|
@@ -49,7 +50,10 @@ class CoinsEntitiesView(APIView):
|
|
| 49 |
"total": total,
|
| 50 |
"page": page,
|
| 51 |
"page_size": page_size,
|
| 52 |
-
"entities": [
|
|
|
|
|
|
|
|
|
|
| 53 |
})
|
| 54 |
|
| 55 |
|
|
@@ -68,7 +72,10 @@ class CoinsRelationsView(APIView):
|
|
| 68 |
"total": total,
|
| 69 |
"page": page,
|
| 70 |
"page_size": page_size,
|
| 71 |
-
"relations": [
|
|
|
|
|
|
|
|
|
|
| 72 |
})
|
| 73 |
|
| 74 |
|
|
@@ -87,6 +94,30 @@ class CoinsSampleTriplesView(APIView):
|
|
| 87 |
})
|
| 88 |
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
class CoinsModelsView(APIView):
|
| 91 |
def get(self, request):
|
| 92 |
registry = ModelRegistry.get()
|
|
|
|
| 7 |
QUERY_STRUCTURE_INTERNAL, QUERY_TREE_MAPPINGS,
|
| 8 |
)
|
| 9 |
from api.services.registry import ModelRegistry
|
| 10 |
+
from api.utils import clean_entity_name, clean_relation_name
|
| 11 |
|
| 12 |
|
| 13 |
def _require_loader(dataset_id):
|
|
|
|
| 50 |
"total": total,
|
| 51 |
"page": page,
|
| 52 |
"page_size": page_size,
|
| 53 |
+
"entities": [
|
| 54 |
+
{"id": eid, "name": name, "label": clean_entity_name(name, dataset_id)}
|
| 55 |
+
for eid, name in page_items
|
| 56 |
+
],
|
| 57 |
})
|
| 58 |
|
| 59 |
|
|
|
|
| 72 |
"total": total,
|
| 73 |
"page": page,
|
| 74 |
"page_size": page_size,
|
| 75 |
+
"relations": [
|
| 76 |
+
{"id": rid, "name": name, "label": clean_relation_name(name, dataset_id)}
|
| 77 |
+
for rid, name in page_items
|
| 78 |
+
],
|
| 79 |
})
|
| 80 |
|
| 81 |
|
|
|
|
| 94 |
})
|
| 95 |
|
| 96 |
|
| 97 |
+
class CoinsSampleQueryView(APIView):
|
| 98 |
+
def get(self, request, dataset_id):
|
| 99 |
+
registry = _require_loader(dataset_id)
|
| 100 |
+
query_structure = request.query_params.get("query_structure")
|
| 101 |
+
if not query_structure:
|
| 102 |
+
raise InvalidRequestError("Missing required parameter: query_structure")
|
| 103 |
+
valid_qs = {qs["id"] for qs in QUERY_STRUCTURES}
|
| 104 |
+
if query_structure not in valid_qs:
|
| 105 |
+
raise InvalidRequestError(
|
| 106 |
+
f"Unknown query_structure '{query_structure}'. Must be one of: {sorted(valid_qs)}"
|
| 107 |
+
)
|
| 108 |
+
count = int(request.query_params.get("count", 1))
|
| 109 |
+
count = max(1, min(10, count))
|
| 110 |
+
seed_raw = request.query_params.get("seed")
|
| 111 |
+
seed = seed_raw if seed_raw not in (None, "") else None
|
| 112 |
+
|
| 113 |
+
queries = registry.sample_query(dataset_id, query_structure, count, seed=seed)
|
| 114 |
+
return Response({
|
| 115 |
+
"dataset_id": dataset_id,
|
| 116 |
+
"query_structure": query_structure,
|
| 117 |
+
"queries": queries,
|
| 118 |
+
})
|
| 119 |
+
|
| 120 |
+
|
| 121 |
class CoinsModelsView(APIView):
|
| 122 |
def get(self, request):
|
| 123 |
registry = ModelRegistry.get()
|
|
The diff for this file is too large to render.
See raw diff
|
|
|