mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-30 12:39:27 +08:00
Refactor: merge dataset scope graph. (#17526)
### Summary merge dataset scope graph. --------- Co-authored-by: Yingfeng Zhang <yingfeng.zhang@gmail.com>
This commit is contained in:
@@ -1833,45 +1833,86 @@ async def get_dataset_structure(dataset_id: str, tenant_id: str, kind: str, keyw
|
||||
"kind": row_kind or template_kind_cache.get(tid) or resolved_kind,
|
||||
}
|
||||
|
||||
# ── Discovery: dataset_graph blob rows, metadata only (no huge content). ──
|
||||
# Keep only rows whose TOP-LEVEL kind matches the request. Raw entity/relation
|
||||
# rows stamp ``compilation_template_kind_kwd`` with config.kind (which folds the
|
||||
# knowledge_graph family), so we scope raw-row queries by template id — resolved
|
||||
# here from the blobs, whose stamp is the top-level kind — not by kind directly.
|
||||
meta_fields = ["compile_kwd", "compilation_template_ids", "compilation_template_kind_kwd"]
|
||||
try:
|
||||
res = await thread_pool_exec(
|
||||
settings.docStoreConn.search,
|
||||
meta_fields,
|
||||
[],
|
||||
{"knowledge_graph_kwd": [_DATASET_STRUCTURE_ROW_KWD]},
|
||||
[],
|
||||
OrderByExpr(),
|
||||
0,
|
||||
1000,
|
||||
index_nm,
|
||||
[dataset_id],
|
||||
)
|
||||
meta_rows = settings.docStoreConn.get_fields(res, meta_fields) or {}
|
||||
except Exception:
|
||||
logging.exception("get_dataset_structure: docStore discovery failed for kb=%s", dataset_id)
|
||||
return True, empty
|
||||
|
||||
# ── Discovery: the dataset-scoped rows the structure merge writes. ──
|
||||
# ``run_structure_merge`` (rag.svr.task_executor_refactor.dataset_structure_merger)
|
||||
# writes merged ``knowledge_graph_kwd="entity"/"relation"`` rows with
|
||||
# ``scope_kwd="dataset"``, stamped with ``compilation_template_ids`` and the
|
||||
# top-level ``compilation_template_kind_kwd``. Page through them (metadata
|
||||
# fields only) to enumerate the distinct template ids whose top-level kind
|
||||
# matches the request; ``build_bucket`` below then reads each template's rows.
|
||||
meta_fields = ["id", "compile_kwd", "compilation_template_ids", "compilation_template_kind_kwd"]
|
||||
kind_template_ids: list[str] = []
|
||||
seen_tid: set[str] = set()
|
||||
has_templateless = False
|
||||
for row in meta_rows.values():
|
||||
tid = _row_template_id(row)
|
||||
stamped_kind = (row.get("compilation_template_kind_kwd") or "").strip()
|
||||
row_kind = stamped_kind or _template_meta(tid) or ""
|
||||
if _resolve_dataset_structure_kind(row_kind) != resolved_kind:
|
||||
continue
|
||||
if tid:
|
||||
if tid not in seen_tid:
|
||||
seen_tid.add(tid)
|
||||
kind_template_ids.append(tid)
|
||||
else:
|
||||
has_templateless = True
|
||||
offset = 0
|
||||
page_size = 1000
|
||||
pages = 0
|
||||
while True:
|
||||
pages += 1
|
||||
try:
|
||||
res = await thread_pool_exec(
|
||||
settings.docStoreConn.search,
|
||||
meta_fields,
|
||||
[],
|
||||
{"knowledge_graph_kwd": ["entity", "relation"], "scope_kwd": ["dataset"]},
|
||||
[],
|
||||
OrderByExpr(),
|
||||
offset,
|
||||
page_size,
|
||||
index_nm,
|
||||
[dataset_id],
|
||||
)
|
||||
meta_rows = settings.docStoreConn.get_fields(res, meta_fields) or {}
|
||||
except Exception:
|
||||
logging.exception("get_dataset_structure: docStore discovery failed for kb=%s", dataset_id)
|
||||
return True, empty
|
||||
if not meta_rows:
|
||||
break
|
||||
for row in meta_rows.values():
|
||||
tid = _row_template_id(row)
|
||||
stamped_kind = (row.get("compilation_template_kind_kwd") or "").strip()
|
||||
row_kind = stamped_kind or _template_meta(tid) or ""
|
||||
if _resolve_dataset_structure_kind(row_kind) != resolved_kind:
|
||||
continue
|
||||
if tid:
|
||||
if tid not in seen_tid:
|
||||
seen_tid.add(tid)
|
||||
kind_template_ids.append(tid)
|
||||
else:
|
||||
has_templateless = True
|
||||
if len(meta_rows) < page_size:
|
||||
break
|
||||
offset += page_size
|
||||
|
||||
# Detect datasets that have ONLY the legacy dataset_graph blob (no
|
||||
# entity/relation rows yet) so the fallback path below handles them.
|
||||
if not kind_template_ids and not has_templateless:
|
||||
try:
|
||||
legacy_check = await thread_pool_exec(
|
||||
settings.docStoreConn.search,
|
||||
["id"],
|
||||
[],
|
||||
{"knowledge_graph_kwd": [_DATASET_STRUCTURE_ROW_KWD]},
|
||||
[],
|
||||
OrderByExpr(),
|
||||
0,
|
||||
1,
|
||||
index_nm,
|
||||
[dataset_id],
|
||||
)
|
||||
legacy_fm = settings.docStoreConn.get_fields(legacy_check, ["id"]) or {}
|
||||
if legacy_fm:
|
||||
has_templateless = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logging.debug(
|
||||
"get_dataset_structure: discovered %d template(s) in %d page(s) for kb=%s kind=%s",
|
||||
len(kind_template_ids),
|
||||
pages,
|
||||
dataset_id,
|
||||
resolved_kind,
|
||||
)
|
||||
|
||||
# ── keywords mode: global KNN across the kind → top-1's focused subgraph. ──
|
||||
if keywords:
|
||||
@@ -1888,18 +1929,20 @@ async def get_dataset_structure(dataset_id: str, tenant_id: str, kind: str, keyw
|
||||
tid = _row_template_id(row) or ""
|
||||
stamped = (row.get("compilation_template_kind_kwd") or "").strip()
|
||||
meta = _bucket_meta_for(tid, stamped) if tid else {"template_id": f"kind:{resolved_kind}", "template_name": f"kind:{resolved_kind}", "kind": resolved_kind}
|
||||
return meta, {"compilation_template_ids": [tid]} if tid else {"compilation_template_kind_kwd": [stamped]}
|
||||
if tid:
|
||||
return meta, {"compilation_template_ids": [tid], "scope_kwd": ["dataset"]}
|
||||
return meta, {"compilation_template_kind_kwd": [stamped], "scope_kwd": ["dataset"]}
|
||||
|
||||
bucket_meta, kw_entities, kw_relations = await sgc.keyword_subgraph(
|
||||
index_nm,
|
||||
dataset_id,
|
||||
embd_mdl,
|
||||
{"compilation_template_ids": kind_template_ids, "knowledge_graph_kwd": ["entity"]},
|
||||
{"compilation_template_ids": kind_template_ids, "knowledge_graph_kwd": ["entity"], "scope_kwd": ["dataset"]},
|
||||
keywords,
|
||||
_scope_for_template,
|
||||
log_ctx=f"kb={dataset_id}",
|
||||
)
|
||||
if resolved_kind == "mind_map":
|
||||
if resolved_kind in {"knowledge_graph", "mind_map", "timeline"}:
|
||||
kw_entities = sgc.filter_entities_with_relations(kw_entities, kw_relations)
|
||||
if not kw_entities:
|
||||
return True, empty
|
||||
@@ -1914,11 +1957,11 @@ async def get_dataset_structure(dataset_id: str, tenant_id: str, kind: str, keyw
|
||||
templates_out: list[dict] = []
|
||||
for tid in kind_template_ids:
|
||||
try:
|
||||
entities, relations = await sgc.build_bucket(index_nm, dataset_id, {"compilation_template_ids": [tid]})
|
||||
entities, relations = await sgc.build_bucket(index_nm, dataset_id, {"compilation_template_ids": [tid], "scope_kwd": ["dataset"]})
|
||||
except Exception:
|
||||
logging.exception("get_dataset_structure: bucket build failed for kb=%s template=%s", dataset_id, tid)
|
||||
continue
|
||||
if resolved_kind == "mind_map":
|
||||
if resolved_kind in {"knowledge_graph", "mind_map", "timeline"}:
|
||||
entities = sgc.filter_entities_with_relations(entities, relations)
|
||||
if not entities:
|
||||
continue
|
||||
@@ -1960,10 +2003,10 @@ async def get_dataset_structure(dataset_id: str, tenant_id: str, kind: str, keyw
|
||||
continue
|
||||
legacy_bucket["entities"].extend(graph.get("entities") or [])
|
||||
legacy_bucket["relations"].extend(graph.get("relations") or [])
|
||||
if resolved_kind == "mind_map":
|
||||
if resolved_kind in {"knowledge_graph", "mind_map", "timeline"}:
|
||||
legacy_bucket["entities"] = sgc.filter_entities_with_relations(legacy_bucket["entities"], legacy_bucket["relations"])
|
||||
if legacy_bucket["entities"] or legacy_bucket["relations"]:
|
||||
if resolved_kind != "mind_map" or legacy_bucket["entities"]:
|
||||
if resolved_kind not in {"knowledge_graph", "mind_map", "timeline"} or legacy_bucket["entities"]:
|
||||
templates_out.append(legacy_bucket)
|
||||
|
||||
return True, {"kind": kind, "templates": templates_out}
|
||||
|
||||
@@ -133,11 +133,65 @@ def dedup_entities(entities: list[dict]) -> list[dict]:
|
||||
return out
|
||||
|
||||
|
||||
def _entity_response_id(entity: dict) -> str:
|
||||
for field in ("id", "name", "slug"):
|
||||
value = entity.get(field)
|
||||
if isinstance(value, str) and value.strip():
|
||||
return value.strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _endpoint_terms(value: str) -> list[str]:
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return []
|
||||
return sorted({value, value.lower()})
|
||||
|
||||
|
||||
def normalize_relation_endpoints(entities: list[dict], relations: list[dict]) -> list[dict]:
|
||||
"""Align relation endpoints to the returned entity ids/names."""
|
||||
if not entities or not relations:
|
||||
return relations
|
||||
|
||||
lookup: dict[str, str] = {}
|
||||
ambiguous: set[str] = set()
|
||||
for entity in entities:
|
||||
response_id = _entity_response_id(entity)
|
||||
if not response_id:
|
||||
continue
|
||||
for field in ("id", "name", "slug"):
|
||||
value = entity.get(field)
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
continue
|
||||
key = value.strip().lower()
|
||||
if key in lookup and lookup[key] != response_id:
|
||||
ambiguous.add(key)
|
||||
continue
|
||||
lookup[key] = response_id
|
||||
for key in ambiguous:
|
||||
lookup.pop(key, None)
|
||||
|
||||
normalized: list[dict] = []
|
||||
for relation in relations:
|
||||
if not isinstance(relation, dict):
|
||||
continue
|
||||
item = dict(relation)
|
||||
for field in ("from", "to"):
|
||||
value = item.get(field)
|
||||
if isinstance(value, str):
|
||||
item[field] = lookup.get(value.strip().lower(), value)
|
||||
normalized.append(item)
|
||||
return normalized
|
||||
|
||||
|
||||
def filter_entities_with_relations(entities: list[dict], relations: list[dict]) -> list[dict]:
|
||||
"""Keep only entities that are referenced by at least one relation."""
|
||||
if not entities or not relations:
|
||||
return []
|
||||
|
||||
# Match case-insensitively: the dataset-scoped merge lowercases relation
|
||||
# endpoints while entity names keep their original case, so exact matching
|
||||
# would drop connected nodes from graph-like views.
|
||||
connected: set[str] = set()
|
||||
for relation in relations:
|
||||
if not isinstance(relation, dict):
|
||||
@@ -145,7 +199,7 @@ def filter_entities_with_relations(entities: list[dict], relations: list[dict])
|
||||
for endpoint_key in ("from", "to"):
|
||||
endpoint = relation.get(endpoint_key)
|
||||
if isinstance(endpoint, str):
|
||||
endpoint = endpoint.strip()
|
||||
endpoint = endpoint.strip().lower()
|
||||
if endpoint:
|
||||
connected.add(endpoint)
|
||||
|
||||
@@ -164,7 +218,7 @@ def filter_entities_with_relations(entities: list[dict], relations: list[dict])
|
||||
for field in ("id", "name", "slug"):
|
||||
value = entity.get(field)
|
||||
if isinstance(value, str):
|
||||
value = value.strip()
|
||||
value = value.strip().lower()
|
||||
if value:
|
||||
keys.add(value)
|
||||
if keys & connected:
|
||||
@@ -198,7 +252,8 @@ async def build_bucket(index_name, kb_id, scope: dict) -> tuple[list[dict], list
|
||||
node = project_entity(row)
|
||||
if node:
|
||||
entities.append(node)
|
||||
return dedup_entities(entities), relations
|
||||
entities = dedup_entities(entities)
|
||||
return entities, normalize_relation_endpoints(entities, relations)
|
||||
|
||||
# Large bucket: sample. A = top entities by mention_count_int desc.
|
||||
order_by = OrderByExpr()
|
||||
@@ -209,12 +264,13 @@ async def build_bucket(index_name, kb_id, scope: dict) -> tuple[list[dict], list
|
||||
ent_a_map, _ = await graph_search(index_name, kb_id, GRAPH_ENTITY_FIELDS, dict(scope, knowledge_graph_kwd=["entity"]), order_by, GRAPH_TOP_ENTITIES)
|
||||
set_a = [n for n in (project_entity(r) for r in ent_a_map.values()) if n]
|
||||
a_names = sorted({str(e.get("name") or "").strip() for e in set_a if str(e.get("name") or "").strip()})
|
||||
a_name_terms = sorted({term for name in a_names for term in _endpoint_terms(name)})
|
||||
|
||||
# relations whose source is one of A.
|
||||
relations = []
|
||||
target_names_lower: set[str] = set()
|
||||
if a_names:
|
||||
rel_map, _ = await graph_search(index_name, kb_id, GRAPH_RELATION_FIELDS, dict(scope, knowledge_graph_kwd=["relation"], from_entity_kwd=a_names), OrderByExpr(), GRAPH_EXPANSION_CAP)
|
||||
if a_name_terms:
|
||||
rel_map, _ = await graph_search(index_name, kb_id, GRAPH_RELATION_FIELDS, dict(scope, knowledge_graph_kwd=["relation"], from_entity_kwd=a_name_terms), OrderByExpr(), GRAPH_EXPANSION_CAP)
|
||||
for row in rel_map.values():
|
||||
edge = project_relation(row)
|
||||
if edge:
|
||||
@@ -229,7 +285,8 @@ async def build_bucket(index_name, kb_id, scope: dict) -> tuple[list[dict], list
|
||||
tgt_map, _ = await graph_search(index_name, kb_id, GRAPH_ENTITY_FIELDS, dict(scope, knowledge_graph_kwd=["entity"], name_kwd=sorted(target_names_lower)), OrderByExpr(), GRAPH_EXPANSION_CAP)
|
||||
set_t = [n for n in (project_entity(r) for r in tgt_map.values()) if n]
|
||||
|
||||
return dedup_entities(set_a + set_t), relations
|
||||
entities = dedup_entities(set_a + set_t)
|
||||
return entities, normalize_relation_endpoints(entities, relations)
|
||||
|
||||
|
||||
async def keyword_subgraph(index_name, kb_id, embd_mdl, base_entity_condition, keywords, scope_for_template, log_ctx="") -> tuple[dict | None, list[dict], list[dict]]:
|
||||
@@ -280,8 +337,9 @@ async def keyword_subgraph(index_name, kb_id, embd_mdl, base_entity_condition, k
|
||||
relations: list[dict] = []
|
||||
seen_rel: set[tuple[str, str, str]] = set()
|
||||
neighbor_names_lower: set[str] = set()
|
||||
top_name_terms = _endpoint_terms(top_name)
|
||||
for field in ("from_entity_kwd", "to_entity_kwd"):
|
||||
rel_map, _ = await graph_search(index_name, kb_id, GRAPH_RELATION_FIELDS, dict(scope, knowledge_graph_kwd=["relation"], **{field: [top_name]}), OrderByExpr(), GRAPH_EXPANSION_CAP)
|
||||
rel_map, _ = await graph_search(index_name, kb_id, GRAPH_RELATION_FIELDS, dict(scope, knowledge_graph_kwd=["relation"], **{field: top_name_terms}), OrderByExpr(), GRAPH_EXPANSION_CAP)
|
||||
for row in rel_map.values():
|
||||
edge = project_relation(row)
|
||||
if not edge:
|
||||
@@ -301,4 +359,5 @@ async def keyword_subgraph(index_name, kb_id, embd_mdl, base_entity_condition, k
|
||||
nb_map, _ = await graph_search(index_name, kb_id, GRAPH_ENTITY_FIELDS, dict(scope, knowledge_graph_kwd=["entity"], name_kwd=sorted(neighbor_names_lower)), OrderByExpr(), GRAPH_EXPANSION_CAP)
|
||||
entities.extend(n for n in (project_entity(r) for r in nb_map.values()) if n)
|
||||
|
||||
return bucket_meta, dedup_entities(entities), relations
|
||||
entities = dedup_entities(entities)
|
||||
return bucket_meta, entities, normalize_relation_endpoints(entities, relations)
|
||||
|
||||
@@ -512,6 +512,20 @@ class DocumentService(CommonService):
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to delete chunks from doc store for document {doc.id}: {e}")
|
||||
|
||||
# Record doc deletion for incremental structure-merge ghost cleanup.
|
||||
# Runs after the doc_id sweep so the marker (stored under
|
||||
# deleted_doc_id to avoid matching the same sweep) survives.
|
||||
try:
|
||||
from rag.svr.task_executor_refactor.dataset_structure_merger import (
|
||||
record_doc_deletion,
|
||||
)
|
||||
|
||||
record_doc_deletion(tenant_id, doc.kb_id, doc.id)
|
||||
except Exception as e:
|
||||
logging.warning(
|
||||
f"Failed to record doc deletion for structure merge: {e}",
|
||||
)
|
||||
|
||||
# Ref-counted cleanup of wiki/artifact products this doc fed into
|
||||
# (non-critical, log and continue). A product shared by other docs
|
||||
# survives; one this doc solely owned is removed.
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"extra": {"type": "varchar", "default": ""},
|
||||
|
||||
"compile_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"},
|
||||
"scope_kwd": {"type": "varchar", "default": "doc", "analyzer": "whitespace-#"},
|
||||
"source_chunk_ids": {"type": "json", "default": "[]", "index_type": {"type": "secondary", "cardinality": "low"}},
|
||||
"source_doc_ids": {"type": "json", "default": "[]", "index_type": {"type": "secondary", "cardinality": "low"}},
|
||||
"compilation_template_ids": {"type": "json", "default": "[]", "index_type": {"type": "secondary", "cardinality": "low"}},
|
||||
|
||||
@@ -572,6 +572,8 @@ def _struct_to_doc_storage_doc(
|
||||
target_field: str | None = None,
|
||||
compilation_template_id: str | None = None,
|
||||
compilation_template_kind: str | None = None,
|
||||
scope: str = "doc",
|
||||
doc_ids: list[str] | None = None,
|
||||
) -> dict:
|
||||
"""Build one ES doc for an extracted entity or relation.
|
||||
|
||||
@@ -582,11 +584,11 @@ def _struct_to_doc_storage_doc(
|
||||
``from_entity_kwd`` / ``to_entity_kwd``.
|
||||
compilation_template_id / compilation_template_kind: stamped onto
|
||||
every row so the document-structure endpoint can group by
|
||||
template id and the UI can render one tab per template. The
|
||||
id is stored as a single-element list under
|
||||
``compilation_template_ids`` because the same logical entity
|
||||
*could* later be claimed by multiple templates during a
|
||||
cross-template merge (rare, but the schema is forward-compat).
|
||||
template id and the UI can render one tab per template.
|
||||
scope: ``"doc"`` for document-level rows, ``"dataset"`` for the KB-wide
|
||||
merged entity rows written by the Build button.
|
||||
doc_ids: only set for ``scope="dataset"`` rows — lists the documents
|
||||
that contributed to this merged entity.
|
||||
"""
|
||||
content_with_weight = json.dumps(payload, ensure_ascii=False)
|
||||
if hasattr(vec, "tolist"):
|
||||
@@ -602,13 +604,18 @@ def _struct_to_doc_storage_doc(
|
||||
# Mix the template id into the stable row id so two templates with the
|
||||
# same compile_kwd don't collide on identical payloads (e.g. two
|
||||
# different list-kind templates that each extract "headline X").
|
||||
# Dataset-scope rows get the scope in the row seed to avoid colliding
|
||||
# with doc-scope rows for the same entity.
|
||||
row_seed_extras = [template_id_str] if template_id_str else []
|
||||
if scope == "dataset":
|
||||
row_seed_extras.append("dataset")
|
||||
row_id = _stable_row_id(content_with_weight, doc_id_str, *row_seed_extras)
|
||||
|
||||
doc = {
|
||||
"content_with_weight": content_with_weight,
|
||||
"compile_kwd": compile_kwd,
|
||||
"knowledge_graph_kwd": kind,
|
||||
"scope_kwd": scope,
|
||||
"doc_id": doc_id_str,
|
||||
"source_chunk_ids": list(chunk_ids or []),
|
||||
"content_ltks": content_ltks,
|
||||
@@ -616,6 +623,8 @@ def _struct_to_doc_storage_doc(
|
||||
f"q_{len(vec_list)}_vec": vec_list,
|
||||
"id": row_id,
|
||||
}
|
||||
if scope == "dataset" and doc_ids:
|
||||
doc["doc_ids_kwd"] = list(doc_ids)
|
||||
|
||||
# Surface two payload fields as queryable top-level columns so the store can
|
||||
# filter/sort on them without parsing ``content_with_weight``. Both are
|
||||
@@ -2226,44 +2235,116 @@ async def _struct_upsert_dataset_graph_json(
|
||||
compile_kwd: str,
|
||||
compilation_template_id: str | None = None,
|
||||
structure_kind: str | None = None,
|
||||
embd_mdl=None,
|
||||
) -> None:
|
||||
"""Write dataset-level entity/relation rows from a merged graph.
|
||||
|
||||
Replaces the old ``dataset_graph`` JSON blob with individual searchable
|
||||
entity/relation rows (``scope_kwd="dataset"``). Each row carries its own
|
||||
embedding and tokenized text, so both KNN and full-text search can hit it.
|
||||
"""
|
||||
from common import settings
|
||||
from rag.nlp import search as _rag_search
|
||||
from ._common import encode as _encode, tokenize_for_search as _tokenize_for_search, stable_row_id as _stable_row_id
|
||||
|
||||
index = _rag_search.index_name(tenant_id)
|
||||
row_id = _dataset_struct_graph_row_id(kb_id, compile_kwd, compilation_template_id)
|
||||
# The dataset graph is not per-document generated data, but the store schema
|
||||
# requires ``doc_id``; carry ``kb_id`` there so the row is self-describing
|
||||
# and per-doc graph queries (filtered by a real doc_id) never pick it up.
|
||||
row = {
|
||||
"id": row_id,
|
||||
"content_with_weight": json.dumps(graph, ensure_ascii=False),
|
||||
kb_id_str = str(kb_id)
|
||||
|
||||
# Write a single metadata row so the artifacts_structure discovery endpoint
|
||||
# can find this template without scanning entity rows.
|
||||
meta_id = _dataset_struct_graph_row_id(kb_id, compile_kwd, compilation_template_id)
|
||||
meta_row = {
|
||||
"id": meta_id,
|
||||
"compile_kwd": compile_kwd,
|
||||
"knowledge_graph_kwd": "dataset_graph",
|
||||
"doc_id": kb_id,
|
||||
"kb_id": kb_id,
|
||||
"scope_kwd": "dataset",
|
||||
"doc_id": kb_id_str,
|
||||
"kb_id": kb_id_str,
|
||||
"available_int": 0,
|
||||
"compilation_template_ids": [compilation_template_id] if compilation_template_id else [],
|
||||
}
|
||||
if compilation_template_id:
|
||||
row["compilation_template_ids"] = [compilation_template_id]
|
||||
# On ``dataset_graph`` rows ``compilation_template_kind_kwd`` carries the
|
||||
# template's *top-level* kind (graph/mind_map/timeline/session_essence/
|
||||
# session_graph) so the artifacts_structure endpoint can filter by it —
|
||||
# unlike entity/relation rows where the field holds the ``config.kind``
|
||||
# (which collapses the knowledge_graph family together).
|
||||
if structure_kind:
|
||||
row["compilation_template_kind_kwd"] = str(structure_kind)
|
||||
old = await thread_pool_exec(settings.docStoreConn.get, row_id, index, [kb_id])
|
||||
meta_row["compilation_template_kind_kwd"] = str(structure_kind)
|
||||
old = await thread_pool_exec(settings.docStoreConn.get, meta_id, index, [kb_id])
|
||||
if old:
|
||||
await thread_pool_exec(
|
||||
settings.docStoreConn.update,
|
||||
{"id": row_id},
|
||||
{k: v for k, v in row.items() if k != "id"},
|
||||
index,
|
||||
kb_id,
|
||||
)
|
||||
await thread_pool_exec(settings.docStoreConn.update, {"id": meta_id}, {k: v for k, v in meta_row.items() if k != "id"}, index, kb_id)
|
||||
else:
|
||||
await thread_pool_exec(settings.docStoreConn.insert, [row], index, kb_id)
|
||||
await thread_pool_exec(settings.docStoreConn.insert, [meta_row], index, kb_id)
|
||||
|
||||
# Write individual entity/relation rows (scope_kwd="dataset", searchable).
|
||||
rows = []
|
||||
for ent in graph.get("entities") or []:
|
||||
payload = {"name": ent.get("name", ""), "type": ent.get("type", "other"), "description": ent.get("description", "")}
|
||||
ent_name = (ent.get("name") or "").strip()
|
||||
desc = ent.get("description") or ent_name
|
||||
ltks, sm_ltks = _tokenize_for_search(desc)
|
||||
mention_count = ent.get("mention_count", 1)
|
||||
source_chunk_ids = ent.get("source_chunk_ids") or []
|
||||
doc_ids = ent.get("doc_ids_kwd") or []
|
||||
row_id = _stable_row_id(ent_name.lower(), kb_id_str, compile_kwd, compilation_template_id or "", "dataset")
|
||||
row = {
|
||||
"id": row_id,
|
||||
"content_with_weight": json.dumps(payload, ensure_ascii=False),
|
||||
"compile_kwd": compile_kwd,
|
||||
"knowledge_graph_kwd": "entity",
|
||||
"scope_kwd": "dataset",
|
||||
"doc_id": kb_id_str,
|
||||
"kb_id": kb_id_str,
|
||||
"source_chunk_ids": source_chunk_ids,
|
||||
"content_ltks": ltks,
|
||||
"content_sm_ltks": sm_ltks,
|
||||
"mention_count_int": mention_count,
|
||||
"name_kwd": ent_name.lower(),
|
||||
"available_int": 1,
|
||||
}
|
||||
if compilation_template_id:
|
||||
row["compilation_template_ids"] = [compilation_template_id]
|
||||
if structure_kind:
|
||||
row["compilation_template_kind_kwd"] = str(structure_kind)
|
||||
if doc_ids:
|
||||
row["doc_ids_kwd"] = doc_ids
|
||||
# Re-embed: use embd_mdl if available, otherwise skip the vector.
|
||||
if embd_mdl:
|
||||
vecs = await _encode(embd_mdl, [desc])
|
||||
if vecs and len(vecs[0]) > 0:
|
||||
dim = len(vecs[0])
|
||||
row[f"q_{dim}_vec"] = list(vecs[0])
|
||||
rows.append(row)
|
||||
|
||||
for rel in graph.get("relations") or []:
|
||||
src = str(rel.get("from", "")).strip()
|
||||
tgt = str(rel.get("to", "")).strip()
|
||||
if not src or not tgt:
|
||||
continue
|
||||
rel_type = str(rel.get("type", "related")).strip()
|
||||
payload = {"from": src, "to": tgt, "type": rel_type}
|
||||
desc = f"{src} {rel_type} {tgt}"
|
||||
ltks, sm_ltks = _tokenize_for_search(desc)
|
||||
rel_key = f"{src.lower()} -> {rel_type.lower()} -> {tgt.lower()}"
|
||||
row_id = _stable_row_id(rel_key, kb_id_str, compile_kwd, compilation_template_id or "", "dataset")
|
||||
doc_ids = rel.get("doc_ids_kwd") or []
|
||||
row = {
|
||||
"id": row_id,
|
||||
"content_with_weight": json.dumps(payload, ensure_ascii=False),
|
||||
"compile_kwd": compile_kwd,
|
||||
"knowledge_graph_kwd": "relation",
|
||||
"scope_kwd": "dataset",
|
||||
"doc_id": kb_id_str,
|
||||
"kb_id": kb_id_str,
|
||||
"from_entity_kwd": src.lower(),
|
||||
"to_entity_kwd": tgt.lower(),
|
||||
"content_ltks": ltks,
|
||||
"content_sm_ltks": sm_ltks,
|
||||
"available_int": 1,
|
||||
}
|
||||
if compilation_template_id:
|
||||
row["compilation_template_ids"] = [compilation_template_id]
|
||||
if doc_ids:
|
||||
row["doc_ids_kwd"] = doc_ids
|
||||
rows.append(row)
|
||||
|
||||
if rows:
|
||||
await thread_pool_exec(settings.docStoreConn.insert, rows, index, kb_id)
|
||||
|
||||
|
||||
async def rebuild_dataset_structure_graph_json(
|
||||
@@ -2272,19 +2353,16 @@ async def rebuild_dataset_structure_graph_json(
|
||||
compile_kwd: str,
|
||||
compilation_template_id: str | None = None,
|
||||
structure_kind: str | None = None,
|
||||
embd_mdl=None,
|
||||
) -> dict:
|
||||
"""Rebuild and persist the KB-wide (dataset) structure graph for one
|
||||
(compile_kwd, template_id) pair.
|
||||
"""Rebuild and persist the KB-wide dataset structure graph.
|
||||
|
||||
Reads every document's merged entities/relations in the KB (no ``doc_id``
|
||||
filter) and writes a single ``knowledge_graph_kwd="dataset_graph"`` row.
|
||||
Meant to run once per template after all per-document flushes finish; when
|
||||
``dataset`` merge scope is on the entity/relation rows are already
|
||||
deduplicated across documents, so this simply projects them into a graph.
|
||||
Reads every document's entity/relation rows in the KB (no ``doc_id``
|
||||
filter) and writes individual ``scope_kwd="dataset"`` entity/relation
|
||||
rows with embeddings and tokenized text — making them searchable.
|
||||
|
||||
``structure_kind`` is the template's top-level kind (e.g. ``knowledge_graph``,
|
||||
``session_graph``); it is stamped on the row so the dataset structure API
|
||||
can filter by kind."""
|
||||
``session_graph``); it is stamped on the meta row so the API can filter."""
|
||||
graph = await _struct_rebuild_graph_json(
|
||||
tenant_id,
|
||||
kb_id,
|
||||
@@ -2299,6 +2377,7 @@ async def rebuild_dataset_structure_graph_json(
|
||||
compile_kwd,
|
||||
compilation_template_id,
|
||||
structure_kind=structure_kind,
|
||||
embd_mdl=embd_mdl,
|
||||
)
|
||||
return graph
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user