mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-13 20:26:51 +08:00
Generate navigation, navigation search (#18096)
### Summary
2 API
POST /api/v1/datasets/{dataset_id}/navigation
GET
/api/v1/datasets/{dataset_id}/navigation/search?q={query}&mode={mode}&top_k={topk}
2 cli
uv run --no-sync python3 admin/client/ragflow_cli.py -h 127.0.0.1 -p
9380 -t user
ragflow> GENERATE NAVIGATION OF DATASET 'frames tree';
ragflow> NAVIGATION SEARCH 'Christie introduced blockchain-based digital
passports' IN DATASET 'frames tree' MODE 'all' topk 20;
(mode: chunk, nav_cluster, nav_doc, navigation_tree, all)
This commit is contained in:
@@ -16,10 +16,11 @@
|
||||
import logging
|
||||
|
||||
from peewee import OperationalError
|
||||
from quart import request, make_response
|
||||
from common.constants import RetCode
|
||||
from api.apps import login_required, current_user
|
||||
from api.utils.api_utils import get_error_argument_result, get_error_data_result, get_json_result, get_result, add_tenant_id_to_kwargs
|
||||
from quart import make_response, request
|
||||
|
||||
from api.apps import current_user, login_required
|
||||
from api.apps.services import dataset_api_service
|
||||
from api.utils.api_utils import add_tenant_id_to_kwargs, get_error_argument_result, get_error_data_result, get_json_result, get_result
|
||||
from api.utils.pagination_utils import DEFAULT_PAGE, DEFAULT_PAGE_SIZE, validate_rest_api_ids, validate_rest_api_page, validate_rest_api_page_size
|
||||
from api.utils.validation_utils import (
|
||||
CreateDatasetReq,
|
||||
@@ -31,7 +32,7 @@ from api.utils.validation_utils import (
|
||||
validate_and_parse_json_request,
|
||||
validate_and_parse_request_args,
|
||||
)
|
||||
from api.apps.services import dataset_api_service
|
||||
from common.constants import RetCode
|
||||
|
||||
|
||||
@manager.route("/datasets/tags/aggregation", methods=["GET"]) # noqa: F821
|
||||
@@ -980,6 +981,51 @@ async def list_dataset_nav(tenant_id, dataset_id):
|
||||
return get_error_data_result(message="Internal server error")
|
||||
|
||||
|
||||
@manager.route("/datasets/<dataset_id>/navigation/search", methods=["GET"]) # noqa: F821
|
||||
@login_required
|
||||
@add_tenant_id_to_kwargs
|
||||
async def search_dataset_nav(tenant_id, dataset_id):
|
||||
"""Unified navigation search across different knowledge layers.
|
||||
|
||||
GET /api/v1/datasets/<dataset_id>/navigation/search?q=<query>&mode=<mode>&top_k=20
|
||||
|
||||
Modes:
|
||||
- nav_doc: navigation tree document leaves (default)
|
||||
- nav_cluster: navigation tree cluster nodes
|
||||
- navigation_tree: tree-structured BFS beam descent
|
||||
- chunk: raw document chunks (deduplicated by doc_id)
|
||||
- all: union of all modes above
|
||||
|
||||
Success: {"code": 0, "data": {"mode": <mode>, "total": <n>,
|
||||
"items": [{"doc_id": str, "score": float}, ...]}}
|
||||
"""
|
||||
q = (request.args.get("q") or "").strip()
|
||||
if not q:
|
||||
return get_result(data={"mode": request.args.get("mode", "nav_doc"), "total": 0, "items": []})
|
||||
mode = (request.args.get("mode") or "nav_doc").strip()
|
||||
top_k_raw = request.args.get("top_k")
|
||||
top_k = None
|
||||
if top_k_raw:
|
||||
try:
|
||||
top_k = max(1, int(top_k_raw))
|
||||
except (ValueError, TypeError):
|
||||
return get_error_data_result(message="top_k must be a positive integer")
|
||||
try:
|
||||
success, result = await dataset_api_service.search_dataset_layers(
|
||||
dataset_id,
|
||||
tenant_id,
|
||||
q,
|
||||
mode,
|
||||
top_k=top_k,
|
||||
)
|
||||
if success:
|
||||
return get_result(data=result)
|
||||
return get_result(data=False, message=result, code=RetCode.AUTHENTICATION_ERROR)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
return get_error_data_result(message="Internal server error")
|
||||
|
||||
|
||||
@manager.route("/datasets/<dataset_id>/navigation/<path:name>/children", methods=["GET"]) # noqa: F821
|
||||
@login_required
|
||||
@add_tenant_id_to_kwargs
|
||||
@@ -1048,6 +1094,38 @@ async def delete_dataset_nav_node(tenant_id, dataset_id, name):
|
||||
return get_error_data_result(message="Internal server error")
|
||||
|
||||
|
||||
@manager.route("/datasets/<dataset_id>/navigation", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
@add_tenant_id_to_kwargs
|
||||
async def generate_dataset_nav(tenant_id, dataset_id):
|
||||
"""Create the entire navigation tree from all dataset documents.
|
||||
|
||||
Deletes any existing navigation tree first, then rebuilds it from
|
||||
scratch. When ``documents`` is provided, only those docs are used;
|
||||
otherwise all docs in the dataset are auto-discovered.
|
||||
|
||||
POST /api/v1/datasets/<dataset_id>/navigation
|
||||
Body (optional): {"documents": [{"doc_id": "...", "summary": "...",
|
||||
"doc_title": "... (optional)", "source_type": "... (optional)"}, ...]}
|
||||
Success: {"code": 0, "data": {"deleted": <n>, "upserted": <n>}}
|
||||
"""
|
||||
try:
|
||||
req = await request.json or {}
|
||||
documents = req.get("documents")
|
||||
|
||||
success, result = await dataset_api_service.generate_nav(
|
||||
dataset_id,
|
||||
tenant_id,
|
||||
documents,
|
||||
)
|
||||
if success:
|
||||
return get_result(data=result)
|
||||
return get_result(data=False, message=result, code=RetCode.AUTHENTICATION_ERROR)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
return get_error_data_result(message="Internal server error")
|
||||
|
||||
|
||||
@manager.route("/datasets/<dataset_id>/skills/<path:skill_kwd>", methods=["DELETE"]) # noqa: F821
|
||||
@login_required
|
||||
@add_tenant_id_to_kwargs
|
||||
|
||||
@@ -13,25 +13,24 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import logging
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from api.db.joint_services.tenant_model_service import resolve_model_config, resolve_model_id, get_composite_model_name_by_ids
|
||||
from common.constants import PAGERANK_FLD, LLMType
|
||||
from common import settings
|
||||
from api.db.db_models import File
|
||||
from api.db.joint_services.tenant_model_service import get_composite_model_name_by_ids, resolve_model_config, resolve_model_id
|
||||
from api.db.services.connector_service import Connector2KbService
|
||||
from api.db.services.document_service import DocumentService, queue_raptor_o_graphrag_tasks
|
||||
from api.db.services.file2document_service import File2DocumentService
|
||||
from api.db.services.file_service import FileService
|
||||
from api.db.services.knowledgebase_service import KnowledgebaseService, validate_dataset_embedding_models
|
||||
from api.db.services.connector_service import Connector2KbService
|
||||
from api.db.services.task_service import GRAPH_RAPTOR_FAKE_DOC_ID, TaskService
|
||||
from api.db.services.tenant_model_service import TenantModelService
|
||||
from api.db.services.user_service import TenantService, UserService, UserTenantService
|
||||
from common.constants import FileSource, StatusEnum
|
||||
from api.utils.api_utils import deep_merge, get_parser_config, remap_dictionary_keys, verify_embedding_availability
|
||||
from common import settings
|
||||
from common.constants import PAGERANK_FLD, FileSource, LLMType, StatusEnum
|
||||
from common.misc_utils import thread_pool_exec, thread_pool_exec_long_time
|
||||
from rag.advanced_rag.knowlege_compile.wiki import WIKI_PAGE_COMPILE_KWD
|
||||
|
||||
@@ -382,8 +381,7 @@ async def update_dataset(tenant_id: str, dataset_id: str, req: dict):
|
||||
from rag.nlp import search
|
||||
|
||||
settings.docStoreConn.update({"exists": PAGERANK_FLD}, {"remove": PAGERANK_FLD}, search.index_name(kb.tenant_id), kb.id)
|
||||
if "parse_type" in req:
|
||||
del req["parse_type"]
|
||||
req.pop("parse_type", None)
|
||||
|
||||
if not KnowledgebaseService.update_by_id(kb.id, req):
|
||||
return False, "Update dataset error.(Database error)"
|
||||
@@ -547,8 +545,8 @@ def delete_knowledge_graph(dataset_id: str, tenant_id: str):
|
||||
if not KnowledgebaseService.accessible(dataset_id, tenant_id):
|
||||
return False, "no authorization"
|
||||
_, kb = KnowledgebaseService.get_by_id(dataset_id)
|
||||
from rag.nlp import search
|
||||
from rag.graphrag.phase_markers import clear_phase_markers
|
||||
from rag.nlp import search
|
||||
|
||||
settings.docStoreConn.delete({"knowledge_graph_kwd": ["graph", "subgraph", "entity", "relation", "community_report"]}, search.index_name(kb.tenant_id), dataset_id)
|
||||
# Wiping the graph invalidates any phase-completion markers used to
|
||||
@@ -934,8 +932,8 @@ def delete_index(dataset_id: str, tenant_id: str, index_type: str, wipe: bool =
|
||||
TaskService.delete_by_id(task_id)
|
||||
|
||||
if wipe and index_type == "graph":
|
||||
from rag.nlp import search
|
||||
from rag.graphrag.phase_markers import clear_phase_markers
|
||||
from rag.nlp import search
|
||||
|
||||
settings.docStoreConn.delete({"knowledge_graph_kwd": ["graph", "subgraph", "entity", "relation", "community_report"]}, search.index_name(kb.tenant_id), dataset_id)
|
||||
# Wiping the graph invalidates any phase-completion markers used to
|
||||
@@ -1173,12 +1171,11 @@ def check_embedding(dataset_id: str, tenant_id: str, req: dict):
|
||||
import random
|
||||
|
||||
import numpy as np
|
||||
from common.constants import RetCode
|
||||
from common.doc_store.doc_store_base import OrderByExpr
|
||||
from rag.nlp import search
|
||||
|
||||
from api.db.services.llm_service import LLMBundle
|
||||
from common.constants import LLMType
|
||||
from common.constants import LLMType, RetCode
|
||||
from common.doc_store.doc_store_base import OrderByExpr
|
||||
from rag.nlp import search
|
||||
|
||||
def _guess_vec_field(src: dict):
|
||||
for k in src or {}:
|
||||
@@ -1843,10 +1840,10 @@ async def get_dataset_structure(dataset_id: str, tenant_id: str, kind: str, keyw
|
||||
return True, empty
|
||||
index_nm, _ = pack
|
||||
|
||||
from common.doc_store.doc_store_base import OrderByExpr
|
||||
from api.apps.services import structure_graph_common as sgc
|
||||
from api.db.services.compilation_template_service import CompilationTemplateService
|
||||
from api.db.services.tenant_llm_service import TenantLLMService
|
||||
from api.apps.services import structure_graph_common as sgc
|
||||
from common.doc_store.doc_store_base import OrderByExpr
|
||||
|
||||
keywords = (keywords or "").strip()
|
||||
_, active_doc_ids = await _current_dataset_docs(dataset_id)
|
||||
@@ -3125,7 +3122,10 @@ def _nav_item(row: dict) -> dict:
|
||||
return {
|
||||
"name": row.get("name") or "",
|
||||
"description": payload.get("description") or "",
|
||||
# doc_id count under this node: the cluster's tally, or 1 for a leaf.
|
||||
"keywords": list(payload.get("keywords") or []),
|
||||
"entities": list(payload.get("entities") or []),
|
||||
"graph_content": payload.get("graph_content") or "",
|
||||
# doc_id count under this node: the cluster`s tally, or 1 for a leaf.
|
||||
"doc_count": int(row.get("doc_count_int") or 0) if is_cluster else 1,
|
||||
"type": "cluster" if is_cluster else "doc",
|
||||
"doc_id": None if is_cluster else (row.get("doc_id") or row.get("name")),
|
||||
@@ -3348,6 +3348,450 @@ async def delete_nav_node(dataset_id: str, tenant_id: str, name: str):
|
||||
logging.exception("delete_nav_node: lock release failed for kb=%s", dataset_id)
|
||||
|
||||
|
||||
async def generate_nav(
|
||||
dataset_id: str,
|
||||
tenant_id: str,
|
||||
documents: list[dict] | None = None,
|
||||
):
|
||||
"""Create the entire navigation tree.
|
||||
|
||||
Deletes any existing navigation tree first, then rebuilds it from
|
||||
scratch. When ``documents`` is provided, only those doc→summary pairs
|
||||
are used; otherwise all documents in the dataset are auto-discovered
|
||||
and inserted into the tree.
|
||||
|
||||
``documents`` is a list of ``{"doc_id": str, "summary": str,
|
||||
"doc_title": str (optional), "source_type": str (optional)}``.
|
||||
|
||||
Returns ``(True, {"deleted": <n>, "upserted": <n>})`` on success.
|
||||
"""
|
||||
if not KnowledgebaseService.accessible(dataset_id, tenant_id):
|
||||
return False, "no authorization"
|
||||
|
||||
_, kb = KnowledgebaseService.get_by_id(dataset_id)
|
||||
if kb is None:
|
||||
return False, "Dataset not found."
|
||||
|
||||
# Resolve models.
|
||||
from api.db.joint_services.tenant_model_service import (
|
||||
get_tenant_default_model_by_type,
|
||||
)
|
||||
from api.db.services.llm_service import LLMBundle
|
||||
from common.constants import LLMType
|
||||
from rag.advanced_rag.knowlege_compile.dataset_nav import upsert_dataset_nav_doc
|
||||
|
||||
if kb.embd_id:
|
||||
embd_model_config = resolve_model_config(kb.tenant_id, LLMType.EMBEDDING, kb.embd_id)
|
||||
else:
|
||||
embd_model_config = get_tenant_default_model_by_type(kb.tenant_id, LLMType.EMBEDDING)
|
||||
embd_mdl = LLMBundle(kb.tenant_id, embd_model_config)
|
||||
|
||||
chat_model_config = get_tenant_default_model_by_type(kb.tenant_id, LLMType.CHAT)
|
||||
chat_mdl = LLMBundle(kb.tenant_id, chat_model_config)
|
||||
|
||||
# Step 0: auto-discover documents when not explicitly provided.
|
||||
if not documents:
|
||||
try:
|
||||
# Query all documents in the dataset directly without
|
||||
# File / File2Document JOINs so that every document
|
||||
# participates in the navigation tree (including docs
|
||||
# created via API that have no file record).
|
||||
from api.db.db_utils import DB
|
||||
from api.db.services.doc_metadata_service import DocMetadataService
|
||||
|
||||
with DB.connection_context():
|
||||
doc_rows = list(
|
||||
DocumentService.model.select(
|
||||
DocumentService.model.id,
|
||||
DocumentService.model.name,
|
||||
)
|
||||
.where(
|
||||
DocumentService.model.kb_id == dataset_id,
|
||||
)
|
||||
.dicts()
|
||||
)
|
||||
doc_ids = [str(row["id"]) for row in doc_rows]
|
||||
metadata_map = DocMetadataService.get_metadata_for_documents(doc_ids, dataset_id) if doc_ids else {}
|
||||
all_docs = []
|
||||
for row in doc_rows:
|
||||
did = str(row["id"])
|
||||
all_docs.append(
|
||||
{
|
||||
"id": did,
|
||||
"name": (row.get("name") or ""),
|
||||
"meta_fields": metadata_map.get(did, {}),
|
||||
}
|
||||
)
|
||||
|
||||
# Prefer RAPTOR-generated summaries stored in the knowledge
|
||||
# graph (compile_kwd="tree", knowledge_graph_kwd="graph")
|
||||
# so that rebuilt nav descriptions match the original
|
||||
# tree-compilation output. Fall back to meta_fields.title
|
||||
# or filename when the graph is absent.
|
||||
raptor_summaries: dict[str, str] = {}
|
||||
pack = _compiled_index_or_none(kb.tenant_id, dataset_id)
|
||||
if pack is not None:
|
||||
try:
|
||||
index_nm, _ = pack
|
||||
from common.doc_store.doc_store_base import OrderByExpr
|
||||
|
||||
graph_res = settings.docStoreConn.search(
|
||||
select_fields=["doc_id", "content_with_weight"],
|
||||
highlight_fields=[],
|
||||
condition={"compile_kwd": ["tree"], "knowledge_graph_kwd": ["graph"]},
|
||||
match_expressions=[],
|
||||
order_by=OrderByExpr(),
|
||||
offset=0,
|
||||
limit=10000,
|
||||
index_names=index_nm,
|
||||
knowledgebase_ids=[dataset_id],
|
||||
)
|
||||
graph_map = settings.docStoreConn.get_fields(graph_res, ["doc_id", "content_with_weight"])
|
||||
for row in (graph_map or {}).values():
|
||||
gid = str(row.get("doc_id") or "")
|
||||
if not gid:
|
||||
continue
|
||||
try:
|
||||
graph = json.loads(row.get("content_with_weight") or "{}")
|
||||
except Exception:
|
||||
continue
|
||||
entities = graph.get("entities") or []
|
||||
relations = graph.get("relations") or []
|
||||
child_names = {r.get("to") for r in relations if isinstance(r, dict)}
|
||||
|
||||
# Build both:
|
||||
# - root_summary: first line of root desc (short,
|
||||
# for the display title / description field)
|
||||
# - graph_text: structured text from ALL entities
|
||||
# and relations (for embedding, keyword extraction,
|
||||
# entity extraction, and stored as graph_content).
|
||||
# entity name -> description
|
||||
name_desc: dict[str, str] = {}
|
||||
for ent in entities:
|
||||
if not isinstance(ent, dict):
|
||||
continue
|
||||
nm = (ent.get("name") or "").strip()
|
||||
if nm:
|
||||
name_desc[nm] = (ent.get("description") or "").strip()
|
||||
|
||||
# root entity = entity whose name never appears as a
|
||||
# relation target (not a child of anyone).
|
||||
root_name = ""
|
||||
root_summary = ""
|
||||
for ent in entities:
|
||||
if isinstance(ent, dict) and ent.get("name") not in child_names:
|
||||
root_name = (ent.get("name") or "").strip()
|
||||
root_summary = name_desc.get(root_name, "")
|
||||
root_summary = root_summary.splitlines()[0].strip() if root_summary else root_name
|
||||
break
|
||||
|
||||
# Build the full graph text
|
||||
graph_parts: list[str] = []
|
||||
if root_name and name_desc.get(root_name):
|
||||
graph_parts.append(root_name)
|
||||
graph_parts.append(name_desc[root_name])
|
||||
|
||||
child_names_set = {n for n in name_desc if n in child_names}
|
||||
if child_names_set:
|
||||
graph_parts.append("")
|
||||
for cname in sorted(child_names_set):
|
||||
cdesc = name_desc.get(cname, "")
|
||||
line = f"- {cname}"
|
||||
if cdesc:
|
||||
line += f": {cdesc.splitlines()[0].strip()}"
|
||||
graph_parts.append(line)
|
||||
|
||||
graph_text = "\n".join(graph_parts) if graph_parts else ""
|
||||
|
||||
if root_summary:
|
||||
raptor_summaries[gid] = {
|
||||
"title": root_summary,
|
||||
"graph_text": graph_text or root_summary,
|
||||
}
|
||||
except Exception:
|
||||
logging.exception("generate_nav: failed to read RAPTOR graph summaries for kb=%s", dataset_id)
|
||||
|
||||
documents = []
|
||||
for d in all_docs:
|
||||
doc_id = str(d.get("id", ""))
|
||||
if not doc_id:
|
||||
continue
|
||||
if doc_id in raptor_summaries:
|
||||
summary = raptor_summaries[doc_id]
|
||||
else:
|
||||
meta = d.get("meta_fields") or {}
|
||||
summary = (meta.get("title") or "").strip() or d.get("name", "")
|
||||
documents.append(
|
||||
{
|
||||
"doc_id": doc_id,
|
||||
"summary": summary,
|
||||
}
|
||||
)
|
||||
|
||||
except Exception:
|
||||
logging.exception("generate_nav: failed to auto-discover docs for kb=%s", dataset_id)
|
||||
return False, "Failed to auto-discover documents."
|
||||
|
||||
if not documents:
|
||||
return False, "No documents found in dataset."
|
||||
|
||||
# Step 1: delete the entire existing navigation tree so we start clean.
|
||||
deleted = 0
|
||||
pack = _compiled_index_or_none(kb.tenant_id, dataset_id)
|
||||
if pack is not None:
|
||||
index_nm, _ = pack
|
||||
try:
|
||||
deleted = await thread_pool_exec(
|
||||
settings.docStoreConn.delete,
|
||||
{"compile_kwd": [_NAV_COMPILE_KWD]},
|
||||
index_nm,
|
||||
dataset_id,
|
||||
)
|
||||
deleted = int(deleted or 0)
|
||||
except Exception:
|
||||
logging.exception("generate_nav: failed to clear existing nav for kb=%s", dataset_id)
|
||||
return False, "Failed to clear existing navigation tree."
|
||||
|
||||
# Step 2: rebuild the tree from the provided doc→summary pairs.
|
||||
upserted = 0
|
||||
for doc in documents or []:
|
||||
doc_id = (doc.get("doc_id") or "").strip()
|
||||
summary = doc.get("summary")
|
||||
# summary can be a plain string or a RAPTOR tree dict.
|
||||
if isinstance(summary, str):
|
||||
summary = summary.strip()
|
||||
if not doc_id or not summary:
|
||||
continue
|
||||
try:
|
||||
await upsert_dataset_nav_doc(
|
||||
tenant_id=tenant_id,
|
||||
kb_id=dataset_id,
|
||||
doc_id=doc_id,
|
||||
summary_or_tree=summary,
|
||||
embd_mdl=embd_mdl,
|
||||
chat_mdl=chat_mdl,
|
||||
)
|
||||
upserted += 1
|
||||
except Exception:
|
||||
logging.exception("generate_nav: failed for doc=%s kb=%s", doc_id, dataset_id)
|
||||
return True, {"deleted": deleted, "upserted": upserted, "failed_doc_id": doc_id}
|
||||
|
||||
return True, {"deleted": deleted, "upserted": upserted}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Unified Explore Search
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_LAYERS_HANDLERS: dict[str, str] = {
|
||||
"nav_doc": "_search_layers_nav_docs",
|
||||
"nav_cluster": "_search_layers_nav_clusters",
|
||||
"navigation_tree": "_search_layers_navigation_tree",
|
||||
"chunk": "_search_layers_chunks",
|
||||
"all": "_search_layers_all",
|
||||
}
|
||||
|
||||
|
||||
async def search_dataset_layers(
|
||||
dataset_id: str,
|
||||
tenant_id: str,
|
||||
query: str,
|
||||
mode: str,
|
||||
*,
|
||||
top_k: int | None = None,
|
||||
) -> tuple[bool, dict]:
|
||||
"""Unified search across different knowledge layers of a dataset.
|
||||
|
||||
Args:
|
||||
mode: One of ``"chunk"``, ``"nav_doc"``, ``"nav_cluster"``, ``"navigation_tree"``, ``"all"``.
|
||||
- chunk: raw document chunks (via the main retrieval pipeline)
|
||||
- nav_doc: navigation tree document leaves
|
||||
- nav_cluster: navigation tree cluster nodes
|
||||
- navigation_tree: tree-structured BFS beam descent
|
||||
- all: union of all modes, deduplicated by doc_id with best score
|
||||
|
||||
Items are shaped as ``{"doc_id": str, "score": float}``.
|
||||
"""
|
||||
from rag.advanced_rag.knowlege_compile.dataset_nav import search_dataset_nav
|
||||
|
||||
if not KnowledgebaseService.accessible(dataset_id, tenant_id):
|
||||
return False, "no authorization"
|
||||
if mode not in _LAYERS_HANDLERS:
|
||||
return False, f"unknown mode: {mode}, expected one of {list(_LAYERS_HANDLERS.keys())}"
|
||||
_, kb = KnowledgebaseService.get_by_id(dataset_id)
|
||||
|
||||
try:
|
||||
from api.db.joint_services.tenant_model_service import get_tenant_default_model_by_type
|
||||
from api.db.services.llm_service import LLMBundle
|
||||
|
||||
if kb.embd_id:
|
||||
embd_model_config = resolve_model_config(kb.tenant_id, LLMType.EMBEDDING, kb.embd_id)
|
||||
else:
|
||||
embd_model_config = get_tenant_default_model_by_type(kb.tenant_id, LLMType.EMBEDDING)
|
||||
embd_mdl = LLMBundle(kb.tenant_id, embd_model_config)
|
||||
except Exception as e:
|
||||
logging.warning(
|
||||
"search_dataset_layers: failed to create LLMBundle(EMBEDDING) for tenant=%s: %s: %s",
|
||||
kb.tenant_id,
|
||||
type(e).__name__,
|
||||
e,
|
||||
)
|
||||
logging.exception("Full traceback for LLMBundle(EMBEDDING) failure")
|
||||
embd_mdl = None
|
||||
|
||||
if mode == "nav_doc":
|
||||
return await _search_layers_nav_docs(tenant_id, dataset_id, query, top_k, embd_mdl, search_dataset_nav)
|
||||
elif mode == "nav_cluster":
|
||||
return await _search_layers_nav_clusters(tenant_id, dataset_id, query, top_k, embd_mdl, search_dataset_nav)
|
||||
elif mode == "navigation_tree":
|
||||
return await _search_layers_navigation_tree(tenant_id, dataset_id, query, top_k, embd_mdl, search_dataset_nav)
|
||||
elif mode == "chunk":
|
||||
return await _search_layers_chunks(tenant_id, dataset_id, query, top_k, embd_mdl, kb)
|
||||
elif mode == "all":
|
||||
return await _search_layers_all(tenant_id, dataset_id, query, top_k, embd_mdl, kb, search_dataset_nav)
|
||||
else:
|
||||
return False, f"unknown mode: {mode}"
|
||||
|
||||
|
||||
async def _search_layers_nav_docs(tenant_id, dataset_id, query, top_k, embd_mdl, search_fn):
|
||||
items = await _nav_search_result(
|
||||
tenant_id,
|
||||
dataset_id,
|
||||
query,
|
||||
top_k,
|
||||
embd_mdl,
|
||||
search_fn,
|
||||
type_kwd="nav_doc",
|
||||
)
|
||||
return True, {"mode": "nav_doc", "total": len(items), "items": items}
|
||||
|
||||
|
||||
async def _search_layers_nav_clusters(tenant_id, dataset_id, query, top_k, embd_mdl, search_fn):
|
||||
items = await _nav_search_result(
|
||||
tenant_id,
|
||||
dataset_id,
|
||||
query,
|
||||
top_k,
|
||||
embd_mdl,
|
||||
search_fn,
|
||||
type_kwd="nav_cluster",
|
||||
)
|
||||
return True, {"mode": "nav_cluster", "total": len(items), "items": items}
|
||||
|
||||
|
||||
async def _search_layers_navigation_tree(tenant_id, dataset_id, query, top_k, embd_mdl, search_fn):
|
||||
from rag.advanced_rag.knowlege_compile.dataset_nav import search_nav_tree_descent
|
||||
|
||||
items = await search_nav_tree_descent(
|
||||
tenant_id,
|
||||
dataset_id,
|
||||
query,
|
||||
embd_mdl,
|
||||
top_k=top_k,
|
||||
)
|
||||
return True, {"mode": "navigation_tree", "total": len(items), "items": items}
|
||||
|
||||
|
||||
async def _nav_search_result(tenant_id, dataset_id, query, top_k, embd_mdl, search_fn, **kwargs):
|
||||
results = await search_fn(
|
||||
tenant_id,
|
||||
dataset_id,
|
||||
query,
|
||||
embd_mdl=embd_mdl,
|
||||
top_k=top_k,
|
||||
**kwargs,
|
||||
)
|
||||
items: list[dict] = []
|
||||
for r in results:
|
||||
doc_id = (r.get("doc_id") or "").strip() if isinstance(r.get("doc_id"), str) else (r.get("doc_ids") or [None])[0]
|
||||
items.append(
|
||||
{
|
||||
"doc_id": str(doc_id) if doc_id else "",
|
||||
"score": round(float(r.get("score", 0.0)), 4),
|
||||
}
|
||||
)
|
||||
return items
|
||||
|
||||
|
||||
async def _search_layers_chunks(tenant_id, dataset_id, query, top_k, embd_mdl, kb):
|
||||
from common import settings
|
||||
|
||||
tenant_ids = [tenant_id]
|
||||
|
||||
kwargs = {}
|
||||
if top_k is not None:
|
||||
kwargs["top"] = top_k
|
||||
|
||||
fetch_k = max(top_k, 10) * 3 if top_k is not None else 1024
|
||||
try:
|
||||
ranks = await settings.retriever.retrieval(
|
||||
query,
|
||||
embd_mdl,
|
||||
tenant_ids,
|
||||
[dataset_id],
|
||||
1,
|
||||
fetch_k,
|
||||
0.0,
|
||||
0.3,
|
||||
**kwargs,
|
||||
)
|
||||
except Exception:
|
||||
return False, "chunk retrieval failed"
|
||||
|
||||
doc_scores: dict[str, float] = {}
|
||||
for c in ranks.get("chunks", []):
|
||||
doc_id = (c.get("doc_id") or "").strip()
|
||||
score = float(c.get("similarity") or c.get("score") or 0.0)
|
||||
if doc_id and score > doc_scores.get(doc_id, -1.0):
|
||||
doc_scores[doc_id] = score
|
||||
|
||||
items = sorted(
|
||||
({"doc_id": d, "score": round(s, 4)} for d, s in doc_scores.items()),
|
||||
key=lambda x: x["score"],
|
||||
reverse=True,
|
||||
)
|
||||
if top_k is not None and top_k > 0:
|
||||
items = items[:top_k]
|
||||
|
||||
return True, {"mode": "chunk", "total": len(items), "items": items}
|
||||
|
||||
|
||||
async def _search_layers_all(tenant_id, dataset_id, query, top_k, embd_mdl, kb, search_fn):
|
||||
"""Run all modes and return the union of doc_ids, with best score per doc."""
|
||||
import asyncio as _asyncio
|
||||
|
||||
result_lists = await _asyncio.gather(
|
||||
_search_layers_nav_docs(tenant_id, dataset_id, query, top_k, embd_mdl, search_fn),
|
||||
_search_layers_nav_clusters(tenant_id, dataset_id, query, top_k, embd_mdl, search_fn),
|
||||
_search_layers_navigation_tree(tenant_id, dataset_id, query, top_k, embd_mdl, search_fn),
|
||||
_search_layers_chunks(tenant_id, dataset_id, query, top_k, embd_mdl, kb),
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
doc_scores: dict[str, float] = {}
|
||||
for result in result_lists:
|
||||
if isinstance(result, Exception):
|
||||
continue
|
||||
ok, data = result
|
||||
if not ok:
|
||||
continue
|
||||
for item in data.get("items", []):
|
||||
doc_id = item.get("doc_id", "")
|
||||
score = float(item.get("score", 0.0))
|
||||
if doc_id and score > doc_scores.get(doc_id, -1.0):
|
||||
doc_scores[doc_id] = score
|
||||
|
||||
items = sorted(
|
||||
({"doc_id": d, "score": round(s, 4)} for d, s in doc_scores.items()),
|
||||
key=lambda x: x["score"],
|
||||
reverse=True,
|
||||
)
|
||||
if top_k is not None and top_k > 0:
|
||||
items = items[:top_k]
|
||||
|
||||
return True, {"mode": "all", "total": len(items), "items": items}
|
||||
|
||||
|
||||
async def update_wiki_page(
|
||||
dataset_id: str,
|
||||
tenant_id: str,
|
||||
@@ -3390,11 +3834,11 @@ async def update_wiki_page(
|
||||
return True, None
|
||||
index_nm, _ = pack
|
||||
|
||||
from rag.advanced_rag.knowlege_compile.wiki import (
|
||||
_wiki_transform_links,
|
||||
_wiki_extract_summary,
|
||||
)
|
||||
from api.db.services.file_commit_service import FileCommitService
|
||||
from rag.advanced_rag.knowlege_compile.wiki import (
|
||||
_wiki_extract_summary,
|
||||
_wiki_transform_links,
|
||||
)
|
||||
|
||||
full_slug = f"{page_type}/{slug}" if "/" not in slug else slug
|
||||
|
||||
|
||||
Reference in New Issue
Block a user